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,255 +0,0 @@
1
- import { MantineProvider } from "@mantine/core";
2
- import { act, cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
3
- import { useState } from "react";
4
- import { afterEach, expect, test, vi } from "vitest";
5
- import { fixtureTask } from "../fixtures";
6
- import { ActivityTimeline } from "./ActivityTimeline";
7
- import { ManualTransitionConfirmation, TaskDetail, type TerminalStatus } from "./TaskDetail";
8
-
9
- afterEach(() => {
10
- cleanup();
11
- vi.restoreAllMocks();
12
- });
13
-
14
- test("shows only metadata available from a DB-backed observed task", () => {
15
- renderObservedDetail({
16
- archive: false,
17
- lastTurnEvent: "inProgress",
18
- lastTurnEventAt: null,
19
- recentFileActivity: true,
20
- });
21
-
22
- const metadata = screen.getByRole("heading", { name: "Metadata" }).closest("section")!;
23
- expect(within(metadata).getByText("Archive status")).toBeVisible();
24
- expect(within(metadata).getByText("Active")).toBeVisible();
25
- expect(within(metadata).queryByText("Location")).not.toBeInTheDocument();
26
- expect(within(metadata).getByText("inProgress")).toBeVisible();
27
- expect(within(metadata).queryByText("Observed messages")).not.toBeInTheDocument();
28
- expect(within(metadata).queryByText("Log bytes sampled")).not.toBeInTheDocument();
29
- expect(within(metadata).getByText("Queue reason")).toBeVisible();
30
- expect(metadata).not.toHaveTextContent("undefined");
31
- });
32
-
33
- test("shows archived database chat status without claiming a file location", () => {
34
- renderObservedDetail({
35
- archive: true,
36
- lastTurnEvent: "completed",
37
- lastTurnEventAt: null,
38
- recentFileActivity: false,
39
- });
40
-
41
- const metadata = screen.getByRole("heading", { name: "Metadata" }).closest("section")!;
42
- expect(within(metadata).getByText("Archive status")).toBeVisible();
43
- expect(within(metadata).getByText("Archived")).toBeVisible();
44
- expect(within(metadata).queryByText("Location")).not.toBeInTheDocument();
45
- });
46
-
47
- test("keeps log message and byte counts in observed task metadata", () => {
48
- renderObservedDetail({
49
- archive: true,
50
- lastTurnEvent: "completed",
51
- lastTurnEventAt: null,
52
- recentFileActivity: false,
53
- userMessages: 2,
54
- assistantMessages: 3,
55
- sampledBytes: 1024,
56
- fileBytes: 4096,
57
- }, "Local Codex log");
58
-
59
- const metadata = screen.getByRole("heading", { name: "Metadata" }).closest("section")!;
60
- expect(within(metadata).getByText("Location")).toBeVisible();
61
- expect(within(metadata).getByText("Archived sessions")).toBeVisible();
62
- expect(within(metadata).getByText("Observed messages")).toBeVisible();
63
- expect(within(metadata).getByText("2 user, 3 assistant")).toBeVisible();
64
- expect(within(metadata).getByText("Log bytes sampled")).toBeVisible();
65
- expect(within(metadata).getByText("1024 of 4096")).toBeVisible();
66
- });
67
-
68
- function renderObservedDetail(observed: NonNullable<ReturnType<typeof fixtureTask>["observed"]>, updatedBy = "Local Codex database") {
69
- render(
70
- <MantineProvider>
71
- <TaskDetail
72
- busy={false}
73
- error={null}
74
- highlightTurnRef={null}
75
- onClose={() => {}}
76
- onCopy={() => {}}
77
- onOpenCodex={() => {}}
78
- onTransition={async () => ({ ok: true })}
79
- opened
80
- readOnly
81
- task={fixtureTask({ observed, updatedBy })}
82
- />
83
- </MantineProvider>,
84
- );
85
- }
86
-
87
- test("clears a manual-transition confirmation only after success", async () => {
88
- const onTransition = vi.fn().mockResolvedValue({ ok: true });
89
- render(<ConfirmationHarness onTransition={onTransition} />);
90
- expect(screen.getByRole("alert")).toHaveTextContent("Mark task completed?");
91
- fireEvent.click(screen.getByRole("button", { name: "Confirm" }));
92
-
93
- await waitFor(() => expect(onTransition).toHaveBeenCalledWith("completed", expect.any(String)));
94
- await waitFor(() => expect(screen.queryByRole("alert")).not.toBeInTheDocument());
95
- await waitFor(() => expect(screen.getByRole("heading", { name: "Task title" })).toHaveFocus());
96
- });
97
-
98
- test("retains a manual-transition confirmation after failure", async () => {
99
- const onTransition = vi.fn().mockResolvedValue({ ok: false });
100
- render(<ConfirmationHarness onTransition={onTransition} />);
101
- fireEvent.click(screen.getByRole("button", { name: "Confirm" }));
102
-
103
- await waitFor(() => expect(onTransition).toHaveBeenCalledWith("completed", expect.any(String)));
104
- expect(screen.getByRole("alert")).toHaveTextContent("Mark task completed?");
105
- });
106
-
107
- test("reuses an action ID for retryable failures and rotates it after a stale task", async () => {
108
- const onTransition = vi.fn()
109
- .mockResolvedValueOnce({ ok: false })
110
- .mockResolvedValueOnce({ ok: false, rotateActionId: true })
111
- .mockResolvedValueOnce({ ok: false });
112
- render(<ConfirmationHarness onTransition={onTransition} />);
113
- const confirm = screen.getByRole("button", { name: "Confirm" });
114
- fireEvent.click(confirm);
115
- await waitFor(() => expect(onTransition).toHaveBeenCalledTimes(1));
116
- const firstActionId = onTransition.mock.calls[0][1];
117
- fireEvent.click(confirm);
118
- await waitFor(() => expect(onTransition).toHaveBeenCalledTimes(2));
119
- expect(onTransition.mock.calls[1][1]).toBe(firstActionId);
120
- fireEvent.click(confirm);
121
- await waitFor(() => expect(onTransition).toHaveBeenCalledTimes(3));
122
- expect(onTransition.mock.calls[2][1]).not.toBe(firstActionId);
123
- });
124
-
125
- test("covers terminal, active, and unavailable timeline metrics", () => {
126
- {
127
- const task = fixtureTask({
128
- status: "completed",
129
- turns: [{
130
- ...fixtureTask().turns![0],
131
- result: {
132
- status: "completed",
133
- summary: "Done.",
134
- updatedAt: "2026-08-30T08:18:32.000Z",
135
- },
136
- }],
137
- usage: {
138
- status: "available",
139
- turns: { "turn-one": { status: "available", totalTokens: 1_324_567, estimatedCostUsd: 0.12 } },
140
- },
141
- });
142
- const { container } = render(
143
- <MantineProvider><ActivityTimeline highlightTurnRef={null} task={task} /></MantineProvider>,
144
- );
145
- const stats = container.querySelector(".taskchef-turn-stats") as HTMLElement;
146
- expect(stats).not.toBeNull();
147
- expect(container.querySelector(".taskchef-turn-heading .taskchef-status-badge"))
148
- .toHaveTextContent("completed");
149
- expect(screen.queryByText(/^Turn ref:/)).not.toBeInTheDocument();
150
- const cells = [...stats.children] as HTMLElement[];
151
- expect(cells).toHaveLength(4);
152
- expect(within(cells[0]).getByLabelText(/^Turn update time:/i)).toBeVisible();
153
- expect(cells[0].querySelector(".tabler-icon-history")).not.toBeNull();
154
- expect(within(cells[1]).getByLabelText(/completed turn reported wall-clock elapsed time/i))
155
- .toHaveTextContent("18m 32s");
156
- expect(cells[1].querySelector(".tabler-icon-hourglass")).not.toBeNull();
157
- expect(within(cells[2]).getByLabelText("1,324,567 tokens")).toHaveTextContent("1.32M");
158
- expect(cells[2].querySelector(".tabler-icon-stack-2")).not.toBeNull();
159
- expect(within(cells[3]).getByLabelText("Estimated cost $0.12")).toHaveTextContent("0.12");
160
- expect(within(cells[3]).getByLabelText("Estimated cost $0.12")).not.toHaveTextContent("$");
161
- expect(cells[3].querySelector(".tabler-icon-currency-dollar")).not.toBeNull();
162
- cleanup();
163
- }
164
-
165
- {
166
- let now = Date.parse("2026-08-30T08:18:32.000Z");
167
- let tick: (() => void) | undefined;
168
- vi.spyOn(Date, "now").mockImplementation(() => now);
169
- vi.spyOn(window, "setInterval").mockImplementation(((handler: TimerHandler) => {
170
- tick = handler as () => void;
171
- return 1;
172
- }) as typeof window.setInterval);
173
- vi.spyOn(window, "clearInterval").mockImplementation(() => undefined);
174
- const task = fixtureTask();
175
- render(<MantineProvider><ActivityTimeline highlightTurnRef={null} task={task} /></MantineProvider>);
176
- expect(screen.getByLabelText(/current turn reported wall-clock elapsed time/i))
177
- .toHaveTextContent("18m 32s");
178
- expect(screen.getByLabelText("Tokens pending")).toHaveTextContent("Pending");
179
- expect(screen.getByLabelText("Estimated cost pending")).toHaveTextContent("Pending");
180
-
181
- act(() => {
182
- now += 2_000;
183
- tick?.();
184
- });
185
-
186
- expect(screen.getByLabelText(/current turn reported wall-clock elapsed time/i))
187
- .toHaveTextContent("18m 34s");
188
- cleanup();
189
- vi.restoreAllMocks();
190
- }
191
-
192
- {
193
- const task = fixtureTask({
194
- status: "completed",
195
- turns: [{
196
- ...fixtureTask().turns![0],
197
- startedAt: "2026-08-30T08:18:33.000Z",
198
- result: {
199
- status: "completed",
200
- summary: "Done.",
201
- updatedAt: "2026-08-30T08:18:32.000Z",
202
- },
203
- }],
204
- usage: {
205
- status: "unavailable",
206
- turns: {
207
- "turn-one": {
208
- status: "unavailable",
209
- reason: "Manual dashboard turns do not have usage boundaries.",
210
- },
211
- },
212
- },
213
- });
214
- render(<MantineProvider><ActivityTimeline highlightTurnRef={null} task={task} /></MantineProvider>);
215
- expect(screen.getByLabelText(/elapsed unavailable.*reported wall-clock/i))
216
- .toHaveTextContent("n/a");
217
- expect(screen.getByText("Manual dashboard turns do not have usage boundaries.")).toBeVisible();
218
- expect(screen.getByLabelText(/tokens unavailable: manual dashboard turns/i)).toBeVisible();
219
- }
220
- });
221
-
222
- function ConfirmationHarness({ onTransition }: { onTransition: (status: TerminalStatus, actionId: string) => Promise<{ ok: boolean; rotateActionId?: boolean }> }) {
223
- const [opened, setOpened] = useState(true);
224
- return (
225
- <MantineProvider>
226
- <h2 id="task-detail-title" tabIndex={-1}>Task title</h2>
227
- {opened && (
228
- <ManualTransitionConfirmation
229
- busy={false}
230
- onCancel={() => setOpened(false)}
231
- onTransition={onTransition}
232
- status="completed"
233
- />
234
- )}
235
- </MantineProvider>
236
- );
237
- }
238
-
239
-
240
- test("read-only Details allows copying an ID without exposing manual status actions", () => {
241
- const onCopy = vi.fn();
242
- render(<MantineProvider><TaskDetail busy={false} error={null} highlightTurnRef={null} onClose={vi.fn()} onCopy={onCopy} onOpenCodex={vi.fn()} onTransition={vi.fn()} opened readOnly task={fixtureTask()} /></MantineProvider>);
243
- expect(screen.queryByRole("button", {name:"More task actions"})).not.toBeInTheDocument();
244
- fireEvent.click(screen.getByRole("button", {name:"Copy Task ID"}));
245
- expect(onCopy).toHaveBeenCalledOnce();
246
- });
247
-
248
- test("observed web details show the full same-turn request and response", () => {
249
- const task=fixtureTask({promptExcerpt:'Short request',promptText:'Full recorded request',replyExcerpt:'Short response',replyText:'Full recorded response',
250
- observed:{archive:false,lastTurnEvent:'completed',lastTurnEventAt:null,recentFileActivity:false}});
251
- render(<MantineProvider><TaskDetail task={task} opened readOnly busy={false} error={null} highlightTurnRef={null} onClose={()=>{}} onCopy={()=>{}} onOpenCodex={()=>{}} onTransition={async()=>({ok:true})} /></MantineProvider>);
252
- expect(screen.getByLabelText('Latest turn request')).toHaveTextContent('Full recorded request');
253
- expect(screen.getByLabelText('Latest turn response')).toHaveTextContent('Full recorded response');
254
- expect(screen.queryByText('Latest saved reply (excerpt)')).toBeNull();
255
- });
@@ -1,271 +0,0 @@
1
- import { PullRequestBadges } from "./PullRequestBadges";
2
- import {
3
- ActionIcon,
4
- Alert,
5
- Box,
6
- Button,
7
- Divider,
8
- Drawer,
9
- Group,
10
- Menu,
11
- Modal,
12
- ScrollArea,
13
- Stack,
14
- Text,
15
- Title,
16
- Tooltip,
17
- } from "@mantine/core";
18
- import { useMediaQuery } from "@mantine/hooks";
19
- import {
20
- IconCheck,
21
- IconClipboard,
22
- IconDots,
23
- IconX,
24
- } from "@tabler/icons-react";
25
- import { useEffect, useRef, useState, type ReactNode } from "react";
26
- import type { Task } from "../types";
27
- import { ActivityTimeline } from "./ActivityTimeline";
28
- import { GitHubLinks } from "./GitHubLinks";
29
- import { OpenChatButton } from "./OpenChatButton";
30
- import { TurnContent } from "./TurnContent";
31
- import { RelativeTime } from "./RelativeTime";
32
- import { StatusBadge } from "./StatusBadge";
33
- import { UsagePanel } from "./UsagePanel";
34
-
35
- export type TerminalStatus = "completed" | "failed";
36
- export interface ManualTransitionResult {
37
- ok: boolean;
38
- rotateActionId?: boolean;
39
- }
40
-
41
- export function TaskDetail({
42
- busy,
43
- error,
44
- highlightTurnRef,
45
- onClose,
46
- onCopy,
47
- onOpenCodex,
48
- onTransition,
49
- opened,
50
- task,
51
- notifications,
52
- notice,
53
- readOnly = false,
54
- extraActions,
55
- }: {
56
- busy: boolean;
57
- error: string | null;
58
- highlightTurnRef: string | null;
59
- onClose: () => void;
60
- onCopy: () => void;
61
- onOpenCodex: () => void;
62
- onTransition: (status: TerminalStatus, actionId: string) => Promise<ManualTransitionResult>;
63
- opened: boolean;
64
- task: Task | null;
65
- notifications?: ReactNode;
66
- notice?: string | null;
67
- readOnly?: boolean;
68
- extraActions?: React.ReactNode;
69
- }) {
70
- const mobile = useMediaQuery("(max-width: 48em)");
71
- const [confirmStatus, setConfirmStatus] = useState<TerminalStatus | null>(null);
72
- useEffect(() => {
73
- if (!opened) setConfirmStatus(null);
74
- }, [opened]);
75
- useEffect(() => {
76
- if (opened && highlightTurnRef) {
77
- requestAnimationFrame(() => {
78
- document.querySelector<HTMLElement>(`[data-turn-ref="${CSS.escape(highlightTurnRef)}"]`)?.focus();
79
- });
80
- }
81
- }, [highlightTurnRef, opened, task?.turns?.length, task?.updatedAt]);
82
- useEffect(() => {
83
- if (confirmStatus && (!task || !manualTransitionTargets(task).includes(confirmStatus))) {
84
- setConfirmStatus(null);
85
- }
86
- }, [confirmStatus, task?.status]);
87
-
88
- if (!task) return null;
89
- const transitionTargets = manualTransitionTargets(task);
90
- const content = (
91
- <Stack gap="lg">
92
- {notifications}
93
- <Box>
94
- <Group align="flex-start" gap="sm" justify="space-between" wrap="nowrap">
95
- <Box>
96
- <Text c="teal" fw={700} size="xs" tt="uppercase">{task.project.name}</Text>
97
- <Title id="task-detail-title" order={2} size="h3" tabIndex={-1}>{task.title}</Title>
98
- </Box>
99
- <StatusBadge status={task.status} label={task.statusLabel} />
100
- </Group>
101
- <Group gap="xs" mt="md">
102
- <OpenChatButton loading={busy} onClick={onOpenCodex} taskTitle={task.title} />
103
- {extraActions}
104
- {readOnly && <Button leftSection={<IconClipboard size={14} />} onClick={onCopy} variant="default">Copy Task ID</Button>}
105
- {!readOnly && <Menu position="bottom-start" shadow="md" withinPortal zIndex={360}>
106
- <Menu.Target>
107
- <Tooltip label="More task actions">
108
- <ActionIcon aria-label="More task actions" className="taskchef-detail-more" disabled={busy} variant="default">
109
- <IconDots aria-hidden size={17} />
110
- </ActionIcon>
111
- </Tooltip>
112
- </Menu.Target>
113
- <Menu.Dropdown>
114
- <Menu.Item leftSection={<IconClipboard size={14} />} onClick={onCopy}>Copy Task ID</Menu.Item>
115
- {transitionTargets.includes("completed") && (
116
- <Menu.Item leftSection={<IconCheck size={14} />} onClick={() => setConfirmStatus("completed")}>Mark completed</Menu.Item>
117
- )}
118
- {transitionTargets.includes("failed") && (
119
- <Menu.Item color="red" leftSection={<IconX size={14} />} onClick={() => setConfirmStatus("failed")}>Mark failed</Menu.Item>
120
- )}
121
- </Menu.Dropdown>
122
- </Menu>}
123
- </Group>
124
- <Box mt="sm">
125
- <PullRequestBadges pullRequests={task.pullRequests} />
126
- <GitHubLinks task={task} />
127
- </Box>
128
- </Box>
129
-
130
- {confirmStatus && (
131
- <ManualTransitionConfirmation
132
- key={confirmStatus}
133
- busy={busy}
134
- onCancel={() => setConfirmStatus(null)}
135
- onTransition={onTransition}
136
- status={confirmStatus}
137
- />
138
- )}
139
- {notice && <Alert color="teal" role="status">{notice}</Alert>}
140
- {error && <Alert color="red" role="alert">{error}</Alert>}
141
-
142
- {!readOnly && <section aria-labelledby="usage-heading">
143
- <Title id="usage-heading" mb="xs" order={3} size="h5">Usage</Title>
144
- <UsagePanel task={task} />
145
- </section>}
146
- {!readOnly && <Divider />}
147
- {!readOnly && <section aria-labelledby="activity-heading">
148
- <Title id="activity-heading" mb="sm" order={3} size="h5">Activity timeline</Title>
149
- <ActivityTimeline highlightTurnRef={highlightTurnRef} task={task} />
150
- </section>}
151
- {!readOnly && <section aria-labelledby="instruction-heading">
152
- <Title id="instruction-heading" mb="xs" order={3} size="h5">Original instruction</Title>
153
- <Box className="taskchef-code-panel" component="pre">{task.instruction}</Box>
154
- </section>}
155
- {task.observed && <section aria-label="Latest turn"><TurnContent task={task} /></section>}
156
- <section aria-labelledby="metadata-heading">
157
- <Title id="metadata-heading" mb="xs" order={3} size="h5">Metadata</Title>
158
- <dl className="taskchef-metadata">
159
- <dt>Task ID</dt><dd>{task.id}</dd>
160
- <dt>Thread ID</dt><dd>{task.threadId ?? "—"}</dd>
161
- <dt>Current turn ref</dt><dd>{task.turnRef ?? "—"}</dd>
162
- <dt>Project path</dt><dd>{task.project.path}</dd>
163
- <dt>Created</dt><dd><RelativeTime label="Created time" value={task.createdAt} /></dd>
164
- <dt>Updated</dt><dd><RelativeTime label="Updated time" value={task.meaningfulUpdatedAt ?? task.updatedAt} /></dd>
165
- <dt>Updated by</dt><dd>{task.updatedBy ?? "—"}</dd>
166
- {task.observed?.directChildCount !== undefined && <><dt>Direct subagent chats (all history)</dt><dd>{task.observed.directChildCount}</dd></>}
167
- {task.observed && <>
168
- <dt>Queue reason</dt><dd>{task.summary}</dd>
169
- {task.updatedBy === "Local Codex database" ? <>
170
- <dt>Archive status</dt><dd>{task.observed.archive ? "Archived" : "Active"}</dd>
171
- </> : <>
172
- <dt>Location</dt><dd>{task.observed.archive ? "Archived sessions" : "Active sessions"}</dd>
173
- </>}
174
- {task.statusLabel && <><dt>Schedule</dt><dd>{task.scheduled ? "Active" : "None active"}</dd><dt>Latest input</dt><dd>{task.inputSource}</dd><dt>Done reason</dt><dd>{task.observed.archive ? "Archived" : task.manualDone ? "Manual mark" : "Not marked"}</dd></>}
175
- <dt>Latest turn event</dt><dd>{task.observed.lastTurnEvent ?? "Unknown"}</dd>
176
- {task.observed.userMessages !== undefined && task.observed.assistantMessages !== undefined && <>
177
- <dt>Observed messages</dt><dd>{task.observed.userMessages} user, {task.observed.assistantMessages} assistant</dd>
178
- </>}
179
- {task.observed.sampledBytes !== undefined && task.observed.fileBytes !== undefined && <>
180
- <dt>Log bytes sampled</dt><dd>{task.observed.sampledBytes} of {task.observed.fileBytes}</dd>
181
- </>}
182
- </>}
183
- </dl>
184
- </section>
185
- </Stack>
186
- );
187
-
188
- if (mobile) {
189
- return (
190
- <Drawer
191
- closeOnClickOutside={!busy}
192
- closeOnEscape={!busy}
193
- onClose={onClose}
194
- opened={opened}
195
- overlayProps={{ backgroundOpacity: 0.55, blur: 2 }}
196
- position="bottom"
197
- scrollAreaComponent={ScrollArea.Autosize}
198
- size="92%"
199
- title={<span className="taskchef-visually-hidden">Task details</span>}
200
- trapFocus
201
- zIndex={300}
202
- >
203
- {content}
204
- </Drawer>
205
- );
206
- }
207
- return (
208
- <Modal
209
- aria-labelledby="task-detail-title"
210
- closeOnClickOutside={!busy}
211
- closeOnEscape={!busy}
212
- onClose={onClose}
213
- opened={opened}
214
- overlayProps={{ backgroundOpacity: 0.55, blur: 2 }}
215
- scrollAreaComponent={ScrollArea.Autosize}
216
- size="min(600px, calc(100vw - 32px))"
217
- title={<span className="taskchef-visually-hidden">Task details</span>}
218
- trapFocus
219
- zIndex={300}
220
- >
221
- {content}
222
- </Modal>
223
- );
224
- }
225
-
226
- function manualTransitionTargets(task: Task): TerminalStatus[] {
227
- return task.status === "completed"
228
- ? ["failed"]
229
- : task.status === "failed"
230
- ? ["completed"]
231
- : task.status === "working" || task.status === "needs_input"
232
- ? ["completed", "failed"]
233
- : [];
234
- }
235
-
236
- export function ManualTransitionConfirmation({
237
- busy,
238
- onCancel,
239
- onTransition,
240
- status,
241
- }: {
242
- busy: boolean;
243
- onCancel: () => void;
244
- onTransition: (status: TerminalStatus, actionId: string) => Promise<ManualTransitionResult>;
245
- status: TerminalStatus;
246
- }) {
247
- const actionId = useRef(crypto.randomUUID());
248
- return (
249
- <Alert color={status === "failed" ? "red" : "teal"} role="alert" title={`Mark task ${status}?`}>
250
- <Text mb="sm" size="sm">This interrupts active work and appends an audited manual dashboard turn.</Text>
251
- <Group gap="xs">
252
- <Button
253
- color={status === "failed" ? "red" : "teal"}
254
- loading={busy}
255
- onClick={async () => {
256
- const result = await onTransition(status, actionId.current);
257
- if (result.ok) {
258
- onCancel();
259
- requestAnimationFrame(() => document.getElementById("task-detail-title")?.focus());
260
- }
261
- else if (result.rotateActionId) actionId.current = crypto.randomUUID();
262
- }}
263
- size="compact-sm"
264
- >
265
- Confirm
266
- </Button>
267
- <Button disabled={busy} onClick={onCancel} size="compact-sm" variant="default">Cancel</Button>
268
- </Group>
269
- </Alert>
270
- );
271
- }
@@ -1,96 +0,0 @@
1
- import { Box, Paper, Text } from "@mantine/core";
2
- import type { Task } from "../types";
3
- import {
4
- formatCompactTokens,
5
- formatEstimatedCost,
6
- formatFullTokens,
7
- taskReportedWorkView,
8
- usageView,
9
- } from "../presentation";
10
- import { ShimmerText } from "./ShimmerText";
11
-
12
- export function UsagePanel({ task }: { task: Task }) {
13
- const usage = usageView(task);
14
- const reportedWork = taskReportedWorkView(task);
15
- const ready = usage.kind === "ready" ? usage : null;
16
- const models = Object.keys(ready?.usage.models ?? {}).join(", ");
17
- const estimatedCost = ready?.usage.estimatedCostUsd;
18
- const hasEstimatedCost = typeof estimatedCost === "number" && Number.isFinite(estimatedCost);
19
- const stateValue = usage.kind === "pending"
20
- ? "Pending"
21
- : usage.kind === "calculating"
22
- ? "Calculating"
23
- : "n/a";
24
- const accessibleState = usage.kind === "unavailable" ? "unavailable" : stateValue.toLowerCase();
25
- const values: Array<{
26
- accessibleValue?: string;
27
- animated?: boolean;
28
- label: string;
29
- title?: string;
30
- value: string;
31
- }> = [
32
- {
33
- accessibleValue: ready
34
- ? `${formatFullTokens(ready.usage.totalTokens)} tokens`
35
- : usage.label,
36
- animated: usage.kind === "pending" || usage.kind === "calculating" || ready?.knownSoFar,
37
- label: "Tokens",
38
- title: ready ? `${formatFullTokens(ready.usage.totalTokens)} tokens` : usage.label,
39
- value: ready ? `${ready.usage.partial ? "≥" : ""}${formatCompactTokens(ready.usage.totalTokens)}` : stateValue,
40
- },
41
- {
42
- accessibleValue: ready
43
- ? ready.usage.estimatedCostUsd == null
44
- ? "Estimated cost unavailable"
45
- : `Estimated cost ${formatEstimatedCost(ready.usage.estimatedCostUsd)}`
46
- : `Estimated cost ${accessibleState}${
47
- usage.kind === "unavailable" ? `: ${usage.label}` : ""
48
- }`,
49
- animated: usage.kind === "pending"
50
- || usage.kind === "calculating"
51
- || (ready?.knownSoFar && hasEstimatedCost),
52
- label: "Estimated cost",
53
- title: ready?.usage.estimatedCostUsd == null
54
- ? "Estimated cost unavailable"
55
- : `Unrounded estimate: $${ready.usage.estimatedCostUsd}`,
56
- value: ready
57
- ? ready.usage.estimatedCostUsd == null ? "n/a" : `${ready.usage.costPartial ? "≥" : ""}${formatEstimatedCost(ready.usage.estimatedCostUsd)}`
58
- : stateValue,
59
- },
60
- {
61
- accessibleValue: reportedWork.accessibleLabel,
62
- label: "Duration",
63
- title: reportedWork.title,
64
- value: reportedWork.value,
65
- },
66
- {
67
- accessibleValue: models || "Models unavailable",
68
- label: "Models",
69
- title: models || "Models unavailable",
70
- value: models || "n/a",
71
- },
72
- ];
73
- return (
74
- <Box aria-live="polite">
75
- <Box className="taskchef-metrics">
76
- {values.map(({ accessibleValue, animated, label, title, value }) => (
77
- <Paper className="taskchef-metric" key={label} p="sm" radius="md" withBorder>
78
- <Text c="dimmed" size="xs">{label}</Text>
79
- <Text
80
- aria-label={accessibleValue}
81
- className="taskchef-metric-value"
82
- fw={650}
83
- size="sm"
84
- title={title}
85
- >
86
- {animated ? <ShimmerText>{value}</ShimmerText> : value}
87
- </Text>
88
- </Paper>
89
- ))}
90
- </Box>
91
- {usage.kind === "unavailable" && usage.label !== "Token usage unavailable" && (
92
- <Text c="dimmed" mt="xs" size="xs">{usage.label}</Text>
93
- )}
94
- </Box>
95
- );
96
- }
@@ -1,15 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <meta name="color-scheme" content="light dark" />
7
- <meta name="theme-color" content="#f3f5f2" />
8
- <meta name="taskchef-style-nonce" content="__TASKCHEF_STYLE_NONCE__" />
9
- <title>TaskChef Dashboard</title>
10
- </head>
11
- <body>
12
- <div id="root"></div>
13
- <script type="module" src="/main.tsx"></script>
14
- </body>
15
- </html>
@@ -1,12 +0,0 @@
1
- import "@mantine/core/styles.css";
2
- import "@mantine/notifications/styles.css";
3
- import { StrictMode } from "react";
4
- import { createRoot } from "react-dom/client";
5
- import DashboardApp from "./App";
6
- import "./styles.css";
7
-
8
- createRoot(document.getElementById("root")!).render(
9
- <StrictMode>
10
- <DashboardApp />
11
- </StrictMode>,
12
- );