mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-10-09 05:07:53 +00:00
feat(web): migrate task preparation into Plan mode (#1345)
This commit is contained in:
parent
c36fa5bb48
commit
1be8b9de38
10 changed files with 232 additions and 68 deletions
|
|
@ -12,6 +12,13 @@ const mocks = vi.hoisted(() => ({
|
|||
updateField: vi.fn(),
|
||||
updateProgress: vi.fn(),
|
||||
onOpenChange: vi.fn(),
|
||||
taskFeatureSettings: {
|
||||
enableAttachments: true,
|
||||
enableComments: false,
|
||||
enableDependencies: false,
|
||||
enableTimeTracking: false,
|
||||
},
|
||||
progressContent: { value: '## Learnings\n- Mantine task detail renders' },
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/useDebouncedSave', () => ({
|
||||
|
|
@ -48,10 +55,7 @@ vi.mock('@/hooks/useFeatureSettings', () => ({
|
|||
useFeatureSettings: () => ({
|
||||
settings: {
|
||||
tasks: {
|
||||
enableAttachments: true,
|
||||
enableComments: false,
|
||||
enableDependencies: false,
|
||||
enableTimeTracking: false,
|
||||
...mocks.taskFeatureSettings,
|
||||
},
|
||||
agents: {
|
||||
enablePreview: true,
|
||||
|
|
@ -97,7 +101,7 @@ vi.mock('@/hooks/useTasks', () => ({
|
|||
|
||||
vi.mock('@/hooks/useTaskProgress', () => ({
|
||||
useTaskProgress: () => ({
|
||||
data: '## Learnings\n- Mantine task detail renders',
|
||||
data: mocks.progressContent.value,
|
||||
isLoading: false,
|
||||
}),
|
||||
useUpdateProgress: () => ({ mutateAsync: mocks.updateProgress, isPending: false }),
|
||||
|
|
@ -131,6 +135,10 @@ vi.mock('@/components/task/AttachmentsSection', () => ({
|
|||
AttachmentsSection: () => <div>Attachments section</div>,
|
||||
}));
|
||||
|
||||
vi.mock('@/components/task/DependenciesSection', () => ({
|
||||
DependenciesSection: () => <div>Dependencies section</div>,
|
||||
}));
|
||||
|
||||
vi.mock('@/components/task/ObservationsSection', () => ({
|
||||
ObservationsSection: () => <div>Observations section</div>,
|
||||
}));
|
||||
|
|
@ -195,6 +203,13 @@ describe('task detail Mantine migration', () => {
|
|||
mocks.archiveTask.mockResolvedValue(undefined);
|
||||
mocks.deleteTask.mockResolvedValue(undefined);
|
||||
mocks.updateProgress.mockResolvedValue(undefined);
|
||||
Object.assign(mocks.taskFeatureSettings, {
|
||||
enableAttachments: true,
|
||||
enableComments: false,
|
||||
enableDependencies: false,
|
||||
enableTimeTracking: false,
|
||||
});
|
||||
mocks.progressContent.value = '## Learnings\n- Mantine task detail renders';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -272,6 +287,30 @@ describe('task detail Mantine migration', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('groups every enabled preparation destination under Plan with stable wayfinding', async () => {
|
||||
mocks.taskFeatureSettings.enableDependencies = true;
|
||||
const user = userEvent.setup();
|
||||
renderTaskDetail();
|
||||
|
||||
const planSections = screen.getByRole('tablist', { name: 'Plan sections' });
|
||||
expect(
|
||||
within(planSections)
|
||||
.getAllByRole('tab')
|
||||
.map((tab) => tab.textContent)
|
||||
).toEqual(['Details', 'Progress', 'Observations', 'Dependencies', 'Attachments']);
|
||||
|
||||
await user.click(within(planSections).getByRole('tab', { name: 'Dependencies' }));
|
||||
|
||||
const scrollRegion = screen.getByTestId('task-detail-scroll-region');
|
||||
expect(scrollRegion.getAttribute('aria-labelledby')).toBe(
|
||||
'task-workspace-mode-heading task-workspace-section-heading'
|
||||
);
|
||||
expect(within(scrollRegion).getByRole('heading', { level: 3 }).textContent).toBe(
|
||||
'Dependencies'
|
||||
);
|
||||
expect(await within(scrollRegion).findByText('Dependencies section')).toBeDefined();
|
||||
});
|
||||
|
||||
it('applies description height and vertical resizing to the textarea input', () => {
|
||||
renderWithProviders(
|
||||
<MarkdownEditor
|
||||
|
|
@ -332,6 +371,21 @@ describe('task detail Mantine migration', () => {
|
|||
expect(baseElement.querySelector('[data-slot="textarea"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps empty progress notes compact until editing starts', async () => {
|
||||
mocks.progressContent.value = '';
|
||||
const user = userEvent.setup();
|
||||
renderTaskDetail();
|
||||
|
||||
await user.click(screen.getByRole('tab', { name: 'Progress' }));
|
||||
|
||||
expect(await screen.findByText('No progress notes yet')).toBeDefined();
|
||||
expect(screen.queryByText('Progress Notes Best Practices:')).toBeNull();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Add notes' }));
|
||||
|
||||
expect(screen.getByPlaceholderText(/Document insights discovered during work/)).toBeDefined();
|
||||
});
|
||||
|
||||
it('uses a direct Mantine modal for destructive delete confirmation', async () => {
|
||||
const user = userEvent.setup();
|
||||
const { baseElement } = renderTaskDetail();
|
||||
|
|
@ -426,6 +480,41 @@ describe('task detail Mantine migration', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('preserves legacy Plan deep links for optional preparation sections', async () => {
|
||||
mocks.taskFeatureSettings.enableDependencies = true;
|
||||
const task = createMockTask({ id: 'task-plan-link', title: 'Prepare a task' });
|
||||
|
||||
const { rerender } = renderWithProviders(
|
||||
<TaskDetailPanel
|
||||
task={task}
|
||||
open
|
||||
onOpenChange={mocks.onOpenChange}
|
||||
navigationTarget={{ tab: 'dependencies' }}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole('tab', { name: 'Dependencies' }).getAttribute('aria-selected')).toBe(
|
||||
'true'
|
||||
)
|
||||
);
|
||||
|
||||
rerender(
|
||||
<TaskDetailPanel
|
||||
task={task}
|
||||
open
|
||||
onOpenChange={mocks.onOpenChange}
|
||||
navigationTarget={{ workspace: { version: 1, mode: 'plan', section: 'attachments' } }}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole('tab', { name: 'Attachments' }).getAttribute('aria-selected')).toBe(
|
||||
'true'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back when the active tab becomes unavailable for the task data', async () => {
|
||||
const user = userEvent.setup();
|
||||
const codeTask = createMockTask({
|
||||
|
|
|
|||
|
|
@ -198,6 +198,27 @@ describe('task detail support sections Mantine migration', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('keeps an empty attachment section compact while preserving upload access', () => {
|
||||
const task = createMockTask({ id: 'task-empty-attachments', attachments: [] });
|
||||
const { container } = renderWithProviders(<AttachmentsSection task={task} />);
|
||||
|
||||
const uploadTarget = screen.getByRole('button', { name: 'Upload attachments' });
|
||||
expect(uploadTarget.className).toContain('p-4');
|
||||
expect(uploadTarget.className).not.toContain('p-8');
|
||||
expect(screen.getByText('Add supporting files')).toBeDefined();
|
||||
expect(screen.queryByText('No attachments yet')).toBeNull();
|
||||
|
||||
const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
fireEvent.change(fileInput, {
|
||||
target: { files: [new File(['plan'], 'plan.md', { type: 'text/markdown' })] },
|
||||
});
|
||||
|
||||
expect(mocks.uploadAttachment).toHaveBeenCalledWith({
|
||||
taskId: task.id,
|
||||
formData: expect.any(FormData),
|
||||
});
|
||||
});
|
||||
|
||||
it('renders observations through direct Mantine select, slider, textarea, badges, and modal', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onAddObservation = vi.fn().mockResolvedValue(undefined);
|
||||
|
|
|
|||
|
|
@ -264,6 +264,14 @@ export function AttachmentsSection({ task }: AttachmentsSectionProps) {
|
|||
</Alert>
|
||||
)}
|
||||
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
onChange={(e) => handleFileSelect(e.target.files)}
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
{/* Upload zone */}
|
||||
<Paper
|
||||
role="button"
|
||||
|
|
@ -279,39 +287,46 @@ export function AttachmentsSection({ task }: AttachmentsSectionProps) {
|
|||
}
|
||||
}}
|
||||
className={cn(
|
||||
'border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors',
|
||||
'cursor-pointer rounded-lg border-2 border-dashed transition-colors',
|
||||
attachments.length === 0 ? 'p-4' : 'p-8 text-center',
|
||||
isDragging && 'border-primary bg-primary/5',
|
||||
!isDragging && 'border-muted-foreground/25 hover:border-muted-foreground/50',
|
||||
uploadAttachment.isPending && 'opacity-50 pointer-events-none'
|
||||
uploadAttachment.isPending && 'pointer-events-none opacity-50'
|
||||
)}
|
||||
radius="lg"
|
||||
aria-label="Upload attachments"
|
||||
>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
onChange={(e) => handleFileSelect(e.target.files)}
|
||||
className="hidden"
|
||||
/>
|
||||
<ThemeIcon variant="transparent" color="gray" size="xl" className="mx-auto mb-2">
|
||||
<Upload className="h-8 w-8 text-muted-foreground" />
|
||||
</ThemeIcon>
|
||||
<Text size="sm" c="dimmed" className="mb-1">
|
||||
{uploadAttachment.isPending ? 'Uploading...' : 'Drop files here or click to browse'}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
Max 10MB per file, 20 files total
|
||||
</Text>
|
||||
{attachments.length === 0 ? (
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
<ThemeIcon variant="light" color="gray" size="md" className="flex-shrink-0">
|
||||
<Upload className="h-4 w-4" />
|
||||
</ThemeIcon>
|
||||
<div className="min-w-0">
|
||||
<Text size="sm" fw={500}>
|
||||
{uploadAttachment.isPending ? 'Uploading...' : 'Add supporting files'}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
Drop files here or browse. Max 10MB per file.
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
) : (
|
||||
<>
|
||||
<ThemeIcon variant="transparent" color="gray" size="xl" className="mx-auto mb-2">
|
||||
<Upload className="h-8 w-8 text-muted-foreground" />
|
||||
</ThemeIcon>
|
||||
<Text size="sm" c="dimmed" className="mb-1">
|
||||
{uploadAttachment.isPending ? 'Uploading...' : 'Drop files here or click to browse'}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
Max 10MB per file, 20 files total
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{/* Attachments list */}
|
||||
{attachments.length === 0 ? (
|
||||
<Paper className="py-4 text-center" radius="md" withBorder>
|
||||
<Text size="sm" c="dimmed" fs="italic">
|
||||
No attachments yet
|
||||
</Text>
|
||||
</Paper>
|
||||
) : (
|
||||
{attachments.length > 0 && (
|
||||
<Stack gap="xs">
|
||||
{attachments.map((attachment) => (
|
||||
<AttachmentItem key={attachment.id} taskId={task.id} attachment={attachment} />
|
||||
|
|
|
|||
|
|
@ -276,7 +276,7 @@ export function ObservationsSection({
|
|||
|
||||
<Stack gap="xs">
|
||||
{observations.length === 0 && !isAdding && (
|
||||
<Text size="sm" c="dimmed" ta="center" className="py-8">
|
||||
<Text size="sm" c="dimmed" ta="center" className="py-3">
|
||||
No observations yet. Add context, decisions, insights, or blockers as you work on this
|
||||
task.
|
||||
</Text>
|
||||
|
|
|
|||
|
|
@ -133,8 +133,9 @@ export function TaskDetailPanel({
|
|||
isCodeTask,
|
||||
hasWorktree,
|
||||
attachmentsEnabled: taskSettings.enableAttachments,
|
||||
dependenciesEnabled: taskSettings.enableDependencies,
|
||||
}),
|
||||
[hasWorktree, isCodeTask, taskSettings.enableAttachments]
|
||||
[hasWorktree, isCodeTask, taskSettings.enableAttachments, taskSettings.enableDependencies]
|
||||
);
|
||||
const visibleTabs = useMemo(
|
||||
() => getAvailableTaskDetailTabs(tabAvailabilityContext),
|
||||
|
|
@ -151,6 +152,7 @@ export function TaskDetailPanel({
|
|||
() => visibleTabs.filter((tab) => getTaskWorkspaceDestination(tab.id).mode === activeMode),
|
||||
[activeMode, visibleTabs]
|
||||
);
|
||||
const activeTabMetadata = visibleTabs.find((tab) => tab.id === activeTab);
|
||||
|
||||
const addObservationForTask = useMemo(
|
||||
() => async (data: TaskDetailObservationInput) => {
|
||||
|
|
@ -401,7 +403,7 @@ export function TaskDetailPanel({
|
|||
|
||||
<Group justify="space-between" align="flex-start" wrap="wrap" gap="sm">
|
||||
<div className="min-w-0">
|
||||
<Text component="h2" fw={650} size="sm">
|
||||
<Text id="task-workspace-mode-heading" component="h2" fw={650} size="sm">
|
||||
{activeModeMetadata?.label ?? 'Workspace'}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" className="mt-0.5 max-w-xl">
|
||||
|
|
@ -490,9 +492,12 @@ export function TaskDetailPanel({
|
|||
<div
|
||||
className="veritas-overlay-scroll min-h-0 flex-1 overflow-y-scroll overscroll-contain px-4 py-4 sm:px-5 sm:py-5"
|
||||
data-testid="task-detail-scroll-region"
|
||||
aria-label={`${activeModeMetadata?.label ?? 'Task workspace'} content`}
|
||||
aria-labelledby="task-workspace-mode-heading task-workspace-section-heading"
|
||||
tabIndex={0}
|
||||
>
|
||||
<Text id="task-workspace-section-heading" component="h3" className="sr-only">
|
||||
{activeTabMetadata?.label ?? 'Task details'}
|
||||
</Text>
|
||||
{visibleTabs.map((tab) => {
|
||||
const tabContent = (
|
||||
<Suspense
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export function ProgressTab({ task }: ProgressTabProps) {
|
|||
onClick={handleEdit}
|
||||
leftSection={<Pencil className="h-3 w-3" />}
|
||||
>
|
||||
Edit
|
||||
{isEmpty ? 'Add notes' : 'Edit'}
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
|
|
@ -118,17 +118,22 @@ export function ProgressTab({ task }: ProgressTabProps) {
|
|||
|
||||
{/* View Mode */}
|
||||
{!isEditing && (
|
||||
<Paper className="min-h-[200px] border bg-card p-4" radius="lg">
|
||||
<Paper
|
||||
className={isEmpty ? 'border bg-card p-4' : 'min-h-[200px] border bg-card p-4'}
|
||||
radius="lg"
|
||||
>
|
||||
{isEmpty ? (
|
||||
<Stack align="center" gap={4} className="py-8 text-center text-muted-foreground">
|
||||
<ThemeIcon color="gray" variant="subtle" size={48}>
|
||||
<FileText className="h-8 w-8 opacity-50" />
|
||||
<Group gap="sm" wrap="nowrap" className="text-muted-foreground">
|
||||
<ThemeIcon color="gray" variant="light" size="md" className="flex-shrink-0">
|
||||
<FileText className="h-4 w-4" />
|
||||
</ThemeIcon>
|
||||
<Text size="sm">No progress notes yet</Text>
|
||||
<Text size="xs">
|
||||
Click Edit to add learnings, issues, and next steps for future sessions
|
||||
</Text>
|
||||
</Stack>
|
||||
<div className="min-w-0">
|
||||
<Text size="sm" fw={500}>
|
||||
No progress notes yet
|
||||
</Text>
|
||||
<Text size="xs">Add learnings, issues, and next steps when they are useful.</Text>
|
||||
</div>
|
||||
</Group>
|
||||
) : (
|
||||
<MarkdownText>{progress}</MarkdownText>
|
||||
)}
|
||||
|
|
@ -136,17 +141,19 @@ export function ProgressTab({ task }: ProgressTabProps) {
|
|||
)}
|
||||
|
||||
{/* Help Text */}
|
||||
<Stack gap={4} className="border-t pt-3 text-xs text-muted-foreground">
|
||||
<Text size="xs" fw={500}>
|
||||
Progress Notes Best Practices:
|
||||
</Text>
|
||||
<ul className="list-disc list-inside space-y-1 ml-2">
|
||||
<li>Document key learnings and insights discovered during work</li>
|
||||
<li>Track issues encountered and their solutions</li>
|
||||
<li>List next steps for future sessions to pick up where you left off</li>
|
||||
<li>Use markdown sections (##) to organize by category</li>
|
||||
</ul>
|
||||
</Stack>
|
||||
{!isEmpty && !isEditing && (
|
||||
<Stack gap={4} className="border-t pt-3 text-xs text-muted-foreground">
|
||||
<Text size="xs" fw={500}>
|
||||
Progress Notes Best Practices:
|
||||
</Text>
|
||||
<ul className="list-disc list-inside space-y-1 ml-2">
|
||||
<li>Document key learnings and insights discovered during work</li>
|
||||
<li>Track issues encountered and their solutions</li>
|
||||
<li>List next steps for future sessions to pick up where you left off</li>
|
||||
<li>Use markdown sections (##) to organize by category</li>
|
||||
</ul>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { MarkdownRenderer } from '@/components/ui/MarkdownRenderer';
|
|||
import { TaskMetadataSection } from './TaskMetadataSection';
|
||||
import { SubtasksSection } from '../SubtasksSection';
|
||||
import { VerificationSection } from '../VerificationSection';
|
||||
import { DependenciesSection } from '../DependenciesSection';
|
||||
import { TimeTrackingSection } from '../TimeTrackingSection';
|
||||
import { CommentsSection } from '../CommentsSection';
|
||||
import { DeliverablesSection } from '../DeliverablesSection';
|
||||
|
|
@ -200,16 +199,6 @@ export function TaskDetailsTab({
|
|||
<VerificationSection task={task} />
|
||||
</Box>
|
||||
|
||||
{/* Dependencies */}
|
||||
{taskSettings.enableDependencies && (
|
||||
<Box className="border-t pt-4">
|
||||
<DependenciesSection
|
||||
task={task}
|
||||
onBlockedByChange={(blockedBy) => onUpdate('blockedBy', blockedBy)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Time Tracking */}
|
||||
{taskSettings.enableTimeTracking && (
|
||||
<Box className="border-t pt-4">
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
Files,
|
||||
GitBranch,
|
||||
History,
|
||||
Network,
|
||||
NotebookPen,
|
||||
Paperclip,
|
||||
type LucideIcon,
|
||||
|
|
@ -42,6 +43,9 @@ const AgentRunTimelinePanel = lazy(() =>
|
|||
const AttachmentsSection = lazy(() =>
|
||||
import('./AttachmentsSection').then((mod) => ({ default: mod.AttachmentsSection }))
|
||||
);
|
||||
const DependenciesSection = lazy(() =>
|
||||
import('./DependenciesSection').then((mod) => ({ default: mod.DependenciesSection }))
|
||||
);
|
||||
const DiffViewer = lazy(() => import('./DiffViewer').then((mod) => ({ default: mod.DiffViewer })));
|
||||
const EvidenceTimelinePanel = lazy(() =>
|
||||
import('@/components/evidence/EvidenceTimelinePanel').then((mod) => ({
|
||||
|
|
@ -124,6 +128,7 @@ const TAB_ICONS: Record<TaskDetailTabIcon, LucideIcon> = {
|
|||
Files,
|
||||
GitBranch,
|
||||
History,
|
||||
Network,
|
||||
NotebookPen,
|
||||
Paperclip,
|
||||
};
|
||||
|
|
@ -157,6 +162,12 @@ const TAB_RENDERERS: Record<TaskDetailTabId, (context: TaskDetailRenderContext)
|
|||
onDeleteObservation={deleteObservation}
|
||||
/>
|
||||
),
|
||||
dependencies: ({ task, updateField }) => (
|
||||
<DependenciesSection
|
||||
task={task}
|
||||
onBlockedByChange={(blockedBy) => updateField('blockedBy', blockedBy)}
|
||||
/>
|
||||
),
|
||||
attachments: ({ task }) => <AttachmentsSection task={task} />,
|
||||
git: ({ task, updateField }) => (
|
||||
<GitSection task={task} onGitChange={(git) => updateField('git', git as Task['git'])} />
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ describe('task workspace navigation', () => {
|
|||
work: 'overview',
|
||||
details: 'plan',
|
||||
progress: 'plan',
|
||||
dependencies: 'plan',
|
||||
'work-products': 'results',
|
||||
observations: 'plan',
|
||||
attachments: 'plan',
|
||||
|
|
@ -49,9 +50,12 @@ describe('task workspace navigation', () => {
|
|||
isCodeTask: false,
|
||||
hasWorktree: false,
|
||||
attachmentsEnabled: false,
|
||||
dependenciesEnabled: false,
|
||||
});
|
||||
const modes = getAvailableTaskWorkspaceModeMetadata(tabs);
|
||||
|
||||
expect(tabs.some((tab) => tab.id === 'dependencies')).toBe(false);
|
||||
expect(tabs.some((tab) => tab.id === 'attachments')).toBe(false);
|
||||
expect(modes.find((mode) => mode.id === 'run')?.disabled).toBe(true);
|
||||
expect(modes.filter((mode) => !mode.disabled).map((mode) => mode.id)).toEqual([
|
||||
'overview',
|
||||
|
|
@ -66,9 +70,22 @@ describe('task workspace navigation', () => {
|
|||
isCodeTask: true,
|
||||
hasWorktree: false,
|
||||
attachmentsEnabled: true,
|
||||
dependenciesEnabled: true,
|
||||
});
|
||||
|
||||
expect(resolveTaskDetailNavigationTab({ tab: 'timeline' }, tabs)).toBe('timeline');
|
||||
for (const section of [
|
||||
'details',
|
||||
'progress',
|
||||
'observations',
|
||||
'dependencies',
|
||||
'attachments',
|
||||
] as const) {
|
||||
expect(resolveTaskDetailNavigationTab({ tab: section }, tabs)).toBe(section);
|
||||
expect(
|
||||
resolveTaskDetailNavigationTab({ workspace: { version: 1, mode: 'plan', section } }, tabs)
|
||||
).toBe(section);
|
||||
}
|
||||
expect(
|
||||
resolveTaskDetailNavigationTab(
|
||||
{ workspace: { version: 1, mode: 'results', section: 'evidence' } },
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ export type TaskDetailTabId =
|
|||
| 'work'
|
||||
| 'details'
|
||||
| 'progress'
|
||||
| 'dependencies'
|
||||
| 'work-products'
|
||||
| 'observations'
|
||||
| 'attachments'
|
||||
|
|
@ -32,6 +33,7 @@ export interface TaskDetailAvailabilityContext {
|
|||
isCodeTask: boolean;
|
||||
hasWorktree: boolean;
|
||||
attachmentsEnabled: boolean;
|
||||
dependenciesEnabled: boolean;
|
||||
}
|
||||
|
||||
export type TaskDetailTabIcon =
|
||||
|
|
@ -44,6 +46,7 @@ export type TaskDetailTabIcon =
|
|||
| 'Files'
|
||||
| 'GitBranch'
|
||||
| 'History'
|
||||
| 'Network'
|
||||
| 'NotebookPen'
|
||||
| 'Paperclip';
|
||||
|
||||
|
|
@ -84,8 +87,8 @@ export const TASK_WORKSPACE_MODE_METADATA: readonly TaskWorkspaceModeMetadata[]
|
|||
{
|
||||
id: 'plan',
|
||||
label: 'Plan',
|
||||
description: 'Task details, progress, observations, and supporting context.',
|
||||
sections: ['details', 'progress', 'observations', 'attachments'],
|
||||
description: 'Task details, progress, observations, dependencies, and supporting context.',
|
||||
sections: ['details', 'progress', 'observations', 'dependencies', 'attachments'],
|
||||
},
|
||||
{
|
||||
id: 'run',
|
||||
|
|
@ -143,6 +146,13 @@ export const TASK_DETAIL_TAB_METADATA: readonly TaskDetailTabMetadata[] = [
|
|||
icon: 'Eye',
|
||||
fallbackTitle: 'Observations section failed to load',
|
||||
},
|
||||
{
|
||||
id: 'dependencies',
|
||||
label: 'Dependencies',
|
||||
icon: 'Network',
|
||||
fallbackTitle: 'Dependencies section failed to load',
|
||||
isVisible: ({ dependenciesEnabled }) => dependenciesEnabled,
|
||||
},
|
||||
{
|
||||
id: 'attachments',
|
||||
label: 'Attachments',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue