taskchef 8.2.0 → 8.2.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.
- package/.codex-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/src/dashboard/dist/app.js +37 -37
- package/src/dashboard/dist/app.js.map +1 -1
- package/src/dashboard/dist/styles.css +1 -1
- package/src/dashboard/react/components/ActivityTimeline.tsx +4 -3
- package/src/dashboard/react/components/TaskCardStats.test.tsx +35 -3
- package/src/dashboard/react/components/TaskCardStats.tsx +2 -2
- package/src/dashboard/react/components/TaskDetail.test.tsx +5 -2
- package/src/dashboard/react/presentation.test.tsx +15 -0
- package/src/dashboard/react/presentation.ts +19 -12
- package/src/dashboard/react/styles.css +2 -1
- package/src/dashboard/time.js +7 -8
|
@@ -36,8 +36,10 @@ export function ActivityTimeline({ highlightTurnRef, task }: { highlightTurnRef:
|
|
|
36
36
|
withBorder
|
|
37
37
|
>
|
|
38
38
|
<Stack gap={7}>
|
|
39
|
-
<
|
|
40
|
-
|
|
39
|
+
<Box className="taskchef-turn-heading">
|
|
40
|
+
<Text c="dimmed" size="xs">{presentation.sourceLabel}</Text>
|
|
41
|
+
<StatusBadge status={presentation.status} />
|
|
42
|
+
</Box>
|
|
41
43
|
<Box>
|
|
42
44
|
<Text c="dimmed" className="taskchef-field-label" size="xs">Request</Text>
|
|
43
45
|
<Text className="taskchef-preserve-lines" size="sm">
|
|
@@ -83,7 +85,6 @@ export function ActivityTimeline({ highlightTurnRef, task }: { highlightTurnRef:
|
|
|
83
85
|
</StatCell>
|
|
84
86
|
</StatsGrid>
|
|
85
87
|
{usage.note && <Text c="dimmed" size="xs">{usage.note}</Text>}
|
|
86
|
-
<Text c="dimmed" className="taskchef-mono" size="xs">Turn ref: {identity ?? "—"}</Text>
|
|
87
88
|
</Stack>
|
|
88
89
|
</Paper>
|
|
89
90
|
);
|
|
@@ -83,7 +83,7 @@ describe("task card stats", () => {
|
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
test("retains known totals and marks both usage values as updating", () => {
|
|
86
|
-
renderStats(fixtureTask({
|
|
86
|
+
const { container } = renderStats(fixtureTask({
|
|
87
87
|
status: "working",
|
|
88
88
|
turnRef: "turn-two",
|
|
89
89
|
usage: {
|
|
@@ -92,8 +92,40 @@ describe("task card stats", () => {
|
|
|
92
92
|
task: { totalTokens: 1_324_567, estimatedCostUsd: 12.3449 },
|
|
93
93
|
},
|
|
94
94
|
}));
|
|
95
|
-
expect(screen.getByLabelText(/1,324,567 tokens; updating/i)).toHaveTextContent("1.32M tokens
|
|
96
|
-
expect(screen.getByLabelText(/estimated cost \$12\.34; updating/i)).toHaveTextContent("12.34
|
|
95
|
+
expect(screen.getByLabelText(/1,324,567 tokens; updating/i)).toHaveTextContent("1.32M tokens");
|
|
96
|
+
expect(screen.getByLabelText(/estimated cost \$12\.34; updating/i)).toHaveTextContent("12.34");
|
|
97
|
+
expect(container.querySelectorAll(".taskchef-shimmer")).toHaveLength(2);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("retains terminal totals while a newer usage generation is calculating", () => {
|
|
101
|
+
const { container } = renderStats(fixtureTask({
|
|
102
|
+
status: "completed",
|
|
103
|
+
turnRef: "turn-two",
|
|
104
|
+
usage: {
|
|
105
|
+
generationTurnRef: "turn-one",
|
|
106
|
+
status: "calculating",
|
|
107
|
+
task: { totalTokens: 1_324_567, estimatedCostUsd: 12.3449 },
|
|
108
|
+
},
|
|
109
|
+
}));
|
|
110
|
+
expect(screen.getByLabelText(/1,324,567 tokens; updating/i)).toHaveTextContent("1.32M tokens");
|
|
111
|
+
expect(screen.getByLabelText(/estimated cost \$12\.34; updating/i)).toHaveTextContent("12.34");
|
|
112
|
+
expect(container.querySelectorAll(".taskchef-shimmer")).toHaveLength(2);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("keeps unavailable cost static while retained tokens update", () => {
|
|
116
|
+
const { container } = renderStats(fixtureTask({
|
|
117
|
+
status: "working",
|
|
118
|
+
turnRef: "turn-two",
|
|
119
|
+
usage: {
|
|
120
|
+
generationTurnRef: "turn-one",
|
|
121
|
+
status: "calculating",
|
|
122
|
+
task: { totalTokens: 1_324_567, estimatedCostUsd: null },
|
|
123
|
+
},
|
|
124
|
+
}));
|
|
125
|
+
expect(screen.getByLabelText(/1,324,567 tokens; updating/i)).toHaveTextContent("1.32M tokens");
|
|
126
|
+
expect(screen.getByLabelText(/estimated cost unavailable/i)).toHaveTextContent("n/a");
|
|
127
|
+
expect(screen.getByLabelText(/estimated cost unavailable/i)).not.toHaveAccessibleName(/updating/i);
|
|
128
|
+
expect(container.querySelectorAll(".taskchef-shimmer")).toHaveLength(1);
|
|
97
129
|
});
|
|
98
130
|
|
|
99
131
|
test("places Open chat beneath the status badge and keeps callbacks working", () => {
|
|
@@ -33,7 +33,7 @@ export function TaskCardStats({ task }: { task: Task }) {
|
|
|
33
33
|
<StatCell usageState={usage.kind}>
|
|
34
34
|
<StaticStat
|
|
35
35
|
accessibleLabel={usage.tokens.accessibleLabel}
|
|
36
|
-
animated={usage.animated}
|
|
36
|
+
animated={usage.tokens.animated}
|
|
37
37
|
icon={<IconStack2 aria-hidden size={14} />}
|
|
38
38
|
title={usage.tokens.title}
|
|
39
39
|
value={usage.tokens.value}
|
|
@@ -45,7 +45,7 @@ export function TaskCardStats({ task }: { task: Task }) {
|
|
|
45
45
|
<StatCell usageState={usage.kind}>
|
|
46
46
|
<StaticStat
|
|
47
47
|
accessibleLabel={usage.cost.accessibleLabel}
|
|
48
|
-
animated={usage.animated}
|
|
48
|
+
animated={usage.cost.animated}
|
|
49
49
|
icon={<IconCurrencyDollar aria-hidden size={14} />}
|
|
50
50
|
title={usage.cost.title}
|
|
51
51
|
value={usage.cost.value}
|
|
@@ -63,7 +63,7 @@ test("covers terminal, active, and unavailable timeline metrics", () => {
|
|
|
63
63
|
}],
|
|
64
64
|
usage: {
|
|
65
65
|
status: "available",
|
|
66
|
-
turns: { "turn-one": { status: "available", totalTokens:
|
|
66
|
+
turns: { "turn-one": { status: "available", totalTokens: 1_324_567, estimatedCostUsd: 0.12 } },
|
|
67
67
|
},
|
|
68
68
|
});
|
|
69
69
|
const { container } = render(
|
|
@@ -71,6 +71,9 @@ test("covers terminal, active, and unavailable timeline metrics", () => {
|
|
|
71
71
|
);
|
|
72
72
|
const stats = container.querySelector(".taskchef-turn-stats") as HTMLElement;
|
|
73
73
|
expect(stats).not.toBeNull();
|
|
74
|
+
expect(container.querySelector(".taskchef-turn-heading .taskchef-status-badge"))
|
|
75
|
+
.toHaveTextContent("completed");
|
|
76
|
+
expect(screen.queryByText(/^Turn ref:/)).not.toBeInTheDocument();
|
|
74
77
|
const cells = [...stats.children] as HTMLElement[];
|
|
75
78
|
expect(cells).toHaveLength(4);
|
|
76
79
|
expect(within(cells[0]).getByLabelText(/^Turn update time:/i)).toBeVisible();
|
|
@@ -78,7 +81,7 @@ test("covers terminal, active, and unavailable timeline metrics", () => {
|
|
|
78
81
|
expect(within(cells[1]).getByLabelText(/completed turn reported wall-clock elapsed time/i))
|
|
79
82
|
.toHaveTextContent("18m 32s");
|
|
80
83
|
expect(cells[1].querySelector(".tabler-icon-hourglass")).not.toBeNull();
|
|
81
|
-
expect(within(cells[2]).getByLabelText("
|
|
84
|
+
expect(within(cells[2]).getByLabelText("1,324,567 tokens")).toHaveTextContent("1.32M");
|
|
82
85
|
expect(cells[2].querySelector(".tabler-icon-stack-2")).not.toBeNull();
|
|
83
86
|
expect(within(cells[3]).getByLabelText("Estimated cost $0.12")).toHaveTextContent("0.12");
|
|
84
87
|
expect(within(cells[3]).getByLabelText("Estimated cost $0.12")).not.toHaveTextContent("$");
|
|
@@ -61,6 +61,21 @@ describe("token and working presentation", () => {
|
|
|
61
61
|
if (view.kind === "ready") expect(view.knownSoFar).toBe(true);
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
+
test("keeps cumulative usage visible while a newer terminal generation is calculating", () => {
|
|
65
|
+
const view = usageView(fixtureTask({
|
|
66
|
+
status: "completed",
|
|
67
|
+
turnRef: "turn-two",
|
|
68
|
+
usage: {
|
|
69
|
+
generationTurnRef: "turn-one",
|
|
70
|
+
status: "calculating",
|
|
71
|
+
task: { totalTokens: 12345, estimatedCostUsd: 0.1234 },
|
|
72
|
+
},
|
|
73
|
+
}));
|
|
74
|
+
expect(view.kind).toBe("ready");
|
|
75
|
+
expect(view.label).toBe("12.35K tokens · Estimated cost $0.12 · known so far");
|
|
76
|
+
if (view.kind === "ready") expect(view.knownSoFar).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
64
79
|
test("distinguishes available, pending, calculating, and unavailable per-turn usage", () => {
|
|
65
80
|
const task = fixtureTask({
|
|
66
81
|
usage: {
|
|
@@ -151,6 +151,7 @@ export function usageView(task: Task): UsageView {
|
|
|
151
151
|
return { kind: "pending", label: "Token usage pending" };
|
|
152
152
|
}
|
|
153
153
|
if (usage?.status === "calculating" || priorGeneration) {
|
|
154
|
+
if (usage.task) return readyUsageView(usage.task, true);
|
|
154
155
|
return { kind: "calculating", label: "Calculating token usage" };
|
|
155
156
|
}
|
|
156
157
|
if (usage?.status === "available" && usage.task) {
|
|
@@ -200,7 +201,8 @@ export function turnUsageMetricsView(task: Task, turn: NonNullable<Task["turns"]
|
|
|
200
201
|
if (view.kind === "ready") {
|
|
201
202
|
const identity = turn.turnRef ?? turn.turnId;
|
|
202
203
|
const usage = identity ? task.usage?.turns?.[identity] : null;
|
|
203
|
-
const
|
|
204
|
+
const fullTokens = typeof usage?.totalTokens === "number" ? formatFullTokens(usage.totalTokens) : NOT_AVAILABLE;
|
|
205
|
+
const compactTokens = typeof usage?.totalTokens === "number" ? formatCompactTokens(usage.totalTokens) : NOT_AVAILABLE;
|
|
204
206
|
const estimatedCost = usage?.estimatedCostUsd;
|
|
205
207
|
const hasCost = typeof estimatedCost === "number" && Number.isFinite(estimatedCost);
|
|
206
208
|
const cost = hasCost ? formatEstimatedCostAmount(estimatedCost) : NOT_AVAILABLE;
|
|
@@ -214,7 +216,7 @@ export function turnUsageMetricsView(task: Task, turn: NonNullable<Task["turns"]
|
|
|
214
216
|
kind: view.kind,
|
|
215
217
|
note: null,
|
|
216
218
|
title: view.label,
|
|
217
|
-
tokens: { accessibleLabel: `${
|
|
219
|
+
tokens: { accessibleLabel: `${fullTokens} tokens`, value: compactTokens },
|
|
218
220
|
};
|
|
219
221
|
}
|
|
220
222
|
const value = view.kind === "pending"
|
|
@@ -247,12 +249,12 @@ export function usageStillCalculating(task: Task) {
|
|
|
247
249
|
|
|
248
250
|
export type ListUsageMetric = {
|
|
249
251
|
accessibleLabel: string;
|
|
252
|
+
animated: boolean;
|
|
250
253
|
title: string;
|
|
251
254
|
value: string;
|
|
252
255
|
};
|
|
253
256
|
|
|
254
257
|
export type ListUsageMetricsView = {
|
|
255
|
-
animated: boolean;
|
|
256
258
|
cost: ListUsageMetric;
|
|
257
259
|
kind: "pending" | "calculating" | "ready" | "unavailable";
|
|
258
260
|
tokens: ListUsageMetric;
|
|
@@ -263,15 +265,16 @@ export function listUsageMetricsView(task: Task): ListUsageMetricsView {
|
|
|
263
265
|
if (view.kind === "pending" || view.kind === "calculating") {
|
|
264
266
|
const value = view.kind === "pending" ? "Pending" : "Calculating";
|
|
265
267
|
return {
|
|
266
|
-
animated: true,
|
|
267
268
|
cost: {
|
|
268
269
|
accessibleLabel: `Estimated cost ${value.toLowerCase()}`,
|
|
270
|
+
animated: true,
|
|
269
271
|
title: view.label,
|
|
270
272
|
value,
|
|
271
273
|
},
|
|
272
274
|
kind: view.kind,
|
|
273
275
|
tokens: {
|
|
274
276
|
accessibleLabel: `Tokens ${value.toLowerCase()}`,
|
|
277
|
+
animated: true,
|
|
275
278
|
title: view.label,
|
|
276
279
|
value,
|
|
277
280
|
},
|
|
@@ -280,15 +283,16 @@ export function listUsageMetricsView(task: Task): ListUsageMetricsView {
|
|
|
280
283
|
if (view.kind === "unavailable") {
|
|
281
284
|
const reason = view.label === "Token usage unavailable" ? "" : `: ${view.label}`;
|
|
282
285
|
return {
|
|
283
|
-
animated: false,
|
|
284
286
|
cost: {
|
|
285
287
|
accessibleLabel: `Estimated cost unavailable${reason}`,
|
|
288
|
+
animated: false,
|
|
286
289
|
title: view.label,
|
|
287
290
|
value: NOT_AVAILABLE,
|
|
288
291
|
},
|
|
289
292
|
kind: "unavailable",
|
|
290
293
|
tokens: {
|
|
291
294
|
accessibleLabel: `Token usage unavailable${reason}`,
|
|
295
|
+
animated: false,
|
|
292
296
|
title: view.label,
|
|
293
297
|
value: `${NOT_AVAILABLE} tokens`,
|
|
294
298
|
},
|
|
@@ -300,26 +304,29 @@ export function listUsageMetricsView(task: Task): ListUsageMetricsView {
|
|
|
300
304
|
const hasCost = typeof estimatedCost === "number" && Number.isFinite(estimatedCost);
|
|
301
305
|
const formattedCost = hasCost ? formatEstimatedCost(estimatedCost) : null;
|
|
302
306
|
const formattedCostAmount = hasCost ? formatEstimatedCostAmount(estimatedCost) : null;
|
|
303
|
-
const
|
|
307
|
+
const tooltipQualifier = view.knownSoFar ? " · Updating…" : "";
|
|
304
308
|
const accessibleQualifier = view.knownSoFar ? "; updating" : "";
|
|
309
|
+
const costTooltipQualifier = hasCost ? tooltipQualifier : "";
|
|
310
|
+
const costAccessibleQualifier = hasCost ? accessibleQualifier : "";
|
|
305
311
|
const freshness = view.usage.sourceUpdatedAt ?? view.usage.sampledAt ?? task.usage?.updatedAt ?? null;
|
|
306
312
|
const freshnessLabel = freshness && !Number.isNaN(Date.parse(freshness))
|
|
307
313
|
? ` Cached usage updated ${new Date(freshness).toLocaleString()}.`
|
|
308
314
|
: "";
|
|
309
315
|
return {
|
|
310
|
-
animated: false,
|
|
311
316
|
cost: {
|
|
312
|
-
accessibleLabel: `Estimated cost ${formattedCost ?? "unavailable"}${
|
|
317
|
+
accessibleLabel: `Estimated cost ${formattedCost ?? "unavailable"}${costAccessibleQualifier}.${freshnessLabel}`,
|
|
318
|
+
animated: view.knownSoFar && hasCost,
|
|
313
319
|
title: `${formattedCost === null
|
|
314
320
|
? "Estimated cost unavailable"
|
|
315
|
-
: `Unrounded estimate: $${view.usage.estimatedCostUsd}`}${
|
|
316
|
-
value:
|
|
321
|
+
: `Unrounded estimate: $${view.usage.estimatedCostUsd}`}${costTooltipQualifier}.${freshnessLabel}`,
|
|
322
|
+
value: formattedCostAmount ?? NOT_AVAILABLE,
|
|
317
323
|
},
|
|
318
324
|
kind: "ready",
|
|
319
325
|
tokens: {
|
|
320
326
|
accessibleLabel: `${fullTokens} tokens${accessibleQualifier}.${freshnessLabel}`,
|
|
321
|
-
|
|
322
|
-
|
|
327
|
+
animated: view.knownSoFar,
|
|
328
|
+
title: `${fullTokens} tokens${tooltipQualifier}.${freshnessLabel}`,
|
|
329
|
+
value: `${compactTokens} tokens`,
|
|
323
330
|
},
|
|
324
331
|
};
|
|
325
332
|
}
|
|
@@ -34,7 +34,8 @@ body {
|
|
|
34
34
|
.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
35
|
.taskchef-task-row:hover { border-color: light-dark(var(--mantine-color-teal-4), var(--mantine-color-teal-8)); }
|
|
36
36
|
.taskchef-task-main { min-width: 0; flex: 1; }
|
|
37
|
-
.taskchef-card-heading
|
|
37
|
+
.taskchef-card-heading,
|
|
38
|
+
.taskchef-turn-heading { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: start; gap: var(--mantine-spacing-sm); }
|
|
38
39
|
.taskchef-card-metadata { width: max-content; min-width: 0; max-width: min(25rem, 45cqw); }
|
|
39
40
|
.taskchef-status-badge { flex: none; font-size: var(--mantine-font-size-xs); }
|
|
40
41
|
.taskchef-title-button {
|
package/src/dashboard/time.js
CHANGED
|
@@ -28,12 +28,10 @@ export function formatExactTime(value, { locale, timeZone } = {}) {
|
|
|
28
28
|
|
|
29
29
|
function hourPhrase(milliseconds, direction) {
|
|
30
30
|
const hours = Math.floor(milliseconds / HOUR_MS);
|
|
31
|
-
const minutes = Math.floor((milliseconds % HOUR_MS) / MINUTE_MS);
|
|
32
31
|
const hourText = hours === 1 ? "1 hour" : `${hours} hours`;
|
|
33
|
-
const detail = hours < 6 && minutes > 0 ? ` ${minutes} minutes` : "";
|
|
34
32
|
return direction === "future"
|
|
35
|
-
? `in ${hourText}
|
|
36
|
-
: `${hourText}
|
|
33
|
+
? `in ${hourText}`
|
|
34
|
+
: `${hourText} ago`;
|
|
37
35
|
}
|
|
38
36
|
|
|
39
37
|
export function formatRelativeTime(value, {
|
|
@@ -63,10 +61,11 @@ export function formatRelativeTime(value, {
|
|
|
63
61
|
: Math.floor(elapsed / DAY_MS);
|
|
64
62
|
return future ? `in ${days} days` : `${days} days ago`;
|
|
65
63
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
64
|
+
const months = future
|
|
65
|
+
? Math.ceil(elapsed / (RELATIVE_DATE_LIMIT_DAYS * DAY_MS))
|
|
66
|
+
: Math.floor(elapsed / (RELATIVE_DATE_LIMIT_DAYS * DAY_MS));
|
|
67
|
+
const monthText = months === 1 ? "1 month" : `${months} months`;
|
|
68
|
+
return future ? `in ${monthText}` : `${monthText} ago`;
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
export function timestampPresentation(value, {
|