taskchef 8.7.1 → 9.0.1

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 (150) hide show
  1. package/.codex-plugin/plugin.json +7 -10
  2. package/.mcp.json +3 -2
  3. package/README.md +35 -158
  4. package/docs/mcp-app.md +23 -6
  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 +2 -2
  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/user-prompt.js +11 -0
  34. package/src/version.js +0 -1
  35. package/BACKLOG.md +0 -48
  36. package/assets/e2e-benchmark-example.json +0 -50
  37. package/assets/taskchef-dispatcher-instructions.md +0 -44
  38. package/bin/taskchef.js +0 -20
  39. package/docs/advanced-guide.md +0 -497
  40. package/docs/dashboard-lifecycle.md +0 -149
  41. package/docs/dashboard-ui.md +0 -40
  42. package/docs/firstmate-taskchef-comparison.md +0 -283
  43. package/docs/images/ccusage-token-consumption.png +0 -0
  44. package/docs/images/dashboard-identity.jpg +0 -0
  45. package/docs/images/dashboard-token-pending.jpg +0 -0
  46. package/docs/images/interrupted-turn-recovery.jpg +0 -0
  47. package/docs/images/notification-event-snapshots.jpg +0 -0
  48. package/docs/images/readme-board.png +0 -0
  49. package/docs/images/readme-completed.png +0 -0
  50. package/docs/images/readme-list.png +0 -0
  51. package/docs/images/readme-needs-input.png +0 -0
  52. package/docs/images/readme-roles.png +0 -0
  53. package/docs/images/reported-work-duration-desktop.jpg +0 -0
  54. package/docs/images/reported-work-duration-mobile.jpg +0 -0
  55. package/docs/images/result-history-dashboard.jpg +0 -0
  56. package/docs/images/taskchef-pr-icon-comparison.jpg +0 -0
  57. package/docs/images/taskchef-unified-status-popup.jpg +0 -0
  58. package/docs/workflows.md +0 -476
  59. package/mcp/dashboard-session.js +0 -51
  60. package/scripts/benchmark-dispatch-prepare.js +0 -90
  61. package/scripts/e2e-benchmark.js +0 -206
  62. package/scripts/roles/resolve_roles.py +0 -293
  63. package/skills/taskchef-bootstrap/SKILL.md +0 -96
  64. package/skills/taskchef-bootstrap/agents/openai.yaml +0 -4
  65. package/skills/taskchef-bootstrap/references/project-index.md +0 -154
  66. package/skills/taskchef-copilot/SKILL.md +0 -75
  67. package/skills/taskchef-copilot/agents/openai.yaml +0 -4
  68. package/skills/taskchef-dashboard/SKILL.md +0 -24
  69. package/skills/taskchef-dashboard/agents/openai.yaml +0 -4
  70. package/skills/taskchef-delegate/SKILL.md +0 -102
  71. package/skills/taskchef-delegate/agents/openai.yaml +0 -4
  72. package/skills/taskchef-delegate/references/model-roles.md +0 -29
  73. package/skills/taskchef-executor/SKILL.md +0 -154
  74. package/skills/taskchef-executor/agents/openai.yaml +0 -4
  75. package/skills/taskchef-executor/references/compatibility.md +0 -22
  76. package/skills/taskchef-executor/references/ending-actions.md +0 -26
  77. package/src/backups.js +0 -359
  78. package/src/cli.js +0 -733
  79. package/src/config-audit.js +0 -99
  80. package/src/config-mutations.js +0 -106
  81. package/src/dashboard/actions.js +0 -136
  82. package/src/dashboard/app.js +0 -1172
  83. package/src/dashboard/dist/app.js +0 -269
  84. package/src/dashboard/dist/app.js.map +0 -1
  85. package/src/dashboard/dist/index.html +0 -16
  86. package/src/dashboard/dist/styles.css +0 -1
  87. package/src/dashboard/index.html +0 -148
  88. package/src/dashboard/react/App.test.tsx +0 -346
  89. package/src/dashboard/react/App.tsx +0 -515
  90. package/src/dashboard/react/api.test.ts +0 -40
  91. package/src/dashboard/react/api.ts +0 -103
  92. package/src/dashboard/react/components/ActivityTimeline.tsx +0 -118
  93. package/src/dashboard/react/components/ModelSettings.test.tsx +0 -180
  94. package/src/dashboard/react/components/ModelSettings.tsx +0 -106
  95. package/src/dashboard/react/components/NotificationCenter.test.tsx +0 -76
  96. package/src/dashboard/react/components/NotificationCenter.tsx +0 -111
  97. package/src/dashboard/react/components/PullRequestBadges.tsx +0 -18
  98. package/src/dashboard/react/components/TaskDetail.test.tsx +0 -255
  99. package/src/dashboard/react/components/TaskDetail.tsx +0 -271
  100. package/src/dashboard/react/components/UsagePanel.tsx +0 -96
  101. package/src/dashboard/react/index.html +0 -15
  102. package/src/dashboard/react/main.tsx +0 -12
  103. package/src/dashboard/react/overlay-state.test.ts +0 -110
  104. package/src/dashboard/react/overlay-state.ts +0 -60
  105. package/src/dashboard/react/presentation.test.tsx +0 -571
  106. package/src/dashboard/styles.css +0 -261
  107. package/src/dashboard-control.js +0 -34
  108. package/src/dashboard-manager.js +0 -339
  109. package/src/dashboard-session-process.js +0 -88
  110. package/src/dashboard-session.js +0 -86
  111. package/src/dashboard.js +0 -1303
  112. package/src/delegation.js +0 -277
  113. package/src/github.js +0 -107
  114. package/src/model-roles.js +0 -70
  115. package/src/recorded-usage-tracker.js +0 -92
  116. package/src/usage-tracker.js +0 -650
  117. package/src/usage.js +0 -367
  118. package/src/workspace-path.js +0 -48
  119. package/src/workspace.js +0 -3629
  120. /package/src/{dashboard/react → ui}/components/AnimatedUsage.test.tsx +0 -0
  121. /package/src/{dashboard/react → ui}/components/CardStatusIcons.test.tsx +0 -0
  122. /package/src/{dashboard/react → ui}/components/ImageGallery.tsx +0 -0
  123. /package/src/{dashboard/react → ui}/components/OpenChatButton.test.tsx +0 -0
  124. /package/src/{dashboard/react → ui}/components/OpenChatButton.tsx +0 -0
  125. /package/src/{dashboard/react → ui}/components/PrGlyph.tsx +0 -0
  126. /package/src/{dashboard/react → ui}/components/QueuedPrompts.test.tsx +0 -0
  127. /package/src/{dashboard/react → ui}/components/QueuedPrompts.tsx +0 -0
  128. /package/src/{dashboard/react → ui}/components/ReplyCover.test.tsx +0 -0
  129. /package/src/{dashboard/react → ui}/components/ReplyCover.tsx +0 -0
  130. /package/src/{dashboard/react → ui}/components/ReplyImagePreview.tsx +0 -0
  131. /package/src/{dashboard/react → ui}/components/ReplyMarkdown.test.tsx +0 -0
  132. /package/src/{dashboard/react → ui}/components/ReplyMarkdown.tsx +0 -0
  133. /package/src/{dashboard/react → ui}/components/SavedUsageFormat.test.tsx +0 -0
  134. /package/src/{dashboard/react → ui}/components/SavedUsageFormat.tsx +0 -0
  135. /package/src/{dashboard/react → ui}/components/ShimmerText.tsx +0 -0
  136. /package/src/{dashboard/react → ui}/components/StatsGrid.tsx +0 -0
  137. /package/src/{dashboard/react → ui}/components/StatusBadge.tsx +0 -0
  138. /package/src/{dashboard/react → ui}/components/TaskBoard.test.tsx +0 -0
  139. /package/src/{dashboard/react → ui}/components/TaskCardStats.test.tsx +0 -0
  140. /package/src/{dashboard/react → ui}/components/TaskCardStats.tsx +0 -0
  141. /package/src/{dashboard/react → ui}/components/TurnContent.test.tsx +0 -0
  142. /package/src/{dashboard/react → ui}/fixtures.ts +0 -0
  143. /package/src/{dashboard → ui}/github-links.js +0 -0
  144. /package/src/{dashboard/react → ui}/image-cache.test.ts +0 -0
  145. /package/src/{dashboard/react → ui}/image-cache.ts +0 -0
  146. /package/src/{dashboard → ui}/state.js +0 -0
  147. /package/src/{dashboard/react → ui}/styles.css +0 -0
  148. /package/src/{dashboard/react → ui}/test-setup.ts +0 -0
  149. /package/src/{dashboard → ui}/time.js +0 -0
  150. /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
- );