taskchef 8.7.1 → 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.
- package/.codex-plugin/plugin.json +7 -10
- package/.mcp.json +3 -2
- package/README.md +31 -158
- package/docs/mcp-app.md +3 -3
- package/docs/spec.md +38 -746
- package/docs/taskchef-next-storage.md +2 -2
- package/index.js +2 -99
- package/package.json +12 -49
- package/skills/delegate/SKILL.md +44 -0
- package/skills/delegate/agents/openai.yaml +4 -0
- package/src/codex-app.js +3 -152
- package/src/codex-session-scanner.js +2 -2
- package/src/file-lock.js +25 -0
- package/src/mcp-app/react/NextTaskDetail.test.tsx +1 -1
- package/src/mcp-app/react/NextTaskDetail.tsx +8 -8
- package/src/mcp-app/react/main.test.tsx +3 -3
- package/src/mcp-app/react/main.tsx +7 -7
- package/src/mcp-app.js +2 -2
- package/src/mcp.js +4 -542
- package/src/next-cache.js +2 -2
- package/src/next-github.js +3 -3
- package/src/next-notifications.js +2 -2
- package/src/{dashboard/react → ui}/components/CardStatusIcons.tsx +1 -1
- package/src/{dashboard/react → ui}/components/GitHubLinks.tsx +1 -1
- package/src/{dashboard/react → ui}/components/LinkedText.tsx +1 -1
- package/src/{dashboard/react → ui}/components/RelativeTime.test.tsx +1 -1
- package/src/{dashboard/react → ui}/components/RelativeTime.tsx +1 -1
- package/src/{dashboard/react → ui}/components/ReplyImagePreview.test.tsx +1 -1
- package/src/{dashboard/react → ui}/components/TaskBoard.tsx +1 -1
- package/src/{dashboard/react → ui}/components/TaskCard.tsx +1 -1
- package/src/{dashboard/react → ui}/components/TurnContent.tsx +1 -1
- package/src/{dashboard/react → ui}/presentation.ts +1 -1
- package/src/version.js +0 -1
- package/BACKLOG.md +0 -48
- package/assets/e2e-benchmark-example.json +0 -50
- package/assets/taskchef-dispatcher-instructions.md +0 -44
- package/bin/taskchef.js +0 -20
- package/docs/advanced-guide.md +0 -497
- package/docs/dashboard-lifecycle.md +0 -149
- package/docs/dashboard-ui.md +0 -40
- package/docs/firstmate-taskchef-comparison.md +0 -283
- package/docs/images/ccusage-token-consumption.png +0 -0
- package/docs/images/dashboard-identity.jpg +0 -0
- package/docs/images/dashboard-token-pending.jpg +0 -0
- package/docs/images/interrupted-turn-recovery.jpg +0 -0
- package/docs/images/notification-event-snapshots.jpg +0 -0
- package/docs/images/readme-board.png +0 -0
- package/docs/images/readme-completed.png +0 -0
- package/docs/images/readme-list.png +0 -0
- package/docs/images/readme-needs-input.png +0 -0
- package/docs/images/readme-roles.png +0 -0
- package/docs/images/reported-work-duration-desktop.jpg +0 -0
- package/docs/images/reported-work-duration-mobile.jpg +0 -0
- package/docs/images/result-history-dashboard.jpg +0 -0
- package/docs/images/taskchef-pr-icon-comparison.jpg +0 -0
- package/docs/images/taskchef-unified-status-popup.jpg +0 -0
- package/docs/workflows.md +0 -476
- package/mcp/dashboard-session.js +0 -51
- package/scripts/benchmark-dispatch-prepare.js +0 -90
- package/scripts/e2e-benchmark.js +0 -206
- package/scripts/roles/resolve_roles.py +0 -293
- package/skills/taskchef-bootstrap/SKILL.md +0 -96
- package/skills/taskchef-bootstrap/agents/openai.yaml +0 -4
- package/skills/taskchef-bootstrap/references/project-index.md +0 -154
- package/skills/taskchef-copilot/SKILL.md +0 -75
- package/skills/taskchef-copilot/agents/openai.yaml +0 -4
- package/skills/taskchef-dashboard/SKILL.md +0 -24
- package/skills/taskchef-dashboard/agents/openai.yaml +0 -4
- package/skills/taskchef-delegate/SKILL.md +0 -102
- package/skills/taskchef-delegate/agents/openai.yaml +0 -4
- package/skills/taskchef-delegate/references/model-roles.md +0 -29
- package/skills/taskchef-executor/SKILL.md +0 -154
- package/skills/taskchef-executor/agents/openai.yaml +0 -4
- package/skills/taskchef-executor/references/compatibility.md +0 -22
- package/skills/taskchef-executor/references/ending-actions.md +0 -26
- package/src/backups.js +0 -359
- package/src/cli.js +0 -733
- package/src/config-audit.js +0 -99
- package/src/config-mutations.js +0 -106
- package/src/dashboard/actions.js +0 -136
- package/src/dashboard/app.js +0 -1172
- package/src/dashboard/dist/app.js +0 -269
- package/src/dashboard/dist/app.js.map +0 -1
- package/src/dashboard/dist/index.html +0 -16
- package/src/dashboard/dist/styles.css +0 -1
- package/src/dashboard/index.html +0 -148
- package/src/dashboard/react/App.test.tsx +0 -346
- package/src/dashboard/react/App.tsx +0 -515
- package/src/dashboard/react/api.test.ts +0 -40
- package/src/dashboard/react/api.ts +0 -103
- package/src/dashboard/react/components/ActivityTimeline.tsx +0 -118
- package/src/dashboard/react/components/ModelSettings.test.tsx +0 -180
- package/src/dashboard/react/components/ModelSettings.tsx +0 -106
- package/src/dashboard/react/components/NotificationCenter.test.tsx +0 -76
- package/src/dashboard/react/components/NotificationCenter.tsx +0 -111
- package/src/dashboard/react/components/PullRequestBadges.tsx +0 -18
- package/src/dashboard/react/components/TaskDetail.test.tsx +0 -255
- package/src/dashboard/react/components/TaskDetail.tsx +0 -271
- package/src/dashboard/react/components/UsagePanel.tsx +0 -96
- package/src/dashboard/react/index.html +0 -15
- package/src/dashboard/react/main.tsx +0 -12
- package/src/dashboard/react/overlay-state.test.ts +0 -110
- package/src/dashboard/react/overlay-state.ts +0 -60
- package/src/dashboard/react/presentation.test.tsx +0 -571
- package/src/dashboard/styles.css +0 -261
- package/src/dashboard-control.js +0 -34
- package/src/dashboard-manager.js +0 -339
- package/src/dashboard-session-process.js +0 -88
- package/src/dashboard-session.js +0 -86
- package/src/dashboard.js +0 -1303
- package/src/delegation.js +0 -277
- package/src/github.js +0 -107
- package/src/model-roles.js +0 -70
- package/src/recorded-usage-tracker.js +0 -92
- package/src/usage-tracker.js +0 -650
- package/src/usage.js +0 -367
- package/src/workspace-path.js +0 -48
- package/src/workspace.js +0 -3629
- /package/src/{dashboard/react → ui}/components/AnimatedUsage.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/CardStatusIcons.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ImageGallery.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/OpenChatButton.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/OpenChatButton.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/PrGlyph.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/QueuedPrompts.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/QueuedPrompts.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyCover.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyCover.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyImagePreview.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyMarkdown.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyMarkdown.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/SavedUsageFormat.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/SavedUsageFormat.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ShimmerText.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/StatsGrid.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/StatusBadge.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/TaskBoard.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/TaskCardStats.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/TaskCardStats.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/TurnContent.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/fixtures.ts +0 -0
- /package/src/{dashboard → ui}/github-links.js +0 -0
- /package/src/{dashboard/react → ui}/image-cache.test.ts +0 -0
- /package/src/{dashboard/react → ui}/image-cache.ts +0 -0
- /package/src/{dashboard → ui}/state.js +0 -0
- /package/src/{dashboard/react → ui}/styles.css +0 -0
- /package/src/{dashboard/react → ui}/test-setup.ts +0 -0
- /package/src/{dashboard → ui}/time.js +0 -0
- /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
|
-
}
|