Skip to content

Commit 9f3ab70

Browse files
committed
Add task detail view with navigation and log streaming
Adds the task detail view layer including: Components: - TaskDetailView as the main detail view container - TaskDetailHeader with back button, status, and action menu - AgentChatHistory for displaying task log entries with scroll tracking - TaskInput with pause button and state-aware placeholder - ErrorBanner for displaying task errors with link to logs App.tsx enhancements: - Navigation between task list and detail view (inline in Task History) - Selected task state persistence and validation - Adaptive polling intervals based on task state (active vs idle) - Real-time log streaming via logsAppend push messages - refs to avoid stale closures in message handlers - Transition animation when switching views Config additions: - TASK_ACTIVE_INTERVAL_MS for faster updates when task is working - TASK_IDLE_INTERVAL_MS for slower updates when task is idle/complete Also adds codicons CSS import for icon rendering.
1 parent 74dbcc4 commit 9f3ab70

File tree

9 files changed

+534
-16
lines changed

9 files changed

+534
-16
lines changed

packages/tasks/src/App.tsx

Lines changed: 189 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import {
22
getState,
33
setState,
4+
getTaskUIState,
45
type Task,
6+
type TaskDetails,
57
type TasksPushMessage,
68
type TaskTemplate,
79
} from "@repo/webview-shared";
@@ -15,23 +17,56 @@ import {
1517
ErrorState,
1618
NoTemplateState,
1719
NotSupportedState,
20+
TaskDetailView,
1821
TaskList,
1922
} from "./components";
2023
import { POLLING_CONFIG } from "./config";
21-
import { taskArraysEqual, templateArraysEqual } from "./utils";
24+
import {
25+
taskArraysEqual,
26+
taskDetailsEqual,
27+
taskEqual,
28+
templateArraysEqual,
29+
} from "./utils";
2230

2331
interface PersistedState {
2432
tasks: Task[];
2533
templates: TaskTemplate[];
34+
selectedTaskId: string | null;
35+
selectedTask: TaskDetails | null;
2636
createExpanded: boolean;
2737
historyExpanded: boolean;
2838
tasksSupported: boolean;
2939
}
3040

41+
function validatePersistedState(
42+
state: PersistedState | undefined,
43+
): PersistedState | undefined {
44+
if (!state) return undefined;
45+
46+
if (state.selectedTask && state.tasks.length > 0) {
47+
const taskExists = state.tasks.some(
48+
(t) => t.id === state.selectedTask?.task.id,
49+
);
50+
if (!taskExists) {
51+
return { ...state, selectedTaskId: null, selectedTask: null };
52+
}
53+
}
54+
55+
return state;
56+
}
57+
58+
function isTaskActive(task: Task | null | undefined): boolean {
59+
if (!task) return false;
60+
const state = getTaskUIState(task);
61+
return state === "working" || state === "initializing";
62+
}
63+
3164
export default function App() {
3265
const api = useTasksApi();
3366

34-
const persistedState = useRef(getState<PersistedState>());
67+
const persistedState = useRef(
68+
validatePersistedState(getState<PersistedState>()),
69+
);
3570
const restored = persistedState.current;
3671

3772
const [initialized, setInitialized] = useState(!!restored?.tasks?.length);
@@ -43,22 +78,35 @@ export default function App() {
4378
restored?.tasksSupported ?? true,
4479
);
4580

81+
const [selectedTask, setSelectedTask] = useState<TaskDetails | null>(
82+
restored?.selectedTask ?? null,
83+
);
4684
const [createExpanded, setCreateExpanded] = useState(
4785
restored?.createExpanded ?? true,
4886
);
4987
const [historyExpanded, setHistoryExpanded] = useState(
5088
restored?.historyExpanded ?? true,
5189
);
90+
const [isTransitioning, setIsTransitioning] = useState(false);
5291

5392
useEffect(() => {
5493
setState<PersistedState>({
5594
tasks,
5695
templates,
96+
selectedTaskId: selectedTask?.task.id ?? null,
97+
selectedTask,
5798
createExpanded,
5899
historyExpanded,
59100
tasksSupported,
60101
});
61-
}, [tasks, templates, createExpanded, historyExpanded, tasksSupported]);
102+
}, [
103+
tasks,
104+
templates,
105+
selectedTask,
106+
createExpanded,
107+
historyExpanded,
108+
tasksSupported,
109+
]);
62110

63111
const [initLoading, setInitLoading] = useState(!restored?.tasks?.length);
64112
const [initError, setInitError] = useState<string | null>(null);
@@ -76,6 +124,16 @@ export default function App() {
76124
setTasksSupported(data.tasksSupported);
77125
setInitialized(true);
78126
setInitError(null);
127+
128+
if (selectedTaskRef.current) {
129+
const taskExists = data.tasks.some(
130+
(t) => t.id === selectedTaskRef.current?.task.id,
131+
);
132+
if (!taskExists) {
133+
expectedTaskIdRef.current = null;
134+
setSelectedTask(null);
135+
}
136+
}
79137
} catch (err) {
80138
if (cancelled) return;
81139
setInitError(
@@ -97,12 +155,19 @@ export default function App() {
97155
const tasksRef = useRef<Task[]>(tasks);
98156
tasksRef.current = tasks;
99157

158+
const selectedTaskRef = useRef<TaskDetails | null>(selectedTask);
159+
selectedTaskRef.current = selectedTask;
160+
100161
const templatesRef = useRef<TaskTemplate[]>(templates);
101162
templatesRef.current = templates;
102163

103-
// Poll for task list updates
164+
const expectedTaskIdRef = useRef<string | null>(
165+
restored?.selectedTaskId ?? null,
166+
);
167+
168+
// Poll for task list updates when not viewing a specific task
104169
useEffect(() => {
105-
if (!initialized) return;
170+
if (!initialized || selectedTask) return;
106171

107172
let cancelled = false;
108173
const pollInterval = setInterval(() => {
@@ -121,7 +186,38 @@ export default function App() {
121186
cancelled = true;
122187
clearInterval(pollInterval);
123188
};
124-
}, [api, initialized]);
189+
}, [api, initialized, selectedTask]);
190+
191+
const selectedTaskId = selectedTask?.task.id ?? null;
192+
const isActive = isTaskActive(selectedTask?.task);
193+
194+
// Poll for selected task with adaptive interval based on task state
195+
useEffect(() => {
196+
if (!initialized || !selectedTaskId) return;
197+
198+
let cancelled = false;
199+
const interval = isActive
200+
? POLLING_CONFIG.TASK_ACTIVE_INTERVAL_MS
201+
: POLLING_CONFIG.TASK_IDLE_INTERVAL_MS;
202+
203+
const poll = () => {
204+
api
205+
.getTaskDetails(selectedTaskId)
206+
.then((details) => {
207+
if (cancelled || expectedTaskIdRef.current !== selectedTaskId) return;
208+
if (!taskDetailsEqual(selectedTaskRef.current, details)) {
209+
setSelectedTask(details);
210+
}
211+
})
212+
.catch(() => undefined);
213+
};
214+
215+
const pollInterval = setInterval(poll, interval);
216+
return () => {
217+
cancelled = true;
218+
clearInterval(pollInterval);
219+
};
220+
}, [api, initialized, selectedTaskId, isActive]);
125221

126222
const handleRetry = useCallback(() => {
127223
setInitLoading(true);
@@ -147,18 +243,44 @@ export default function App() {
147243

148244
useMessage<TasksPushMessage>((msg) => {
149245
switch (msg.type) {
150-
case "tasksUpdated":
246+
case "tasksUpdated": {
151247
setTasks(msg.data);
248+
const currentSelectedId = selectedTaskRef.current?.task.id;
249+
if (currentSelectedId) {
250+
const updatedTask = msg.data.find((t) => t.id === currentSelectedId);
251+
if (
252+
updatedTask &&
253+
!taskEqual(selectedTaskRef.current?.task, updatedTask)
254+
) {
255+
setSelectedTask((prev) =>
256+
prev ? { ...prev, task: updatedTask } : null,
257+
);
258+
}
259+
}
152260
break;
261+
}
153262

154263
case "taskUpdated": {
155264
const updatedTask = msg.data;
156265
setTasks((prev) =>
157266
prev.map((t) => (t.id === updatedTask.id ? updatedTask : t)),
158267
);
268+
if (selectedTaskRef.current?.task.id === updatedTask.id) {
269+
setSelectedTask((prev) =>
270+
prev ? { ...prev, task: updatedTask } : null,
271+
);
272+
}
159273
break;
160274
}
161275

276+
case "logsAppend":
277+
if (selectedTaskRef.current) {
278+
setSelectedTask((prev) =>
279+
prev ? { ...prev, logs: [...prev.logs, ...msg.data] } : null,
280+
);
281+
}
282+
break;
283+
162284
case "refresh": {
163285
api
164286
.getTasks()
@@ -176,22 +298,63 @@ export default function App() {
176298
}
177299
})
178300
.catch(() => undefined);
301+
const taskIdAtRequest = selectedTaskRef.current?.task.id;
302+
if (taskIdAtRequest) {
303+
api
304+
.getTaskDetails(taskIdAtRequest)
305+
.then((details) => {
306+
if (expectedTaskIdRef.current !== taskIdAtRequest) return;
307+
if (!taskDetailsEqual(selectedTaskRef.current, details)) {
308+
setSelectedTask(details);
309+
}
310+
})
311+
.catch(() => undefined);
312+
}
179313
break;
180314
}
181315

182316
case "showCreateForm":
317+
setSelectedTask(null);
183318
setCreateExpanded(true);
184319
break;
185-
186-
case "logsAppend":
187-
// Task detail view will handle this in next PR
188-
break;
189320
}
190321
});
191322

192-
const handleSelectTask = useCallback((_taskId: string) => {
193-
// Task detail view will be added in next PR
194-
}, []);
323+
const handleSelectTask = useCallback(
324+
(taskId: string) => {
325+
expectedTaskIdRef.current = taskId;
326+
setIsTransitioning(true);
327+
328+
api
329+
.getTaskDetails(taskId)
330+
.then((details) => {
331+
if (expectedTaskIdRef.current === taskId) {
332+
setSelectedTask(details);
333+
setIsTransitioning(false);
334+
}
335+
})
336+
.catch(() => {
337+
if (expectedTaskIdRef.current === taskId) {
338+
setIsTransitioning(false);
339+
}
340+
});
341+
},
342+
[api],
343+
);
344+
345+
const handleDeselectTask = useCallback(() => {
346+
expectedTaskIdRef.current = null;
347+
setSelectedTask(null);
348+
349+
api
350+
.getTasks()
351+
.then((updatedTasks) => {
352+
if (!taskArraysEqual(tasksRef.current, updatedTasks)) {
353+
setTasks(updatedTasks);
354+
}
355+
})
356+
.catch(() => undefined);
357+
}, [api]);
195358

196359
if (initLoading) {
197360
return (
@@ -228,7 +391,18 @@ export default function App() {
228391
expanded={historyExpanded}
229392
onToggle={() => setHistoryExpanded(!historyExpanded)}
230393
>
231-
<TaskList tasks={tasks} onSelectTask={handleSelectTask} />
394+
<div
395+
className={`task-history-content ${isTransitioning ? "transitioning" : ""}`}
396+
>
397+
{selectedTask ? (
398+
<TaskDetailView
399+
details={selectedTask}
400+
onBack={handleDeselectTask}
401+
/>
402+
) : (
403+
<TaskList tasks={tasks} onSelectTask={handleSelectTask} />
404+
)}
405+
</div>
232406
</CollapsibleSection>
233407
</div>
234408
);

0 commit comments

Comments
 (0)