diff --git a/web/src/__tests__/task-detail-mantine.test.tsx b/web/src/__tests__/task-detail-mantine.test.tsx
index aa329a13..ebf7502d 100644
--- a/web/src/__tests__/task-detail-mantine.test.tsx
+++ b/web/src/__tests__/task-detail-mantine.test.tsx
@@ -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: () =>
Attachments section
,
}));
+vi.mock('@/components/task/DependenciesSection', () => ({
+ DependenciesSection: () => Dependencies section
,
+}));
+
vi.mock('@/components/task/ObservationsSection', () => ({
ObservationsSection: () => Observations section
,
}));
@@ -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(
{
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(
+
+ );
+
+ await waitFor(() =>
+ expect(screen.getByRole('tab', { name: 'Dependencies' }).getAttribute('aria-selected')).toBe(
+ 'true'
+ )
+ );
+
+ rerender(
+
+ );
+
+ 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({
diff --git a/web/src/__tests__/task-detail-support-sections-mantine.test.tsx b/web/src/__tests__/task-detail-support-sections-mantine.test.tsx
index 2e8b72df..ada6bfe9 100644
--- a/web/src/__tests__/task-detail-support-sections-mantine.test.tsx
+++ b/web/src/__tests__/task-detail-support-sections-mantine.test.tsx
@@ -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();
+
+ 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);
diff --git a/web/src/components/task/AttachmentsSection.tsx b/web/src/components/task/AttachmentsSection.tsx
index 4b95eab9..8b442d6e 100644
--- a/web/src/components/task/AttachmentsSection.tsx
+++ b/web/src/components/task/AttachmentsSection.tsx
@@ -264,6 +264,14 @@ export function AttachmentsSection({ task }: AttachmentsSectionProps) {
)}
+ handleFileSelect(e.target.files)}
+ className="hidden"
+ />
+
{/* Upload zone */}
- handleFileSelect(e.target.files)}
- className="hidden"
- />
-
-
-
-
- {uploadAttachment.isPending ? 'Uploading...' : 'Drop files here or click to browse'}
-
-
- Max 10MB per file, 20 files total
-
+ {attachments.length === 0 ? (
+
+
+
+
+
+
+ {uploadAttachment.isPending ? 'Uploading...' : 'Add supporting files'}
+
+
+ Drop files here or browse. Max 10MB per file.
+
+
+
+ ) : (
+ <>
+
+
+
+
+ {uploadAttachment.isPending ? 'Uploading...' : 'Drop files here or click to browse'}
+
+
+ Max 10MB per file, 20 files total
+
+ >
+ )}
{/* Attachments list */}
- {attachments.length === 0 ? (
-
-
- No attachments yet
-
-
- ) : (
+ {attachments.length > 0 && (
{attachments.map((attachment) => (
diff --git a/web/src/components/task/ObservationsSection.tsx b/web/src/components/task/ObservationsSection.tsx
index 905b45a5..1a799cda 100644
--- a/web/src/components/task/ObservationsSection.tsx
+++ b/web/src/components/task/ObservationsSection.tsx
@@ -276,7 +276,7 @@ export function ObservationsSection({
{observations.length === 0 && !isAdding && (
-
+
No observations yet. Add context, decisions, insights, or blockers as you work on this
task.
diff --git a/web/src/components/task/TaskDetailPanel.tsx b/web/src/components/task/TaskDetailPanel.tsx
index 0b5aa738..36240c86 100644
--- a/web/src/components/task/TaskDetailPanel.tsx
+++ b/web/src/components/task/TaskDetailPanel.tsx
@@ -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({
-
+
{activeModeMetadata?.label ?? 'Workspace'}
@@ -490,9 +492,12 @@ export function TaskDetailPanel({
+
+ {activeTabMetadata?.label ?? 'Task details'}
+
{visibleTabs.map((tab) => {
const tabContent = (
}
>
- Edit
+ {isEmpty ? 'Add notes' : 'Edit'}
)}
@@ -118,17 +118,22 @@ export function ProgressTab({ task }: ProgressTabProps) {
{/* View Mode */}
{!isEditing && (
-
+
{isEmpty ? (
-
-
-
+
+
+
- No progress notes yet
-
- Click Edit to add learnings, issues, and next steps for future sessions
-
-
+
+
+ No progress notes yet
+
+ Add learnings, issues, and next steps when they are useful.
+
+
) : (
{progress}
)}
@@ -136,17 +141,19 @@ export function ProgressTab({ task }: ProgressTabProps) {
)}
{/* Help Text */}
-
-
- Progress Notes Best Practices:
-
-
- - Document key learnings and insights discovered during work
- - Track issues encountered and their solutions
- - List next steps for future sessions to pick up where you left off
- - Use markdown sections (##) to organize by category
-
-
+ {!isEmpty && !isEditing && (
+
+
+ Progress Notes Best Practices:
+
+
+ - Document key learnings and insights discovered during work
+ - Track issues encountered and their solutions
+ - List next steps for future sessions to pick up where you left off
+ - Use markdown sections (##) to organize by category
+
+
+ )}
);
}
diff --git a/web/src/components/task/detail/TaskDetailsTab.tsx b/web/src/components/task/detail/TaskDetailsTab.tsx
index e10cfe3d..77f68796 100644
--- a/web/src/components/task/detail/TaskDetailsTab.tsx
+++ b/web/src/components/task/detail/TaskDetailsTab.tsx
@@ -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({
- {/* Dependencies */}
- {taskSettings.enableDependencies && (
-
- onUpdate('blockedBy', blockedBy)}
- />
-
- )}
-
{/* Time Tracking */}
{taskSettings.enableTimeTracking && (
diff --git a/web/src/components/task/task-detail-tabs.tsx b/web/src/components/task/task-detail-tabs.tsx
index ea3bf7af..d741a099 100644
--- a/web/src/components/task/task-detail-tabs.tsx
+++ b/web/src/components/task/task-detail-tabs.tsx
@@ -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 = {
Files,
GitBranch,
History,
+ Network,
NotebookPen,
Paperclip,
};
@@ -157,6 +162,12 @@ const TAB_RENDERERS: Record
),
+ dependencies: ({ task, updateField }) => (
+ updateField('blockedBy', blockedBy)}
+ />
+ ),
attachments: ({ task }) => ,
git: ({ task, updateField }) => (
updateField('git', git as Task['git'])} />
diff --git a/web/src/lib/__tests__/task-detail-workspace.test.ts b/web/src/lib/__tests__/task-detail-workspace.test.ts
index fb80134f..832fda6f 100644
--- a/web/src/lib/__tests__/task-detail-workspace.test.ts
+++ b/web/src/lib/__tests__/task-detail-workspace.test.ts
@@ -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' } },
diff --git a/web/src/lib/task-detail-tabs.ts b/web/src/lib/task-detail-tabs.ts
index 206609f2..1ee53064 100644
--- a/web/src/lib/task-detail-tabs.ts
+++ b/web/src/lib/task-detail-tabs.ts
@@ -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',