taskchef 8.7.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/.codex-plugin/plugin.json +7 -10
  2. package/.mcp.json +3 -2
  3. package/README.md +31 -158
  4. package/docs/mcp-app.md +21 -4
  5. package/docs/spec.md +38 -746
  6. package/docs/taskchef-next-storage.md +2 -2
  7. package/index.js +2 -99
  8. package/package.json +12 -49
  9. package/skills/delegate/SKILL.md +44 -0
  10. package/skills/delegate/agents/openai.yaml +4 -0
  11. package/src/codex-app.js +3 -152
  12. package/src/codex-session-scanner.js +2 -2
  13. package/src/file-lock.js +25 -0
  14. package/src/mcp-app/react/NextTaskDetail.test.tsx +1 -1
  15. package/src/mcp-app/react/NextTaskDetail.tsx +8 -8
  16. package/src/mcp-app/react/main.test.tsx +3 -3
  17. package/src/mcp-app/react/main.tsx +7 -7
  18. package/src/mcp-app.js +4 -4
  19. package/src/mcp.js +4 -542
  20. package/src/next-cache.js +2 -2
  21. package/src/next-github.js +3 -3
  22. package/src/next-notifications.js +2 -2
  23. package/src/{dashboard/react → ui}/components/CardStatusIcons.tsx +1 -1
  24. package/src/{dashboard/react → ui}/components/GitHubLinks.tsx +1 -1
  25. package/src/{dashboard/react → ui}/components/LinkedText.tsx +1 -1
  26. package/src/{dashboard/react → ui}/components/RelativeTime.test.tsx +1 -1
  27. package/src/{dashboard/react → ui}/components/RelativeTime.tsx +1 -1
  28. package/src/{dashboard/react → ui}/components/ReplyImagePreview.test.tsx +1 -1
  29. package/src/{dashboard/react → ui}/components/TaskBoard.tsx +1 -1
  30. package/src/{dashboard/react → ui}/components/TaskCard.tsx +1 -1
  31. package/src/{dashboard/react → ui}/components/TurnContent.tsx +1 -1
  32. package/src/{dashboard/react → ui}/presentation.ts +1 -1
  33. package/src/version.js +2 -1
  34. package/BACKLOG.md +0 -48
  35. package/assets/e2e-benchmark-example.json +0 -50
  36. package/assets/taskchef-dispatcher-instructions.md +0 -44
  37. package/bin/taskchef.js +0 -20
  38. package/docs/advanced-guide.md +0 -497
  39. package/docs/dashboard-lifecycle.md +0 -149
  40. package/docs/dashboard-ui.md +0 -40
  41. package/docs/firstmate-taskchef-comparison.md +0 -283
  42. package/docs/images/ccusage-token-consumption.png +0 -0
  43. package/docs/images/dashboard-identity.jpg +0 -0
  44. package/docs/images/dashboard-token-pending.jpg +0 -0
  45. package/docs/images/interrupted-turn-recovery.jpg +0 -0
  46. package/docs/images/notification-event-snapshots.jpg +0 -0
  47. package/docs/images/readme-board.png +0 -0
  48. package/docs/images/readme-completed.png +0 -0
  49. package/docs/images/readme-list.png +0 -0
  50. package/docs/images/readme-needs-input.png +0 -0
  51. package/docs/images/readme-roles.png +0 -0
  52. package/docs/images/reported-work-duration-desktop.jpg +0 -0
  53. package/docs/images/reported-work-duration-mobile.jpg +0 -0
  54. package/docs/images/result-history-dashboard.jpg +0 -0
  55. package/docs/images/taskchef-pr-icon-comparison.jpg +0 -0
  56. package/docs/images/taskchef-unified-status-popup.jpg +0 -0
  57. package/docs/workflows.md +0 -476
  58. package/mcp/dashboard-session.js +0 -51
  59. package/scripts/benchmark-dispatch-prepare.js +0 -90
  60. package/scripts/e2e-benchmark.js +0 -206
  61. package/scripts/roles/resolve_roles.py +0 -293
  62. package/skills/taskchef-bootstrap/SKILL.md +0 -96
  63. package/skills/taskchef-bootstrap/agents/openai.yaml +0 -4
  64. package/skills/taskchef-bootstrap/references/project-index.md +0 -154
  65. package/skills/taskchef-copilot/SKILL.md +0 -75
  66. package/skills/taskchef-copilot/agents/openai.yaml +0 -4
  67. package/skills/taskchef-dashboard/SKILL.md +0 -24
  68. package/skills/taskchef-dashboard/agents/openai.yaml +0 -4
  69. package/skills/taskchef-delegate/SKILL.md +0 -102
  70. package/skills/taskchef-delegate/agents/openai.yaml +0 -4
  71. package/skills/taskchef-delegate/references/model-roles.md +0 -29
  72. package/skills/taskchef-executor/SKILL.md +0 -154
  73. package/skills/taskchef-executor/agents/openai.yaml +0 -4
  74. package/skills/taskchef-executor/references/compatibility.md +0 -22
  75. package/skills/taskchef-executor/references/ending-actions.md +0 -26
  76. package/src/backups.js +0 -359
  77. package/src/cli.js +0 -733
  78. package/src/config-audit.js +0 -99
  79. package/src/config-mutations.js +0 -106
  80. package/src/dashboard/actions.js +0 -136
  81. package/src/dashboard/app.js +0 -1172
  82. package/src/dashboard/dist/app.js +0 -269
  83. package/src/dashboard/dist/app.js.map +0 -1
  84. package/src/dashboard/dist/index.html +0 -16
  85. package/src/dashboard/dist/styles.css +0 -1
  86. package/src/dashboard/index.html +0 -148
  87. package/src/dashboard/react/App.test.tsx +0 -346
  88. package/src/dashboard/react/App.tsx +0 -515
  89. package/src/dashboard/react/api.test.ts +0 -40
  90. package/src/dashboard/react/api.ts +0 -103
  91. package/src/dashboard/react/components/ActivityTimeline.tsx +0 -118
  92. package/src/dashboard/react/components/ModelSettings.test.tsx +0 -180
  93. package/src/dashboard/react/components/ModelSettings.tsx +0 -106
  94. package/src/dashboard/react/components/NotificationCenter.test.tsx +0 -76
  95. package/src/dashboard/react/components/NotificationCenter.tsx +0 -111
  96. package/src/dashboard/react/components/PullRequestBadges.tsx +0 -18
  97. package/src/dashboard/react/components/TaskDetail.test.tsx +0 -255
  98. package/src/dashboard/react/components/TaskDetail.tsx +0 -271
  99. package/src/dashboard/react/components/UsagePanel.tsx +0 -96
  100. package/src/dashboard/react/index.html +0 -15
  101. package/src/dashboard/react/main.tsx +0 -12
  102. package/src/dashboard/react/overlay-state.test.ts +0 -110
  103. package/src/dashboard/react/overlay-state.ts +0 -60
  104. package/src/dashboard/react/presentation.test.tsx +0 -571
  105. package/src/dashboard/styles.css +0 -261
  106. package/src/dashboard-control.js +0 -34
  107. package/src/dashboard-manager.js +0 -339
  108. package/src/dashboard-session-process.js +0 -88
  109. package/src/dashboard-session.js +0 -86
  110. package/src/dashboard.js +0 -1303
  111. package/src/delegation.js +0 -277
  112. package/src/github.js +0 -107
  113. package/src/model-roles.js +0 -70
  114. package/src/recorded-usage-tracker.js +0 -92
  115. package/src/usage-tracker.js +0 -650
  116. package/src/usage.js +0 -367
  117. package/src/workspace-path.js +0 -48
  118. package/src/workspace.js +0 -3629
  119. /package/src/{dashboard/react → ui}/components/AnimatedUsage.test.tsx +0 -0
  120. /package/src/{dashboard/react → ui}/components/CardStatusIcons.test.tsx +0 -0
  121. /package/src/{dashboard/react → ui}/components/ImageGallery.tsx +0 -0
  122. /package/src/{dashboard/react → ui}/components/OpenChatButton.test.tsx +0 -0
  123. /package/src/{dashboard/react → ui}/components/OpenChatButton.tsx +0 -0
  124. /package/src/{dashboard/react → ui}/components/PrGlyph.tsx +0 -0
  125. /package/src/{dashboard/react → ui}/components/QueuedPrompts.test.tsx +0 -0
  126. /package/src/{dashboard/react → ui}/components/QueuedPrompts.tsx +0 -0
  127. /package/src/{dashboard/react → ui}/components/ReplyCover.test.tsx +0 -0
  128. /package/src/{dashboard/react → ui}/components/ReplyCover.tsx +0 -0
  129. /package/src/{dashboard/react → ui}/components/ReplyImagePreview.tsx +0 -0
  130. /package/src/{dashboard/react → ui}/components/ReplyMarkdown.test.tsx +0 -0
  131. /package/src/{dashboard/react → ui}/components/ReplyMarkdown.tsx +0 -0
  132. /package/src/{dashboard/react → ui}/components/SavedUsageFormat.test.tsx +0 -0
  133. /package/src/{dashboard/react → ui}/components/SavedUsageFormat.tsx +0 -0
  134. /package/src/{dashboard/react → ui}/components/ShimmerText.tsx +0 -0
  135. /package/src/{dashboard/react → ui}/components/StatsGrid.tsx +0 -0
  136. /package/src/{dashboard/react → ui}/components/StatusBadge.tsx +0 -0
  137. /package/src/{dashboard/react → ui}/components/TaskBoard.test.tsx +0 -0
  138. /package/src/{dashboard/react → ui}/components/TaskCardStats.test.tsx +0 -0
  139. /package/src/{dashboard/react → ui}/components/TaskCardStats.tsx +0 -0
  140. /package/src/{dashboard/react → ui}/components/TurnContent.test.tsx +0 -0
  141. /package/src/{dashboard/react → ui}/fixtures.ts +0 -0
  142. /package/src/{dashboard → ui}/github-links.js +0 -0
  143. /package/src/{dashboard/react → ui}/image-cache.test.ts +0 -0
  144. /package/src/{dashboard/react → ui}/image-cache.ts +0 -0
  145. /package/src/{dashboard → ui}/state.js +0 -0
  146. /package/src/{dashboard/react → ui}/styles.css +0 -0
  147. /package/src/{dashboard/react → ui}/test-setup.ts +0 -0
  148. /package/src/{dashboard → ui}/time.js +0 -0
  149. /package/src/{dashboard/react → ui}/types.ts +0 -0
@@ -1,515 +0,0 @@
1
- import { ModelSettings } from "./components/ModelSettings";
2
- import {
3
- Alert,
4
- ActionIcon,
5
- AppShell,
6
- Box,
7
- Button,
8
- CloseButton,
9
- Container,
10
- Group,
11
- MantineProvider,
12
- Paper,
13
- SegmentedControl,
14
- Select,
15
- Stack,
16
- Text,
17
- Title,
18
- Tooltip,
19
- createTheme,
20
- useComputedColorScheme,
21
- useMantineColorScheme,
22
- } from "@mantine/core";
23
- import { IconArrowLeft, IconSettings, IconAlertTriangle, IconCircleFilled, IconMoon, IconSun } from "@tabler/icons-react";
24
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
25
- import {
26
- dismissNotification,
27
- filterTasks,
28
- manualTransitionExpectedState,
29
- reconcileManualTransitionResponse,
30
- reconcileNotifications,
31
- statusFilterCounts,
32
- STATUS_FILTERS,
33
- } from "../state.js";
34
- import { connectDashboard, dashboardVersion, manualTransition, openInCodex, taskDetail } from "./api";
35
- import {
36
- clearNotificationHistory,
37
- listUsageSignature,
38
- mergeListTaskIntoDetail,
39
- notificationClicked,
40
- } from "./overlay-state";
41
- import { usageStillCalculating } from "./presentation";
42
- import type { DashboardSnapshot, NotificationSnapshot, Task } from "./types";
43
- import { NotificationCenter } from "./components/NotificationCenter";
44
- import { TaskCard } from "./components/TaskCard";
45
- import { TaskBoard } from "./components/TaskBoard";
46
- import { TaskDetail } from "./components/TaskDetail";
47
- import { RelativeTimeProvider } from "./components/RelativeTime";
48
-
49
- const theme = createTheme({
50
- primaryColor: "teal",
51
- colors: {
52
- teal: [
53
- "#e6fcf5",
54
- "#c3fae8",
55
- "#96f2d7",
56
- "#63e6be",
57
- "#38d9a9",
58
- "#20c997",
59
- "#087f5b",
60
- "#067052",
61
- "#055e46",
62
- "#044d39",
63
- ],
64
- },
65
- defaultRadius: "md",
66
- fontFamily: "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif",
67
- headings: { fontFamily: "inherit", fontWeight: "680" },
68
- components: {
69
- Button: { defaultProps: { radius: "md" } },
70
- Paper: { defaultProps: { radius: "md" } },
71
- Select: { defaultProps: { size: "xs" } },
72
- },
73
- });
74
- const styleNonce = document.querySelector<HTMLMetaElement>('meta[name="taskchef-style-nonce"]')?.content;
75
- const VIEW_STORAGE_KEY = "taskchef.dashboard.view";
76
-
77
- function savedView(): "board" | "list" {
78
- try { return window.localStorage.getItem(VIEW_STORAGE_KEY) === "board" ? "board" : "list"; }
79
- catch { return "list"; }
80
- }
81
-
82
- interface NotificationState {
83
- initialized: boolean;
84
- announcements: NotificationSnapshot[];
85
- notifications: NotificationSnapshot[];
86
- seenIds: Set<string>;
87
- signatures: Map<string, string>;
88
- }
89
-
90
- export function TaskResultsSummary({
91
- totalCount,
92
- visibleCount,
93
- }: {
94
- totalCount: number;
95
- visibleCount: number;
96
- }) {
97
- return (
98
- <Text aria-live="polite" className="taskchef-results-summary taskchef-content" id="task-results-summary">
99
- Tasks: {visibleCount} of {totalCount}
100
- </Text>
101
- );
102
- }
103
-
104
- export function DashboardApp({
105
- connect = true,
106
- initialFilters = {},
107
- initialTasks = [],
108
- }: {
109
- connect?: boolean;
110
- initialFilters?: { project?: string; status?: string };
111
- initialTasks?: Task[];
112
- }) {
113
- const [settings, setSettings] = useState(() => window.location.hash === '#settings');
114
- useEffect(() => {
115
- const update = () => setSettings(window.location.hash === '#settings');
116
- window.addEventListener('hashchange', update);
117
- return () => window.removeEventListener('hashchange', update);
118
- }, []);
119
- const [tasks, setTasks] = useState<Task[]>(initialTasks);
120
- const [projectIndex, setProjectIndex] = useState<DashboardSnapshot["projectIndex"]>();
121
- const [connected, setConnected] = useState(false);
122
- const [message, setMessage] = useState<string | null>(null);
123
- const [version, setVersion] = useState<string | null>(null);
124
- const [project, setProject] = useState(initialFilters.project ?? "");
125
- const [status, setStatus] = useState(initialFilters.status ?? "");
126
- const [date, setDate] = useState("all");
127
- const [preferredView, setPreferredView] = useState(savedView);
128
- const [completedLimit, setCompletedLimit] = useState(5);
129
- const [now, setNow] = useState(() => Date.now());
130
- const [selectedTask, setSelectedTask] = useState<Task | null>(null);
131
- const [detailOpened, setDetailOpened] = useState(false);
132
- const [detailBusy, setDetailBusy] = useState(false);
133
- const [detailError, setDetailError] = useState<string | null>(null);
134
- const [detailNotice, setDetailNotice] = useState<string | null>(null);
135
- const [highlightTurnRef, setHighlightTurnRef] = useState<string | null>(null);
136
- const [notificationState, setNotificationState] = useState<NotificationState>({
137
- initialized: initialTasks.length > 0,
138
- announcements: [],
139
- notifications: [],
140
- seenIds: new Set(),
141
- signatures: new Map(),
142
- });
143
- const selectedId = selectedTask?.id ?? null;
144
- const selectedIdRef = useRef<string | null>(selectedId);
145
- selectedIdRef.current = selectedId;
146
- const detailGeneration = useRef(0);
147
- const detailListUpdatedAt = useRef<string | null>(null);
148
- const detailListUsageSignature = useRef(listUsageSignature(null));
149
-
150
- const applySnapshot = useCallback((snapshot: DashboardSnapshot) => {
151
- setTasks(snapshot.tasks);
152
- setProjectIndex(snapshot.projectIndex);
153
- setNotificationState((current) => {
154
- const reconciled = reconcileNotifications(current, snapshot.tasks);
155
- return {
156
- initialized: true,
157
- announcements: reconciled.additions,
158
- notifications: reconciled.notifications,
159
- seenIds: reconciled.seenIds,
160
- signatures: reconciled.signatures,
161
- };
162
- });
163
- setMessage(snapshot.healthy === false
164
- ? "The task log is temporarily unavailable. Showing the last valid snapshot."
165
- : null);
166
- const currentSelectedId = selectedIdRef.current;
167
- if (currentSelectedId) {
168
- const updated = snapshot.tasks.find((task) => task.id === currentSelectedId);
169
- if (updated) {
170
- setSelectedTask((current) => current ? mergeListTaskIntoDetail(current, updated) : updated);
171
- }
172
- }
173
- }, []);
174
-
175
- useEffect(() => {
176
- const timer = window.setInterval(() => setNow(Date.now()), 30_000);
177
- return () => window.clearInterval(timer);
178
- }, []);
179
-
180
- useEffect(() => {
181
- if (!connect) return;
182
- void dashboardVersion().then(setVersion).catch(() => setVersion(null));
183
- const stream = connectDashboard({
184
- onConnection: setConnected,
185
- onError: setMessage,
186
- onSnapshot: applySnapshot,
187
- });
188
- return () => stream.close();
189
- }, [applySnapshot, connect]);
190
-
191
- const loadDetail = useCallback(async (task: Task, focusTurnRef: string | null = null) => {
192
- const generation = ++detailGeneration.current;
193
- detailListUpdatedAt.current = task.updatedAt;
194
- detailListUsageSignature.current = listUsageSignature(task);
195
- setSelectedTask((current) => current?.id === task.id
196
- ? mergeListTaskIntoDetail(current, task)
197
- : task);
198
- setDetailOpened(true);
199
- setHighlightTurnRef(focusTurnRef);
200
- setDetailError(null);
201
- setDetailNotice(null);
202
- try {
203
- for (let attempt = 0; attempt <= 40; attempt += 1) {
204
- if (generation !== detailGeneration.current) return;
205
- const detail = await taskDetail(task.id);
206
- if (generation !== detailGeneration.current) return;
207
- detailListUpdatedAt.current = detail.updatedAt;
208
- detailListUsageSignature.current = listUsageSignature(detail);
209
- setSelectedTask(detail);
210
- if (!usageStillCalculating(detail) || attempt === 40) return;
211
- await new Promise((resolve) => window.setTimeout(resolve, 1_500));
212
- }
213
- } catch {
214
- if (generation === detailGeneration.current) {
215
- setDetailError("Task activity is temporarily unavailable. Showing the latest list snapshot.");
216
- }
217
- }
218
- }, []);
219
-
220
- const selectedListTask = tasks.find((task) => task.id === selectedId) ?? null;
221
- const selectedListUsageSignature = listUsageSignature(selectedListTask);
222
-
223
- useEffect(() => {
224
- if (!detailOpened || !selectedId || !connect) return;
225
- if (!selectedListTask) return;
226
- if (
227
- selectedListTask.updatedAt === detailListUpdatedAt.current
228
- && selectedListUsageSignature === detailListUsageSignature.current
229
- ) return;
230
- void loadDetail(selectedListTask, highlightTurnRef);
231
- // Task or cached usage changes while detail is open refresh the full projection.
232
- // eslint-disable-next-line react-hooks/exhaustive-deps
233
- }, [selectedListTask?.updatedAt, selectedListUsageSignature]);
234
-
235
- const projects = useMemo(() => [
236
- { label: "All projects", value: "" },
237
- ...[...new Set(tasks.map((task) => task.project.name))].sort().map((value) => ({ label: value, value })),
238
- ], [tasks]);
239
- const visible = useMemo(() => filterTasks(tasks, { project, status, date, now }), [tasks, project, status, date, now]);
240
- const boardTasks = useMemo(() => filterTasks(tasks, { project, date, now }), [tasks, project, date, now]);
241
- const board = preferredView === "board";
242
- const listLayout = !board && !settings;
243
- const counts = useMemo(() => statusFilterCounts(tasks, { project, date, now }), [tasks, project, date, now]);
244
- const statusData = STATUS_FILTERS.map(({ label, value }: { label: string; value: string }) => ({
245
- label: counts[value] > 0 ? `${label} ${counts[value]}` : label,
246
- value,
247
- }));
248
-
249
- function changeView(value: string) {
250
- if (value !== "board" && value !== "list") return;
251
- setPreferredView(value);
252
- try { window.localStorage.setItem(VIEW_STORAGE_KEY, value); } catch { /* Preference remains in memory. */ }
253
- }
254
-
255
- async function handleOpenCodex(task: Task) {
256
- setDetailBusy(true);
257
- try {
258
- const result = await openInCodex(task.id);
259
- if (result) setMessage(result);
260
- } catch {
261
- setMessage("Codex could not be opened. Open the project and select the recorded task instead.");
262
- } finally {
263
- setDetailBusy(false);
264
- }
265
- }
266
-
267
- async function handleTransition(targetStatus: "completed" | "failed", actionId: string) {
268
- if (!selectedTask) return { ok: false };
269
- const requestTask = selectedTask;
270
- const expected = manualTransitionExpectedState(requestTask);
271
- setDetailBusy(true);
272
- setDetailError(null);
273
- const result = await manualTransition(requestTask, targetStatus, actionId);
274
- setDetailBusy(false);
275
- if (result.task) {
276
- setSelectedTask((current) => reconcileManualTransitionResponse({
277
- requestTask,
278
- expected,
279
- responseTask: result.task,
280
- selectedTask: current,
281
- }));
282
- setTasks((current) => current.map((candidate) => candidate.id === requestTask.id
283
- ? reconcileManualTransitionResponse({
284
- requestTask,
285
- expected,
286
- responseTask: result.task,
287
- selectedTask: candidate,
288
- })
289
- : candidate));
290
- }
291
- if (result.ok && result.task) {
292
- setHighlightTurnRef(result.task.turnRef ?? result.task.turnId);
293
- return { ok: true };
294
- } else {
295
- setDetailError(result.message ?? "Task state could not be changed.");
296
- return { ok: false, rotateActionId: result.code === "stale_task" };
297
- }
298
- }
299
-
300
- function handleNotificationOpen(notification: NotificationSnapshot) {
301
- const currentTask = tasks.find((task) => task.id === notification.taskId);
302
- const overlay = notificationClicked({
303
- detailTaskId: selectedId,
304
- detailOpened,
305
- highlightTurnRef,
306
- notifications: notificationState.notifications,
307
- refreshGeneration: detailGeneration.current,
308
- }, notification, Boolean(currentTask));
309
- setNotificationState((current) => ({ ...current, notifications: overlay.notifications }));
310
- if (!currentTask) {
311
- setMessage("This task is no longer present in the current snapshot.");
312
- return;
313
- }
314
- void loadDetail(currentTask, overlay.highlightTurnRef);
315
- }
316
-
317
- return (
318
- <MantineProvider
319
- defaultColorScheme="auto"
320
- getStyleNonce={styleNonce ? () => styleNonce : undefined}
321
- theme={theme}
322
- >
323
- <RelativeTimeProvider now={now}>
324
- <AppShell className="taskchef-shell" padding={0}>
325
- <Box className="taskchef-header-shell" component="header">
326
- <Container className={`taskchef-header${board ? " taskchef-header-board" : ""}${listLayout ? " taskchef-list-layout" : ""}`} size="100%">
327
- <Group className="taskchef-header-layout" justify="space-between" wrap="nowrap">
328
- <Group className="taskchef-header-copy" gap="xs" wrap="nowrap">
329
- <BrandIcon />
330
- <Title className="taskchef-brand-title" order={1}>
331
- <span>TaskChef Dashboard</span>
332
- {version && <span className="taskchef-version">v{version}</span>}
333
- </Title>
334
- </Group>
335
- <Group className="taskchef-header-actions" gap={2} justify="flex-end" wrap="nowrap">
336
- <Tooltip label="Settings"><ActionIcon className="taskchef-icon-button" component="a" href="#settings" aria-label="Settings" aria-current={settings ? 'page' : undefined} variant="subtle" color="gray" size="lg"><IconSettings size={18} stroke={1.8} /></ActionIcon></Tooltip>
337
- <ThemeToggle />
338
- <Group aria-live="polite" className="taskchef-connection" gap={7} role="status" wrap="nowrap">
339
- <IconCircleFilled aria-hidden color={connected ? "var(--mantine-color-teal-6)" : "var(--mantine-color-yellow-6)"} size={9} />
340
- <Text c="dimmed" size="xs">{connected ? "Live" : connect ? "Connecting…" : "Fixture preview"}</Text>
341
- </Group>
342
- </Group>
343
- </Group>
344
- </Container>
345
- </Box>
346
-
347
- <AppShell.Main>
348
- <Container className={`taskchef-main${board ? " taskchef-main-board" : ""}${listLayout ? " taskchef-list-layout" : ""}`} pb={80} size="100%">
349
- {projectIndex?.status === "unavailable" && (
350
- <Alert color="yellow" icon={<IconAlertTriangle size={18} />} mb="md" title="Project index unavailable">
351
- Task history is still visible. Verify the index and inspect backups before making project changes.
352
- </Alert>
353
- )}
354
- {projectIndex?.status === "available" && projectIndex.missingProjects.length > 0 && (
355
- <Alert color="yellow" icon={<IconAlertTriangle size={18} />} mb="md" title="Historical projects are missing from the current index">
356
- {projectIndex.missingProjects.map((item) => (
357
- <Text key={item.path} size="sm">
358
- {item.snapshotNames.join(", ")}: {item.taskCount} task(s). Run <code>taskchef backup list --json</code> and preview any restore; the dashboard never restores automatically.
359
- </Text>
360
- ))}
361
- </Alert>
362
- )}
363
- {settings ? <Stack className="taskchef-settings-page taskchef-content" gap="sm"><Button component="a" href="#" variant="subtle" size="xs" leftSection={<IconArrowLeft size={14} />} style={{ alignSelf: 'flex-start' }}>Back to tasks</Button><ModelSettings /></Stack> : <>
364
- <Paper className="taskchef-toolbar" radius={0}>
365
- <Stack gap="sm">
366
- <Group className="taskchef-toolbar-view">
367
- <SegmentedControl aria-label="View" data={[{ label: "Board", value: "board" }, { label: "List", value: "list" }]} onChange={changeView} size="xs" value={preferredView} withItemsBorders={false} />
368
- </Group>
369
- <Group className="taskchef-toolbar-filters" gap="sm" wrap="nowrap">
370
- <Select aria-label="Project" data={projects} onChange={(value) => { setProject(value ?? ""); setCompletedLimit(5); }} value={project} />
371
- <Select aria-label="Updated" data={[{ label: "Latest 24 hours", value: "24h" }, { label: "Latest 7 days", value: "7d" }, { label: "All time", value: "all" }]} onChange={(value) => { setDate(value ?? "all"); setCompletedLimit(5); }} value={date} />
372
- </Group>
373
- {!board && <Box className="taskchef-toolbar-status">
374
- <SegmentedControl aria-label="Status" data={statusData} onChange={setStatus} size="xs" value={status} withItemsBorders={false} />
375
- </Box>}
376
- </Stack>
377
- </Paper>
378
-
379
- {!board && <TaskResultsSummary totalCount={tasks.length} visibleCount={visible.length} />}
380
-
381
- {message && (
382
- <Alert color="yellow" icon={<IconAlertTriangle aria-hidden size={17} />} mt="md" role="status">
383
- <Group gap="sm" justify="space-between" wrap="nowrap">
384
- <Text size="sm">{message}</Text>
385
- <CloseButton aria-label="Dismiss dashboard message" onClick={() => setMessage(null)} size="sm" />
386
- </Group>
387
- </Alert>
388
- )}
389
-
390
- {board ? <TaskBoard
391
- completedLimit={completedLimit}
392
- onMoreCompleted={() => setCompletedLimit((limit) => limit + 5)}
393
- onOpenCodex={handleOpenCodex}
394
- onOpenDetail={(value) => void loadDetail(value)}
395
- tasks={boardTasks}
396
- /> : <Stack aria-describedby="task-results-summary" aria-label="Tasks" className="taskchef-list taskchef-content" component="section" gap="sm" mt="xs">
397
- {visible.map((task: Task) => (
398
- <TaskCard key={task.id} onOpenCodex={handleOpenCodex} onOpenDetail={(value) => void loadDetail(value)} task={task} />
399
- ))}
400
- {visible.length === 0 && (
401
- <Paper className="taskchef-empty" p={48} ta="center" withBorder>
402
- <Title order={2} size="h4">No tasks match these filters</Title>
403
- <Text c="dimmed" mt={4} size="sm">Choose a different project, update window, or status.</Text>
404
- </Paper>
405
- )}
406
- </Stack>}
407
- </>}
408
- </Container>
409
- </AppShell.Main>
410
- </AppShell>
411
-
412
- <TaskDetail
413
- busy={detailBusy}
414
- error={detailError}
415
- highlightTurnRef={highlightTurnRef}
416
- onClose={() => {
417
- if (!detailBusy) {
418
- detailGeneration.current += 1;
419
- setDetailOpened(false);
420
- setHighlightTurnRef(null);
421
- }
422
- }}
423
- onCopy={() => {
424
- if (!selectedTask) return;
425
- setDetailNotice(null);
426
- void navigator.clipboard.writeText(selectedTask.id).then(
427
- () => setDetailNotice("Task ID copied."),
428
- () => setDetailNotice("Clipboard access is unavailable. Copy the Task ID from metadata."),
429
- );
430
- }}
431
- onOpenCodex={() => selectedTask && void handleOpenCodex(selectedTask)}
432
- onTransition={handleTransition}
433
- opened={detailOpened}
434
- task={selectedTask}
435
- notice={detailNotice}
436
- notifications={detailOpened ? (
437
- <NotificationCenter
438
- announcements={notificationState.announcements}
439
- notifications={notificationState.notifications}
440
- onClear={() => setNotificationState((current) => ({
441
- ...current,
442
- notifications: clearNotificationHistory({
443
- detailTaskId: selectedId,
444
- detailOpened,
445
- highlightTurnRef,
446
- notifications: current.notifications,
447
- refreshGeneration: detailGeneration.current,
448
- }).notifications,
449
- }))}
450
- onDismiss={(notification) => setNotificationState((current) => ({ ...current, notifications: dismissNotification(current.notifications, notification.id) }))}
451
- onOpen={handleNotificationOpen}
452
- tasks={tasks}
453
- withinPortal={false}
454
- />
455
- ) : null}
456
- />
457
- {!detailOpened && (
458
- <NotificationCenter
459
- announcements={notificationState.announcements}
460
- notifications={notificationState.notifications}
461
- onClear={() => setNotificationState((current) => ({
462
- ...current,
463
- notifications: clearNotificationHistory({
464
- detailTaskId: selectedId,
465
- detailOpened,
466
- highlightTurnRef,
467
- notifications: current.notifications,
468
- refreshGeneration: detailGeneration.current,
469
- }).notifications,
470
- }))}
471
- onDismiss={(notification) => setNotificationState((current) => ({ ...current, notifications: dismissNotification(current.notifications, notification.id) }))}
472
- onOpen={handleNotificationOpen}
473
- tasks={tasks}
474
- />
475
- )}
476
- </RelativeTimeProvider>
477
- </MantineProvider>
478
- );
479
- }
480
-
481
- function ThemeToggle() {
482
- const { setColorScheme } = useMantineColorScheme();
483
- const colorScheme = useComputedColorScheme("light");
484
- const next = colorScheme === "dark" ? "light" : "dark";
485
- return (
486
- <Tooltip label={`Use ${next} theme`}>
487
- <ActionIcon
488
- aria-label={`Use ${next} theme`}
489
- className="taskchef-icon-button"
490
- color="gray"
491
- onClick={() => setColorScheme(next)}
492
- size="lg"
493
- variant="subtle"
494
- >
495
- {colorScheme === "dark" ? <IconSun aria-hidden size={18} stroke={1.8} /> : <IconMoon aria-hidden size={18} stroke={1.8} />}
496
- </ActionIcon>
497
- </Tooltip>
498
- );
499
- }
500
-
501
- function BrandIcon() {
502
- const colorScheme = useComputedColorScheme("light");
503
- return (
504
- <img
505
- alt=""
506
- aria-hidden
507
- className="taskchef-brand-icon"
508
- height="28"
509
- src={colorScheme === "dark" ? "/assets/taskchef-dark.svg" : "/assets/taskchef.svg"}
510
- width="28"
511
- />
512
- );
513
- }
514
-
515
- export default DashboardApp;
@@ -1,40 +0,0 @@
1
- import { afterEach, expect, test, vi } from "vitest";
2
- import { fixtureTask } from "./fixtures";
3
- import { manualTransition, updateModelRole } from "./api";
4
-
5
- afterEach(() => {
6
- vi.useRealTimers();
7
- vi.unstubAllGlobals();
8
- });
9
-
10
- test("times out a stalled manual transition and returns a retryable error", async () => {
11
- vi.useFakeTimers();
12
- vi.stubGlobal("fetch", vi.fn((_url: string, init?: RequestInit) => new Promise((_resolve, reject) => {
13
- init?.signal?.addEventListener("abort", () => reject(new DOMException("Aborted", "AbortError")));
14
- })));
15
-
16
- const request = manualTransition(fixtureTask(), "completed", crypto.randomUUID());
17
- await vi.advanceTimersByTimeAsync(10_000);
18
-
19
- await expect(request).resolves.toMatchObject({
20
- ok: false,
21
- code: "request_timeout",
22
- message: "Task state change timed out. Try again.",
23
- });
24
- });
25
-
26
- test("updates a personal model role with an exact JSON request", async () => {
27
- const fetchMock = vi.fn().mockResolvedValue({
28
- ok: true,
29
- json: async () => ({ profile: { id: "personal" } }),
30
- });
31
- vi.stubGlobal("fetch", fetchMock);
32
- await expect(updateModelRole("planner", "gpt-fixture", "high")).resolves.toEqual({
33
- profile: { id: "personal" },
34
- });
35
- expect(fetchMock).toHaveBeenCalledWith("/api/settings/planner", {
36
- method: "POST",
37
- headers: { "Content-Type": "application/json" },
38
- body: JSON.stringify({ schemaVersion: 1, model: "gpt-fixture", effort: "high" }),
39
- });
40
- });
@@ -1,103 +0,0 @@
1
- import type { DashboardSnapshot, ManualTransitionResponse, Task, TaskStatus } from "./types";
2
-
3
- export interface DashboardStream {
4
- close(): void;
5
- }
6
-
7
- export function connectDashboard({
8
- onConnection,
9
- onError,
10
- onSnapshot,
11
- }: {
12
- onConnection: (connected: boolean) => void;
13
- onError: (message: string) => void;
14
- onSnapshot: (snapshot: DashboardSnapshot) => void;
15
- }): DashboardStream {
16
- const events = new EventSource("/api/events");
17
- events.addEventListener("open", () => onConnection(true));
18
- events.addEventListener("error", () => onConnection(false));
19
- events.addEventListener("snapshot", (event) => {
20
- onConnection(true);
21
- onSnapshot(JSON.parse((event as MessageEvent).data) as DashboardSnapshot);
22
- });
23
- events.addEventListener("dashboard-error", (event) => {
24
- const payload = JSON.parse((event as MessageEvent).data) as { message?: string };
25
- onError(payload.message ?? "The task log is temporarily unavailable.");
26
- });
27
- return events;
28
- }
29
-
30
- async function jsonRequest<T>(url: string, init?: RequestInit): Promise<T> {
31
- const response = await fetch(url, init);
32
- const payload = await response.json().catch(() => ({}));
33
- if (!response.ok) throw Object.assign(new Error(payload.message ?? "Request failed."), { payload });
34
- return payload as T;
35
- }
36
-
37
- export async function dashboardVersion(): Promise<string> {
38
- const health = await jsonRequest<{ taskchefVersion: string }>("/api/health");
39
- return health.taskchefVersion;
40
- }
41
-
42
- export async function updateModelRole(role: string, model: string, effort: string) {
43
- return jsonRequest<{ profile: unknown }>(`/api/settings/${encodeURIComponent(role)}`, {
44
- method: "POST",
45
- headers: { "Content-Type": "application/json" },
46
- body: JSON.stringify({ schemaVersion: 1, model, effort }),
47
- });
48
- }
49
-
50
- export async function taskDetail(taskId: string): Promise<Task> {
51
- return (await jsonRequest<{ task: Task }>(`/api/tasks/${encodeURIComponent(taskId)}`)).task;
52
- }
53
-
54
- export async function openInCodex(taskId: string): Promise<string | null> {
55
- const result = await jsonRequest<{ message?: string }>(
56
- `/api/tasks/${encodeURIComponent(taskId)}/open-codex`,
57
- { method: "POST" },
58
- );
59
- return result.message ?? null;
60
- }
61
-
62
- export async function manualTransition(
63
- task: Task,
64
- targetStatus: Exclude<TaskStatus, "working" | "needs_input" | null>,
65
- actionId: string,
66
- ): Promise<ManualTransitionResponse> {
67
- const controller = new AbortController();
68
- const timeout = window.setTimeout(() => controller.abort(), 10_000);
69
- try {
70
- const result = await jsonRequest<{ task: Task }>(
71
- `/api/tasks/${encodeURIComponent(task.id)}/manual-transition`,
72
- {
73
- method: "POST",
74
- headers: { "Content-Type": "application/json" },
75
- body: JSON.stringify({
76
- schemaVersion: 1,
77
- actionId,
78
- expected: {
79
- status: task.status,
80
- turnRef: task.turnRef,
81
- threadId: task.threadId,
82
- updatedAt: task.updatedAt,
83
- },
84
- targetStatus,
85
- }),
86
- signal: controller.signal,
87
- },
88
- );
89
- return { ok: true, task: result.task };
90
- } catch (error) {
91
- const payload = (error as { payload?: { code?: string; message?: string; task?: Task } }).payload;
92
- return {
93
- ok: false,
94
- code: payload?.code ?? (controller.signal.aborted ? "request_timeout" : "network_error"),
95
- message: payload?.message ?? (controller.signal.aborted
96
- ? "Task state change timed out. Try again."
97
- : "Task state changes are temporarily unavailable. Try again."),
98
- task: payload?.task ?? null,
99
- };
100
- } finally {
101
- window.clearTimeout(timeout);
102
- }
103
- }