taskchef 8.3.2 → 8.4.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.
@@ -0,0 +1,147 @@
1
+ import { Box, Button, Paper, Stack, Text, Title } from "@mantine/core";
2
+ import { useRef, type PointerEvent as ReactPointerEvent } from "react";
3
+ import { IconArrowUpRight } from "@tabler/icons-react";
4
+ import { hasLinkedCodexThread, latestTurnPresentation } from "../../state.js";
5
+ import type { Task, TaskStatus } from "../types";
6
+ import { LinkedText } from "./LinkedText";
7
+ import { RelativeTime } from "./RelativeTime";
8
+
9
+ const lanes: { status: TaskStatus; label: string }[] = [
10
+ { status: "working", label: "Working" },
11
+ { status: "needs_input", label: "Needs input" },
12
+ { status: "completed", label: "Completed" },
13
+ { status: "failed", label: "Failed" },
14
+ { status: null, label: "Unresolved" },
15
+ ];
16
+ const knownStatuses = new Set<TaskStatus>(["working", "needs_input", "completed", "failed"]);
17
+
18
+ function laneFor(task: Task): TaskStatus {
19
+ return knownStatuses.has(task.status) ? task.status : null;
20
+ }
21
+
22
+ export function TaskBoard({
23
+ completedLimit,
24
+ onMoreCompleted,
25
+ onOpenCodex,
26
+ onOpenDetail,
27
+ tasks,
28
+ }: {
29
+ completedLimit: number;
30
+ onMoreCompleted: () => void;
31
+ onOpenCodex: (task: Task) => void;
32
+ onOpenDetail: (task: Task) => void;
33
+ tasks: Task[];
34
+ }) {
35
+ const boardRef = useRef<HTMLDivElement>(null);
36
+ const dragRef = useRef<{ pointerId: number; startX: number; scrollLeft: number; moved: boolean } | null>(null);
37
+ const suppressClickRef = useRef(false);
38
+
39
+ function onPointerDown(event: ReactPointerEvent<HTMLDivElement>) {
40
+ if (event.pointerType !== "mouse" || event.button !== 0 || !event.isPrimary) return;
41
+ const target = event.target as Element;
42
+ if (target.closest("button, a, input, textarea, select, [contenteditable], .taskchef-board-card, h1, h2, h3, p, span")) return;
43
+ const board = boardRef.current;
44
+ if (!board || board.scrollWidth <= board.clientWidth) return;
45
+ dragRef.current = { pointerId: event.pointerId, startX: event.clientX, scrollLeft: board.scrollLeft, moved: false };
46
+ board.setPointerCapture(event.pointerId);
47
+ }
48
+
49
+ function onPointerMove(event: ReactPointerEvent<HTMLDivElement>) {
50
+ const drag = dragRef.current;
51
+ const board = boardRef.current;
52
+ if (!drag || !board || drag.pointerId !== event.pointerId) return;
53
+ const delta = event.clientX - drag.startX;
54
+ if (!drag.moved && Math.abs(delta) < 6) return;
55
+ if (!drag.moved) {
56
+ drag.moved = true;
57
+ board.classList.add("taskchef-board-dragging");
58
+ }
59
+ event.preventDefault();
60
+ board.scrollLeft = drag.scrollLeft - delta;
61
+ }
62
+
63
+ function endPointer(event: ReactPointerEvent<HTMLDivElement>) {
64
+ const drag = dragRef.current;
65
+ const board = boardRef.current;
66
+ if (!drag || !board || drag.pointerId !== event.pointerId) return;
67
+ dragRef.current = null;
68
+ board.classList.remove("taskchef-board-dragging");
69
+ if (board.hasPointerCapture(event.pointerId)) board.releasePointerCapture(event.pointerId);
70
+ if (drag.moved) {
71
+ suppressClickRef.current = true;
72
+ window.setTimeout(() => { suppressClickRef.current = false; }, 0);
73
+ }
74
+ }
75
+
76
+ const visibleLanes = lanes.filter((lane) => lane.status !== null || tasks.some((task) => laneFor(task) === null));
77
+ return (
78
+ <Box
79
+ aria-label="Task board"
80
+ className="taskchef-board"
81
+ component="section"
82
+ onClickCapture={(event) => {
83
+ if (!suppressClickRef.current) return;
84
+ event.preventDefault();
85
+ event.stopPropagation();
86
+ suppressClickRef.current = false;
87
+ }}
88
+ onPointerCancel={endPointer}
89
+ onPointerDown={onPointerDown}
90
+ onPointerMove={onPointerMove}
91
+ onPointerUp={endPointer}
92
+ ref={boardRef}
93
+ tabIndex={0}
94
+ >
95
+ {visibleLanes.map(({ status, label }) => {
96
+ const matching = tasks.filter((task) => laneFor(task) === status);
97
+ const shown = status === "completed" ? matching.slice(0, completedLimit) : matching;
98
+ return (
99
+ <Box aria-label={`${label}, ${matching.length} tasks`} className="taskchef-board-lane" component="section" key={label}>
100
+ <Box className="taskchef-board-lane-heading">
101
+ <Title order={2} size="h5">{label}</Title>
102
+ <Text aria-label={`${matching.length} tasks`} c="dimmed" size="sm">{matching.length}</Text>
103
+ </Box>
104
+ <Stack gap="sm">
105
+ {shown.map((task) => <BoardCard key={task.id} onOpenCodex={onOpenCodex} onOpenDetail={onOpenDetail} task={task} />)}
106
+ {matching.length === 0 && <Text c="dimmed" className="taskchef-board-empty" size="sm">No tasks</Text>}
107
+ {status === "completed" && matching.length > shown.length && (
108
+ <Button className="taskchef-board-more" onClick={onMoreCompleted} size="compact-sm" variant="subtle">
109
+ Show {Math.min(5, matching.length - shown.length)} more · {shown.length} of {matching.length}
110
+ </Button>
111
+ )}
112
+ </Stack>
113
+ </Box>
114
+ );
115
+ })}
116
+ </Box>
117
+ );
118
+ }
119
+
120
+ function BoardCard({ task, onOpenCodex, onOpenDetail }: {
121
+ task: Task;
122
+ onOpenCodex: (task: Task) => void;
123
+ onOpenDetail: (task: Task) => void;
124
+ }) {
125
+ const latest = latestTurnPresentation(task);
126
+ const excerpt = task.status === "working" ? latest.requestSummary : latest.resultSummary;
127
+ const linked = hasLinkedCodexThread(task);
128
+ return (
129
+ <Paper className="taskchef-board-card" component="article" px="sm" pt="sm" pb={6} withBorder>
130
+ <Title className="taskchef-board-title" order={3} size="h5">
131
+ <button className="taskchef-title-button" onClick={() => onOpenDetail(task)} type="button">{task.title}</button>
132
+ </Title>
133
+ <Text className="taskchef-board-project" size="xs">{task.project.name}</Text>
134
+ <Text className="taskchef-board-excerpt taskchef-preserve-lines" size="sm">
135
+ <LinkedText task={task} text={excerpt} />
136
+ </Text>
137
+ <Box className="taskchef-board-card-footer">
138
+ <RelativeTime label="Updated time" value={task.meaningfulUpdatedAt ?? task.updatedAt} />
139
+ {linked ? (
140
+ <button aria-label={`Open chat for ${task.title}`} className="taskchef-board-chat" onClick={() => onOpenCodex(task)} title="Open chat" type="button">
141
+ <IconArrowUpRight aria-hidden size={19} stroke={1.6} />
142
+ </button>
143
+ ) : task.status === "working" ? <Text c="dimmed" className="taskchef-board-link-pending" size="xs">Chat link pending</Text> : null}
144
+ </Box>
145
+ </Paper>
146
+ );
147
+ }
@@ -1,9 +1,9 @@
1
1
  import { Box, Paper, Stack, Text, Title } from "@mantine/core";
2
+ import { IconArrowUpRight } from "@tabler/icons-react";
2
3
  import { latestTurnPresentation } from "../../state.js";
3
4
  import type { Task } from "../types";
4
5
  import { GitHubLinks } from "./GitHubLinks";
5
6
  import { LinkedText } from "./LinkedText";
6
- import { OpenChatButton } from "./OpenChatButton";
7
7
  import { ShimmerText } from "./ShimmerText";
8
8
  import { StatusBadge } from "./StatusBadge";
9
9
  import { TaskCardStats } from "./TaskCardStats";
@@ -19,7 +19,7 @@ export function TaskCard({
19
19
  }) {
20
20
  const latest = latestTurnPresentation(task);
21
21
  return (
22
- <Paper className="taskchef-task-row" component="article" p="md" radius="md" withBorder>
22
+ <Paper className="taskchef-task-row" component="article" pb={8} pt="md" px="sm" radius="md" withBorder>
23
23
  <Stack className="taskchef-task-main" gap="sm">
24
24
  <Box>
25
25
  <Box className="taskchef-card-heading">
@@ -30,7 +30,6 @@ export function TaskCard({
30
30
  </Title>
31
31
  <Stack align="flex-end" className="taskchef-card-metadata" gap={4}>
32
32
  <StatusBadge status={task.status} />
33
- <OpenChatButton onClick={() => onOpenCodex(task)} taskTitle={task.title} />
34
33
  </Stack>
35
34
  </Box>
36
35
  <Text c="dimmed" mt={2} size="xs">{task.project.name}</Text>
@@ -50,7 +49,12 @@ export function TaskCard({
50
49
  </Box>
51
50
 
52
51
  <GitHubLinks task={task} />
53
- <TaskCardStats task={task} />
52
+ <Box className="taskchef-list-card-footer">
53
+ <TaskCardStats task={task} />
54
+ <button aria-label={`Open chat for ${task.title}`} className="taskchef-board-chat" onClick={() => onOpenCodex(task)} title="Open chat" type="button">
55
+ <IconArrowUpRight aria-hidden size={19} stroke={1.6} />
56
+ </button>
57
+ </Box>
54
58
  </Stack>
55
59
  </Paper>
56
60
  );
@@ -128,7 +128,7 @@ describe("task card stats", () => {
128
128
  expect(container.querySelectorAll(".taskchef-shimmer")).toHaveLength(1);
129
129
  });
130
130
 
131
- test("places Open chat beneath the status badge and keeps callbacks working", () => {
131
+ test("places statistics and icon-only Open chat together in the card footer", () => {
132
132
  const openChat = vi.fn();
133
133
  const openDetail = vi.fn();
134
134
  const { container } = render(<MantineProvider><TaskCard
@@ -137,9 +137,16 @@ describe("task card stats", () => {
137
137
  task={fixtureTask()}
138
138
  /></MantineProvider>);
139
139
  const metadata = container.querySelector(".taskchef-card-metadata") as HTMLElement;
140
+ const footer = container.querySelector(".taskchef-list-card-footer") as HTMLElement;
141
+ const stats = container.querySelector(".taskchef-card-stats") as HTMLElement;
140
142
  expect(metadata.children[0]).toHaveClass("taskchef-status-badge");
141
- expect(within(metadata).getByRole("button", { name: /open chat/i })).toBe(metadata.children[1]);
142
- fireEvent.click(within(metadata).getByRole("button", { name: /open chat/i }));
143
+ expect(metadata.children).toHaveLength(1);
144
+ expect(footer.children[0]).toBe(stats);
145
+ const button = within(footer).getByRole("button", { name: /open chat/i });
146
+ expect(footer.children[1]).toBe(button);
147
+ expect(button).toHaveClass("taskchef-board-chat");
148
+ expect(button).not.toHaveTextContent("Open chat");
149
+ fireEvent.click(button);
143
150
  fireEvent.click(screen.getByRole("button", { name: /^review checkout reconciliation$/i }));
144
151
  expect(openChat).toHaveBeenCalledOnce();
145
152
  expect(openDetail).toHaveBeenCalledOnce();
@@ -19,7 +19,6 @@ export function TaskCardStats({ task }: { task: Task }) {
19
19
  <RelativeTime
20
20
  icon={<IconHistory aria-hidden size={14} />}
21
21
  label={`Updated time for ${task.title}`}
22
- tooltipLabel="Updated at"
23
22
  value={task.meaningfulUpdatedAt ?? task.updatedAt}
24
23
  />
25
24
  </StatCell>
@@ -97,7 +97,7 @@ export function TaskDetail({
97
97
  <Menu position="bottom-start" shadow="md" withinPortal zIndex={360}>
98
98
  <Menu.Target>
99
99
  <Tooltip label="More task actions">
100
- <ActionIcon aria-label="More task actions" disabled={busy} variant="default">
100
+ <ActionIcon aria-label="More task actions" className="taskchef-detail-more" disabled={busy} variant="default">
101
101
  <IconDots aria-hidden size={17} />
102
102
  </ActionIcon>
103
103
  </Tooltip>
@@ -16,27 +16,32 @@ body {
16
16
  }
17
17
 
18
18
  .taskchef-shell { min-height: 100vh; background: var(--taskchef-page); }
19
- .taskchef-shell header { border-bottom: 1px solid var(--taskchef-border); }
20
- .taskchef-header { padding-top: 28px; padding-bottom: 22px; }
19
+ .taskchef-header-shell { border-bottom: 1px solid var(--taskchef-border); }
20
+ .taskchef-header { max-width: 1510px; padding: 10px clamp(12px, 3vw, 48px); }
21
+ .taskchef-header-layout { min-width: 0; min-height: 36px; gap: 8px; }
21
22
  .taskchef-header-copy { min-width: 0; }
22
- .taskchef-header-actions { align-self: stretch; flex: none; }
23
- .taskchef-connection { align-self: flex-end; }
24
- .taskchef-header-icon-row { justify-content: flex-end; }
23
+ .taskchef-header-actions { flex: none; }
24
+ .taskchef-connection { flex: none; margin-inline-start: 5px; }
25
25
  .taskchef-icon-button { flex: none; }
26
- .taskchef-version { margin-inline-start: 0.45rem; color: var(--taskchef-muted); font-weight: 600; letter-spacing: 0; text-transform: none; }
27
- .taskchef-brand-icon, .taskchef-brand-icon img { display: block; width: 42px; height: 42px; }
28
- .taskchef-main { padding-top: var(--mantine-spacing-md); }
29
- .taskchef-toolbar { padding-top: var(--mantine-spacing-md); background: light-dark(rgb(255 255 255 / 86%), rgb(34 39 36 / 88%)); }
30
- .taskchef-toolbar .mantine-InputWrapper-root { flex: 1 1 190px; max-width: 240px; }
26
+ .taskchef-brand-title { min-width: 0; color: var(--taskchef-accent); font-size: 0.875rem; font-weight: 750; letter-spacing: 0.035em; line-height: 1; text-transform: uppercase; white-space: nowrap; }
27
+ .taskchef-version { margin-inline-start: 0.45rem; color: var(--taskchef-muted); font-size: 0.78em; font-weight: 600; letter-spacing: 0; text-transform: none; }
28
+ .taskchef-brand-icon, .taskchef-brand-icon img { display: block; flex: none; width: 28px; height: 28px; }
29
+ .taskchef-main { max-width: 1510px; padding-top: 8px; padding-inline: clamp(16px, 3vw, 48px); }
30
+ .taskchef-toolbar { max-width: none; margin-bottom: 22px; padding: 8px 0 var(--mantine-spacing-md); border: 0; border-bottom: 1px solid var(--taskchef-border); background: transparent; box-shadow: none; }
31
+ .taskchef-toolbar-filters { width: min(100%, 488px); min-width: 0; }
32
+ .taskchef-toolbar-filters .mantine-InputWrapper-root { flex: 1 1 0; width: 0; min-width: 0; max-width: 240px; }
31
33
  .taskchef-toolbar .mantine-InputWrapper-label, .taskchef-filter-label { font-size: var(--mantine-font-size-sm); font-weight: 600; line-height: var(--mantine-line-height-sm); }
32
34
  .taskchef-toolbar .mantine-SegmentedControl-root { max-width: 650px; }
33
- .taskchef-results-summary { margin-top: 12px; color: var(--taskchef-muted); font-size: var(--mantine-font-size-xs); line-height: 1.35; white-space: nowrap; }
35
+ .taskchef-results-summary { max-width: 1080px; margin-top: 12px; color: var(--taskchef-muted); font-size: var(--mantine-font-size-xs); line-height: 1.35; white-space: nowrap; }
36
+ .taskchef-list { max-width: 1080px; }
34
37
  .taskchef-task-row { container-type: inline-size; background: light-dark(#fff, #202421); box-shadow: 0 1px 0 light-dark(rgb(22 31 25 / 3%), transparent); }
35
38
  .taskchef-task-row:hover { border-color: light-dark(var(--mantine-color-teal-4), var(--mantine-color-teal-8)); }
36
39
  .taskchef-task-main { min-width: 0; flex: 1; }
37
40
  .taskchef-card-heading,
38
41
  .taskchef-turn-heading { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: start; gap: var(--mantine-spacing-sm); }
39
42
  .taskchef-card-metadata { width: max-content; min-width: 0; max-width: min(25rem, 45cqw); }
43
+ .taskchef-list-card-footer { display: flex; align-items: end; gap: 4px; min-width: 0; }
44
+ .taskchef-list-card-footer .taskchef-card-stats { flex: 1 1 auto; min-width: 0; margin-top: 2px; }
40
45
  .taskchef-status-badge { flex: none; font-size: var(--mantine-font-size-xs); }
41
46
  .taskchef-title-button {
42
47
  all: unset;
@@ -68,6 +73,47 @@ body {
68
73
  .taskchef-stat + .taskchef-stat { border-inline-start: 1px solid var(--taskchef-border); }
69
74
  .taskchef-empty { border-style: dashed; background: transparent; }
70
75
 
76
+ .taskchef-toolbar .mantine-Select-input { width: 100%; height: 30px; min-height: 30px; padding-inline-start: 12px; font-size: var(--mantine-font-size-xs); }
77
+ .taskchef-toolbar-view .mantine-SegmentedControl-root, .taskchef-toolbar-status .mantine-SegmentedControl-root { width: max-content; max-width: 100%; height: 30px; padding: 0; }
78
+ .taskchef-toolbar-view .mantine-SegmentedControl-label, .taskchef-toolbar-status .mantine-SegmentedControl-label { display: flex; align-items: center; height: 100%; padding-inline: 12px; font-size: var(--mantine-font-size-xs); text-align: start; }
79
+ .taskchef-board {
80
+ display: grid;
81
+ grid-auto-flow: column;
82
+ grid-auto-columns: min(340px, calc(100% - 36px));
83
+ align-items: stretch;
84
+ gap: 18px;
85
+ width: 100%;
86
+ min-width: 0;
87
+ min-height: max(360px, calc(100dvh - 270px));
88
+ overflow-x: auto;
89
+ overscroll-behavior-x: contain;
90
+ scroll-snap-type: x mandatory;
91
+ scrollbar-width: thin;
92
+ }
93
+ .taskchef-board:focus-visible { outline: 2px solid var(--mantine-color-teal-6); outline-offset: 2px; }
94
+ .taskchef-board-dragging { scroll-snap-type: none; user-select: none; cursor: grabbing; }
95
+ .taskchef-board-lane { min-width: 0; scroll-snap-align: start; cursor: grab; }
96
+ .taskchef-board-lane-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; padding: 0 13px 6px; margin-bottom: 6px; }
97
+ .taskchef-board-lane-heading h2 { font-size: var(--mantine-font-size-sm); font-weight: 680; }
98
+ .taskchef-board-empty { padding: 12px 4px; }
99
+ .taskchef-board-card { display: flex; flex-direction: column; min-width: 0; cursor: auto; background: light-dark(#fff, #202421); border-color: var(--taskchef-border); box-shadow: 0 1px 2px light-dark(rgb(22 31 25 / 3%), transparent); }
100
+ .taskchef-board-card:hover { border-color: light-dark(#bdc8c0, #5b685e); }
101
+ .taskchef-board-project { color: var(--taskchef-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
102
+ .taskchef-board-title { line-height: 1.3; }
103
+ .taskchef-board-project { margin-top: 8px; }
104
+ .taskchef-board-title .taskchef-title-button { display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical; -webkit-line-clamp: 3; line-clamp: 3; }
105
+ .taskchef-board-excerpt { display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical; -webkit-line-clamp: 3; line-clamp: 3; margin-top: 18px; color: var(--taskchef-text); }
106
+ .taskchef-board-card-footer { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; margin-top: auto; padding-top: 10px; }
107
+ .taskchef-board-card-footer .taskchef-time { padding-inline-start: 0; }
108
+ .taskchef-board-chat { display: inline-flex; align-items: center; justify-content: center; flex: none; width: 30px; height: 30px; padding: 0; border: 0; border-radius: 4px; background: transparent; color: var(--taskchef-muted); cursor: pointer; }
109
+ .taskchef-board-chat:hover { background: var(--taskchef-surface-muted); color: var(--taskchef-text); }
110
+ .taskchef-board-chat:focus-visible { outline: 2px solid var(--mantine-color-teal-6); outline-offset: 2px; }
111
+ .taskchef-open-chat, .taskchef-detail-more { height: 26px; min-height: 26px; }
112
+ .taskchef-open-chat .mantine-Button-section[data-position="left"] { margin-inline-end: 4px; }
113
+ .taskchef-detail-more { width: 26px; min-width: 26px; }
114
+ .taskchef-board-link-pending { text-align: right; }
115
+ .taskchef-board-more { align-self: flex-start; padding-inline: 4px; color: var(--taskchef-muted); }
116
+
71
117
  .taskchef-notifications {
72
118
  position: fixed;
73
119
  z-index: 400;
@@ -120,7 +166,7 @@ body {
120
166
  .taskchef-visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
121
167
 
122
168
  @media (max-width: 48em) {
123
- .taskchef-toolbar .mantine-InputWrapper-root { max-width: none; }
169
+ .taskchef-toolbar-filters .mantine-InputWrapper-root { max-width: none; }
124
170
  .taskchef-toolbar .mantine-Group-root { align-items: stretch; }
125
171
  .taskchef-toolbar .mantine-SegmentedControl-root { overflow-x: auto; }
126
172
  .taskchef-notifications { right: 16px; bottom: 16px; width: min(330px, calc(100vw - 32px)); }
@@ -130,8 +176,15 @@ body {
130
176
  }
131
177
 
132
178
  @media (max-width: 34em) {
133
- .taskchef-header { padding-top: 22px; padding-bottom: 18px; }
134
- .taskchef-brand-icon, .taskchef-brand-icon img { width: 36px; height: 36px; }
179
+ .taskchef-header { padding-inline: 8px; padding-block: 7px; }
180
+ .taskchef-header-layout { min-height: 32px; gap: 4px; }
181
+ .taskchef-header-copy { gap: 5px; }
182
+ .taskchef-brand-title { font-size: clamp(0.625rem, 2.7vw, 0.75rem); letter-spacing: 0.015em; line-height: 1.15; white-space: normal; }
183
+ .taskchef-version { margin-inline-start: 0.3rem; }
184
+ .taskchef-brand-icon, .taskchef-brand-icon img { width: 24px; height: 24px; }
185
+ .taskchef-header-actions { gap: 0; }
186
+ .taskchef-icon-button { width: 32px; min-width: 32px; height: 32px; min-height: 32px; }
187
+ .taskchef-connection { gap: 5px; margin-inline-start: 2px; }
135
188
  }
136
189
 
137
190
  @container (max-width: 27.5rem) {
@@ -49,10 +49,13 @@ export function taskStatusLabel(task) {
49
49
  return task.status === null ? "unresolved" : task.status.replaceAll("_", " ");
50
50
  }
51
51
 
52
+ export function hasLinkedCodexThread(task) {
53
+ return typeof task.threadId === "string" && CODEX_THREAD_ID_PATTERN.test(task.threadId);
54
+ }
55
+
52
56
  export function isArchiveTaskEligible(task) {
53
57
  return task.status !== "working"
54
- && typeof task.threadId === "string"
55
- && CODEX_THREAD_ID_PATTERN.test(task.threadId);
58
+ && hasLinkedCodexThread(task);
56
59
  }
57
60
 
58
61
  export function canArchiveTask(task) {
package/src/dashboard.js CHANGED
@@ -24,11 +24,15 @@ import {
24
24
  readConfig,
25
25
  readTask,
26
26
  } from "./workspace.js";
27
- import { DASHBOARD_SERVER_VERSION, TASKCHEF_VERSION } from "./version.js";
27
+ import {
28
+ DASHBOARD_SERVER_VERSION,
29
+ PINNED_CCUSAGE_VERSION,
30
+ TASKCHEF_VERSION,
31
+ } from "./version.js";
28
32
  import { taskGitHubProjection } from "./dashboard/github-links.js";
29
33
  import { CODEX_CHAT_ARCHIVE_ENABLED } from "./dashboard/state.js";
30
34
  import { createUsageTracker } from "./usage-tracker.js";
31
- import { readCcusageRuntimeInfo, readUsageStore, usageStorePath } from "./usage.js";
35
+ import { readUsageStore, usageStorePath } from "./usage.js";
32
36
  import { reportedWorkSummary } from "./reported-work.js";
33
37
  import {
34
38
  DASHBOARD_CONTROL_SESSION_PATH,
@@ -794,7 +798,6 @@ export async function createDashboardServer({
794
798
  usageSummaryMonitor = null,
795
799
  usagePreloadIntervalMs = 30_000,
796
800
  resolveRoles = resolveModelRoles,
797
- resolveUsageProvider = readCcusageRuntimeInfo,
798
801
  updateRole = updateModelRole,
799
802
  control = null,
800
803
  } = {}) {
@@ -861,8 +864,6 @@ export async function createDashboardServer({
861
864
  }
862
865
  const clients = new Set();
863
866
  const archiveRequests = new Set();
864
- let usageProviderPromise = null;
865
-
866
867
  const broadcast = (event, value) => {
867
868
  const payload = ssePayload(event, value);
868
869
  for (const client of clients) client.write(payload);
@@ -989,12 +990,11 @@ export async function createDashboardServer({
989
990
 
990
991
  if (url.pathname === "/api/settings" && method === "GET") {
991
992
  const profiles = [{ id: "personal", project: "Personal", ...await resolveRoles({ includeCatalog: true }) }];
992
- usageProviderPromise ??= Promise.resolve(resolveUsageProvider()).catch(() => ({
993
+ const usageProvider = {
993
994
  provider: "ccusage",
994
- status: "unavailable",
995
- version: null,
996
- }));
997
- sendJson(response, 200, { profiles, usageProvider: await usageProviderPromise });
995
+ version: PINNED_CCUSAGE_VERSION,
996
+ };
997
+ sendJson(response, 200, { profiles, usageProvider });
998
998
  return;
999
999
  }
1000
1000
 
package/src/version.js CHANGED
@@ -4,4 +4,5 @@ const require = createRequire(import.meta.url);
4
4
  const packageMetadata = require("../package.json");
5
5
 
6
6
  export const TASKCHEF_VERSION = packageMetadata.version;
7
+ export const PINNED_CCUSAGE_VERSION = packageMetadata.optionalDependencies?.ccusage ?? null;
7
8
  export const DASHBOARD_SERVER_VERSION = "4";