Add preview tab + point and click editor (vibe-kanban) (#870)

* Perfect! The preview tab implementation is complete and all type checks pass. Let me provide a summary of what was implemented:

1. **Updated `TabType`** - Added 'preview' to the union type
2. **Created `useDevserverPreview` hook** - Intelligent devserver URL detection with:
   - Real-time log parsing using the regex `(?:localhost|127\.0\.0\.1|0\.0\.0\.0):(\d+)[^\d]*`
   - Three states: idle/searching/ready
   - URL normalization (0.0.0.0 → localhost)
   - Process monitoring for setup scripts and dev servers

3. **Built `PreviewTab` component** - Full-featured iframe preview with:
   - Proper sandbox settings for security
   - Toolbar with refresh, copy URL, and open external buttons
   - Smart error handling for mixed content/CORS issues
   - Clear empty states and loading indicators

4. **Enhanced `TabNavigation`** - Conditionally shows preview tab with:
   - Monitor icon and spinner when searching
   - Only appears when devserver is detected or likely

5. **Integrated in `TaskDetailsPanel`** - Wires everything together in fullscreen mode

- **Smart Detection**: Parses logs in real-time to find devserver URLs
- **Robust Error Handling**: Graceful fallbacks for iframe loading issues
- **Security**: Proper iframe sandboxing with necessary permissions
- **Performance**: Efficient log processing with deduplication
- **UX**: Clear states (searching/ready/error) with helpful messaging

The implementation follows the Oracle's architectural recommendations with proper separation of concerns and real-time detection capabilities.

* Cleanup script changes for task attempt 4f59c374-1cc5-4b3c-a79a-5f63756a366b

* Perfect! Now the preview tab will use the **first** dev server URL it detects rather than being overwritten by subsequent matches. This means:

- If both backend (e.g., `:3001`) and frontend (e.g., `:5173`) servers start
- The preview tab will show whichever URL appears first in the logs
- Subsequent URLs will be ignored to avoid switching the preview unexpectedly

This provides a more stable preview experience when multiple dev servers are running.

* Cleanup script changes for task attempt 4f59c374-1cc5-4b3c-a79a-5f63756a366b

* add parent lib

* fmt

* Listen for communication from the preview tab (vibe-kanban ace46045)

In frontend/src/components/tasks/TaskDetails/PreviewTab.tsx

We should expect that the iframe will communicate via frontend/src/utils/previewBridge.ts

When a message is received, we should add some details about the clicked element to the follow up textarea

* Component to view clicked element (vibe-kanban e3b90cc1)

frontend/src/components/tasks/TaskDetails/PreviewTab.tsx
frontend/src/components/tasks/TaskFollowUpSection.tsx

When a user clicks on an element, we should display a box in the follow up section similar to how we show reviews or conflicts.

The section should display a summary of each of the elements, the name of the component and the file location.

When the user sends a follow up, a markdown equivalent of the summary should be appended to the top of the follow up message.

* Component to view clicked element (vibe-kanban e3b90cc1)

frontend/src/components/tasks/TaskDetails/PreviewTab.tsx
frontend/src/components/tasks/TaskFollowUpSection.tsx

When a user clicks on an element, we should display a box in the follow up section similar to how we show reviews or conflicts.

The section should display a summary of each of the elements, the name of the component and the file location.

When the user sends a follow up, a markdown equivalent of the summary should be appended to the top of the follow up message.

* Tweaks to component click (vibe-kanban 756e1212)

Preview tab frontend/src/components/tasks/TaskDetails/PreviewTab.tsx
- Preview should remember which URL you were on
- Auto select the follow up box after point and click, so you can type feedback

Clicked elements: frontend/src/components/tasks/ClickedElementsBanner.tsx, frontend/src/contexts/ClickedElementsProvider.tsx
- The list of components should not overflow horizontally, instead we should truncate, omiting components from the left first
- If the user clicks on a component, it should omit the downstream components from the list, they should be displayed disabled and the prompt should start from the selected component

* strip ansi when parsing dev server URL

* cleanup

* cleanup

* improve help copy

* start dev server from preview page

* dev server wip

* restructure

* instructions

* fix

* restructur

* fmt

* i18n

* i18n fix

* config fix

* wip cleanup

* minor cleanup

* Preview tab feedback (vibe-kanban d531fff8)

In the PreviewToolbar, each icon button should have a tooltip

* fix + fmt

* move dev script textarea

* improve when help is shown

* i18n

* improve URL matching

* fix close logs

* auto install companion

* cleanup notices

* Copy tweak
This commit is contained in:
Louis Knight-Webb
2025-10-01 17:15:12 +01:00
committed by GitHub
parent 0ace01b55f
commit 2781e3651b
24 changed files with 2224 additions and 282 deletions

View File

@@ -5,10 +5,11 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { AlertTriangle, Plus } from 'lucide-react';
import { Loader } from '@/components/ui/loader';
import { projectsApi, tasksApi, attemptsApi } from '@/lib/api';
import { tasksApi, attemptsApi } from '@/lib/api';
import { openTaskForm } from '@/lib/openTaskForm';
import { useSearch } from '@/contexts/search-context';
import { useProject } from '@/contexts/project-context';
import { useQuery } from '@tanstack/react-query';
import { useTaskViewManager } from '@/hooks/useTaskViewManager';
import {
@@ -32,7 +33,7 @@ import {
import TaskKanbanBoard from '@/components/tasks/TaskKanbanBoard';
import { TaskDetailsPanel } from '@/components/tasks/TaskDetailsPanel';
import type { TaskWithAttemptStatus, Project, TaskAttempt } from 'shared/types';
import type { TaskWithAttemptStatus, TaskAttempt } from 'shared/types';
import type { DragEndEvent } from '@/components/ui/shadcn-io/kanban';
import { useProjectTasks } from '@/hooks/useProjectTasks';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
@@ -43,7 +44,7 @@ type Task = TaskWithAttemptStatus;
export function ProjectTasks() {
const { t } = useTranslation(['tasks', 'common']);
const { projectId, taskId, attemptId } = useParams<{
const { taskId, attemptId } = useParams<{
projectId: string;
taskId?: string;
attemptId?: string;
@@ -51,6 +52,14 @@ export function ProjectTasks() {
const navigate = useNavigate();
const { enableScope, disableScope } = useHotkeysContext();
// Use project context for project data
const {
project,
projectId,
isLoading: projectLoading,
error: projectError,
} = useProject();
useEffect(() => {
enableScope(Scope.KANBAN);
@@ -59,25 +68,22 @@ export function ProjectTasks() {
};
}, [enableScope, disableScope]);
const [project, setProject] = useState<Project | null>(null);
const [error, setError] = useState<string | null>(null);
// Helper functions to open task forms
const handleCreateTask = () => {
if (project?.id) {
openTaskForm({ projectId: project.id });
if (projectId) {
openTaskForm({ projectId });
}
};
const handleEditTask = (task: Task) => {
if (project?.id) {
openTaskForm({ projectId: project.id, task });
if (projectId) {
openTaskForm({ projectId, task });
}
};
const handleDuplicateTask = (task: Task) => {
if (project?.id) {
openTaskForm({ projectId: project.id, initialTask: task });
if (projectId) {
openTaskForm({ projectId, initialTask: task });
}
};
const { query: searchQuery, focusInput } = useSearch();
@@ -280,17 +286,6 @@ export function ProjectTasks() {
}
);
// Full screen
const fetchProject = useCallback(async () => {
try {
const result = await projectsApi.getById(projectId!);
setProject(result);
} catch (err) {
setError('Failed to load project');
}
}, [projectId]);
const handleClosePanel = useCallback(() => {
// setIsPanelOpen(false);
// setSelectedTask(null);
@@ -458,26 +453,16 @@ export function ProjectTasks() {
});
// UI will update via WebSocket stream
} catch (err) {
setError('Failed to update task status');
console.error('Failed to update task status:', err);
}
},
[tasksById]
);
// Initialize project when projectId changes
useEffect(() => {
if (projectId) {
fetchProject();
}
}, [projectId, fetchProject]);
// Combine loading states for initial load
const isInitialTasksLoad = isLoading && tasks.length === 0;
// Remove legacy direct-navigation handler; live sync above covers this
if (isLoading) {
return <Loader message={t('loading')} size={32} className="py-8" />;
}
if (error) {
if (projectError) {
return (
<div className="p-4">
<Alert>
@@ -485,12 +470,18 @@ export function ProjectTasks() {
<AlertTriangle size="16" />
{t('common:states.error')}
</AlertTitle>
<AlertDescription>{error}</AlertDescription>
<AlertDescription>
{projectError.message || 'Failed to load project'}
</AlertDescription>
</Alert>
</div>
);
}
if (projectLoading && isInitialTasksLoad) {
return <Loader message={t('loading')} size={32} className="py-8" />;
}
return (
<div
className={`min-h-full ${getMainContainerClasses(isPanelOpen, isFullscreen)}`}
@@ -548,7 +539,7 @@ export function ProjectTasks() {
</div>
{/* Right Column - Task Details Panel */}
{isPanelOpen && (
{isPanelOpen && !projectLoading && (
<TaskDetailsPanel
task={selectedTask}
projectHasDevScript={!!project?.dev_script}