taskchef 8.2.0 → 8.2.2
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 +8 -6
- package/src/dashboard/react/components/RelativeTime.tsx +3 -1
- package/src/dashboard/react/components/TaskCardStats.test.tsx +37 -5
- package/src/dashboard/react/components/TaskCardStats.tsx +6 -5
- package/src/dashboard/react/components/TaskDetail.test.tsx +5 -2
- package/src/dashboard/react/components/UsagePanel.tsx +14 -23
- package/src/dashboard/react/presentation.test.tsx +55 -9
- package/src/dashboard/react/presentation.ts +19 -12
- package/src/dashboard/react/styles.css +3 -2
- 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">
|
|
@@ -57,6 +59,7 @@ export function ActivityTimeline({ highlightTurnRef, task }: { highlightTurnRef:
|
|
|
57
59
|
<RelativeTime
|
|
58
60
|
icon={<IconHistory aria-hidden size={14} />}
|
|
59
61
|
label="Turn update time"
|
|
62
|
+
tooltipLabel="Updated at"
|
|
60
63
|
value={presentation.updatedAt}
|
|
61
64
|
/>
|
|
62
65
|
</StatCell>
|
|
@@ -68,7 +71,7 @@ export function ActivityTimeline({ highlightTurnRef, task }: { highlightTurnRef:
|
|
|
68
71
|
accessibleLabel={usage.tokens.accessibleLabel}
|
|
69
72
|
animated={usage.animated}
|
|
70
73
|
icon={<IconStack2 aria-hidden size={14} />}
|
|
71
|
-
title=
|
|
74
|
+
title="Tokens"
|
|
72
75
|
value={usage.tokens.value}
|
|
73
76
|
/>
|
|
74
77
|
</StatCell>
|
|
@@ -77,13 +80,12 @@ export function ActivityTimeline({ highlightTurnRef, task }: { highlightTurnRef:
|
|
|
77
80
|
accessibleLabel={usage.cost.accessibleLabel}
|
|
78
81
|
animated={usage.animated}
|
|
79
82
|
icon={<IconCurrencyDollar aria-hidden size={14} />}
|
|
80
|
-
title=
|
|
83
|
+
title="Estimated cost"
|
|
81
84
|
value={usage.cost.value}
|
|
82
85
|
/>
|
|
83
86
|
</StatCell>
|
|
84
87
|
</StatsGrid>
|
|
85
88
|
{usage.note && <Text c="dimmed" size="xs">{usage.note}</Text>}
|
|
86
|
-
<Text c="dimmed" className="taskchef-mono" size="xs">Turn ref: {identity ?? "—"}</Text>
|
|
87
89
|
</Stack>
|
|
88
90
|
</Paper>
|
|
89
91
|
);
|
|
@@ -99,7 +101,7 @@ function TurnReportedWork({ turn }: { turn: TaskTurn }) {
|
|
|
99
101
|
<StaticStat
|
|
100
102
|
accessibleLabel={elapsed.accessibleLabel}
|
|
101
103
|
icon={<IconHourglass aria-hidden size={14} />}
|
|
102
|
-
title=
|
|
104
|
+
title="Duration"
|
|
103
105
|
value={elapsed.value}
|
|
104
106
|
/>
|
|
105
107
|
);
|
|
@@ -12,10 +12,12 @@ export function RelativeTimeProvider({ children, now }: { children: ReactNode; n
|
|
|
12
12
|
export function RelativeTime({
|
|
13
13
|
icon,
|
|
14
14
|
label,
|
|
15
|
+
tooltipLabel,
|
|
15
16
|
value,
|
|
16
17
|
}: {
|
|
17
18
|
icon?: ReactNode;
|
|
18
19
|
label: string;
|
|
20
|
+
tooltipLabel?: string;
|
|
19
21
|
value: string | null | undefined;
|
|
20
22
|
}) {
|
|
21
23
|
const [exact, setExact] = useState(false);
|
|
@@ -29,7 +31,7 @@ export function RelativeTime({
|
|
|
29
31
|
? "Show relative time"
|
|
30
32
|
: `Exact time: ${exactText}. Show exact time`;
|
|
31
33
|
return (
|
|
32
|
-
<Tooltip label={tooltip}>
|
|
34
|
+
<Tooltip events={{ focus: true, hover: true, touch: false }} label={tooltipLabel ?? tooltip}>
|
|
33
35
|
<UnstyledButton
|
|
34
36
|
aria-label={unavailable
|
|
35
37
|
? `${label}: unavailable.`
|
|
@@ -48,7 +48,7 @@ describe("task card stats", () => {
|
|
|
48
48
|
expect(screen.getByLabelText(/estimated cost unavailable/i)).toHaveTextContent("n/a");
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
test("shows
|
|
51
|
+
test("shows the requested estimated-cost label to keyboard users", () => {
|
|
52
52
|
vi.useFakeTimers();
|
|
53
53
|
renderStats(fixtureTask({
|
|
54
54
|
status: "completed",
|
|
@@ -57,7 +57,7 @@ describe("task card stats", () => {
|
|
|
57
57
|
const cost = screen.getByLabelText(/estimated cost \$12\.34/i);
|
|
58
58
|
fireEvent.focus(cost);
|
|
59
59
|
vi.advanceTimersByTime(150);
|
|
60
|
-
expect(screen.getByRole("tooltip")).toHaveTextContent("
|
|
60
|
+
expect(screen.getByRole("tooltip")).toHaveTextContent("Estimated cost");
|
|
61
61
|
vi.useRealTimers();
|
|
62
62
|
});
|
|
63
63
|
|
|
@@ -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", () => {
|
|
@@ -19,6 +19,7 @@ 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"
|
|
22
23
|
value={task.meaningfulUpdatedAt ?? task.updatedAt}
|
|
23
24
|
/>
|
|
24
25
|
</StatCell>
|
|
@@ -26,16 +27,16 @@ export function TaskCardStats({ task }: { task: Task }) {
|
|
|
26
27
|
<StaticStat
|
|
27
28
|
accessibleLabel={reportedWork.accessibleLabel}
|
|
28
29
|
icon={<IconHourglass aria-hidden size={14} />}
|
|
29
|
-
title=
|
|
30
|
+
title="Duration"
|
|
30
31
|
value={reportedWork.value}
|
|
31
32
|
/>
|
|
32
33
|
</StatCell>
|
|
33
34
|
<StatCell usageState={usage.kind}>
|
|
34
35
|
<StaticStat
|
|
35
36
|
accessibleLabel={usage.tokens.accessibleLabel}
|
|
36
|
-
animated={usage.animated}
|
|
37
|
+
animated={usage.tokens.animated}
|
|
37
38
|
icon={<IconStack2 aria-hidden size={14} />}
|
|
38
|
-
title=
|
|
39
|
+
title="Tokens"
|
|
39
40
|
value={usage.tokens.value}
|
|
40
41
|
/>
|
|
41
42
|
<Text className="taskchef-visually-hidden" component="span" role="status">
|
|
@@ -45,9 +46,9 @@ export function TaskCardStats({ task }: { task: Task }) {
|
|
|
45
46
|
<StatCell usageState={usage.kind}>
|
|
46
47
|
<StaticStat
|
|
47
48
|
accessibleLabel={usage.cost.accessibleLabel}
|
|
48
|
-
animated={usage.animated}
|
|
49
|
+
animated={usage.cost.animated}
|
|
49
50
|
icon={<IconCurrencyDollar aria-hidden size={14} />}
|
|
50
|
-
title=
|
|
51
|
+
title="Estimated cost"
|
|
51
52
|
value={usage.cost.value}
|
|
52
53
|
/>
|
|
53
54
|
</StatCell>
|
|
@@ -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("$");
|
|
@@ -13,12 +13,9 @@ export function UsagePanel({ task }: { task: Task }) {
|
|
|
13
13
|
const usage = usageView(task);
|
|
14
14
|
const reportedWork = taskReportedWorkView(task);
|
|
15
15
|
const ready = usage.kind === "ready" ? usage : null;
|
|
16
|
-
const cached = ready?.usage.cachedInputTokens ?? 0;
|
|
17
|
-
const input = ready?.usage.inputTokens ?? 0;
|
|
18
|
-
const cacheRatio = ready && input + cached > 0
|
|
19
|
-
? `${Math.round((cached / (input + cached)) * 100)}%`
|
|
20
|
-
: "n/a";
|
|
21
16
|
const models = Object.keys(ready?.usage.models ?? {}).join(", ");
|
|
17
|
+
const estimatedCost = ready?.usage.estimatedCostUsd;
|
|
18
|
+
const hasEstimatedCost = typeof estimatedCost === "number" && Number.isFinite(estimatedCost);
|
|
22
19
|
const stateValue = usage.kind === "pending"
|
|
23
20
|
? "Pending"
|
|
24
21
|
: usage.kind === "calculating"
|
|
@@ -36,8 +33,8 @@ export function UsagePanel({ task }: { task: Task }) {
|
|
|
36
33
|
accessibleValue: ready
|
|
37
34
|
? `${formatFullTokens(ready.usage.totalTokens)} tokens`
|
|
38
35
|
: usage.label,
|
|
39
|
-
animated: usage.kind === "pending" || usage.kind === "calculating",
|
|
40
|
-
label:
|
|
36
|
+
animated: usage.kind === "pending" || usage.kind === "calculating" || ready?.knownSoFar,
|
|
37
|
+
label: "Tokens",
|
|
41
38
|
title: ready ? `${formatFullTokens(ready.usage.totalTokens)} tokens` : usage.label,
|
|
42
39
|
value: ready ? formatCompactTokens(ready.usage.totalTokens) : stateValue,
|
|
43
40
|
},
|
|
@@ -49,7 +46,9 @@ export function UsagePanel({ task }: { task: Task }) {
|
|
|
49
46
|
: `Estimated cost ${accessibleState}${
|
|
50
47
|
usage.kind === "unavailable" ? `: ${usage.label}` : ""
|
|
51
48
|
}`,
|
|
52
|
-
animated: usage.kind === "pending"
|
|
49
|
+
animated: usage.kind === "pending"
|
|
50
|
+
|| usage.kind === "calculating"
|
|
51
|
+
|| (ready?.knownSoFar && hasEstimatedCost),
|
|
53
52
|
label: "Estimated cost",
|
|
54
53
|
title: ready?.usage.estimatedCostUsd == null
|
|
55
54
|
? "Estimated cost unavailable"
|
|
@@ -60,24 +59,16 @@ export function UsagePanel({ task }: { task: Task }) {
|
|
|
60
59
|
},
|
|
61
60
|
{
|
|
62
61
|
accessibleValue: reportedWork.accessibleLabel,
|
|
63
|
-
label: "
|
|
62
|
+
label: "Duration",
|
|
64
63
|
title: reportedWork.title,
|
|
65
64
|
value: reportedWork.value,
|
|
66
65
|
},
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
accessibleValue: cacheRatio === "n/a" ? "Cache ratio unavailable" : `Cache ratio ${cacheRatio}`,
|
|
76
|
-
label: "Cache ratio",
|
|
77
|
-
title: cacheRatio === "n/a" ? "Cache ratio unavailable because no input-token total is available." : undefined,
|
|
78
|
-
value: cacheRatio,
|
|
79
|
-
},
|
|
80
|
-
] : []),
|
|
66
|
+
{
|
|
67
|
+
accessibleValue: models || "Models unavailable",
|
|
68
|
+
label: "Models",
|
|
69
|
+
title: models || "Models unavailable",
|
|
70
|
+
value: models || "n/a",
|
|
71
|
+
},
|
|
81
72
|
];
|
|
82
73
|
return (
|
|
83
74
|
<Box aria-live="polite">
|
|
@@ -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: {
|
|
@@ -139,10 +154,11 @@ describe("token and working presentation", () => {
|
|
|
139
154
|
|
|
140
155
|
expect(screen.getByText("Tokens").nextSibling).toHaveTextContent("330");
|
|
141
156
|
expect(screen.getByText("Estimated cost").nextSibling).toHaveTextContent("$0.12");
|
|
142
|
-
expect(screen.getByText("
|
|
157
|
+
expect(screen.getByText("Duration").nextSibling).toHaveTextContent("18m 32s");
|
|
143
158
|
expect(screen.getByLabelText(/reported work 18m 32s.*wall-clock/i)).toBeVisible();
|
|
144
|
-
expect(screen.getByText("
|
|
145
|
-
expect(screen.
|
|
159
|
+
expect(screen.getByText("Models").nextSibling).toHaveTextContent("gpt-5.6-sol, gpt-5.6-luna");
|
|
160
|
+
expect(screen.queryByText("Cache ratio")).not.toBeInTheDocument();
|
|
161
|
+
expect(screen.getByText("Tokens").closest(".taskchef-metrics")?.children).toHaveLength(4);
|
|
146
162
|
expect(screen.queryByText(/API-equivalent/i)).not.toBeInTheDocument();
|
|
147
163
|
});
|
|
148
164
|
|
|
@@ -161,10 +177,38 @@ describe("token and working presentation", () => {
|
|
|
161
177
|
|
|
162
178
|
expect(screen.getByText("Estimated cost").nextSibling).toHaveTextContent("n/a");
|
|
163
179
|
expect(screen.getByLabelText("Estimated cost unavailable")).toBeVisible();
|
|
164
|
-
expect(screen.getByText("
|
|
165
|
-
expect(screen.getByLabelText("
|
|
166
|
-
expect(screen.
|
|
167
|
-
|
|
180
|
+
expect(screen.getByText("Models").nextSibling).toHaveTextContent("n/a");
|
|
181
|
+
expect(screen.getByLabelText("Models unavailable")).toBeVisible();
|
|
182
|
+
expect(screen.queryByText("Cache ratio")).not.toBeInTheDocument();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("shimmers known usage values during a running turn without changing card titles", () => {
|
|
186
|
+
const { container, rerender } = render(
|
|
187
|
+
<MantineProvider>
|
|
188
|
+
<UsagePanel task={fixtureTask({
|
|
189
|
+
usage: {
|
|
190
|
+
status: "available",
|
|
191
|
+
task: { totalTokens: 330, estimatedCostUsd: 0.12 },
|
|
192
|
+
},
|
|
193
|
+
})} />
|
|
194
|
+
</MantineProvider>,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
expect(screen.getByText("Tokens")).toBeVisible();
|
|
198
|
+
expect(screen.queryByText(/known so far/i)).not.toBeInTheDocument();
|
|
199
|
+
expect(container.querySelectorAll(".taskchef-shimmer")).toHaveLength(2);
|
|
200
|
+
|
|
201
|
+
rerender(
|
|
202
|
+
<MantineProvider>
|
|
203
|
+
<UsagePanel task={fixtureTask({
|
|
204
|
+
usage: {
|
|
205
|
+
status: "available",
|
|
206
|
+
task: { totalTokens: 330, estimatedCostUsd: null },
|
|
207
|
+
},
|
|
208
|
+
})} />
|
|
209
|
+
</MantineProvider>,
|
|
210
|
+
);
|
|
211
|
+
expect(container.querySelectorAll(".taskchef-shimmer")).toHaveLength(1);
|
|
168
212
|
});
|
|
169
213
|
|
|
170
214
|
test("keeps reported work visible across pending, calculating, and unavailable usage", () => {
|
|
@@ -185,7 +229,9 @@ describe("token and working presentation", () => {
|
|
|
185
229
|
expect(screen.getByText("Tokens").nextSibling).toHaveTextContent(expected);
|
|
186
230
|
expect(screen.getByText("Estimated cost").nextSibling).toHaveTextContent(expected);
|
|
187
231
|
expect(screen.getByLabelText(accessibleCost)).toBeVisible();
|
|
188
|
-
expect(screen.getByText("
|
|
232
|
+
expect(screen.getByText("Duration").nextSibling).toHaveTextContent("n/a");
|
|
233
|
+
expect(screen.getByText("Models").nextSibling).toHaveTextContent("n/a");
|
|
234
|
+
expect(screen.getByText("Tokens").closest(".taskchef-metrics")?.children).toHaveLength(4);
|
|
189
235
|
cleanup();
|
|
190
236
|
}
|
|
191
237
|
});
|
|
@@ -443,7 +489,7 @@ describe("reported wall-clock work presentation", () => {
|
|
|
443
489
|
reportedWork: { terminalTurns: 1, validTurns: 1, totalMilliseconds: 1_112_000 },
|
|
444
490
|
})).toMatchObject({ kind: "available", value: "18m 32s" });
|
|
445
491
|
render(<MantineProvider><UsagePanel task={compactTerminal} /></MantineProvider>);
|
|
446
|
-
expect(screen.getByText("
|
|
492
|
+
expect(screen.getByText("Duration").nextSibling).toHaveTextContent("n/a");
|
|
447
493
|
cleanup();
|
|
448
494
|
}
|
|
449
495
|
|
|
@@ -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 {
|
|
@@ -124,7 +125,7 @@ body {
|
|
|
124
125
|
.taskchef-toolbar .mantine-SegmentedControl-root { overflow-x: auto; }
|
|
125
126
|
.taskchef-notifications { right: 16px; bottom: 16px; width: min(330px, calc(100vw - 32px)); }
|
|
126
127
|
.taskchef-metadata { grid-template-columns: 1fr; }
|
|
127
|
-
.taskchef-metrics { grid-template-columns: repeat(
|
|
128
|
+
.taskchef-metrics { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
128
129
|
.taskchef-metadata dt { padding-bottom: 0; border-bottom: 0; }
|
|
129
130
|
}
|
|
130
131
|
|
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, {
|