taskchef 7.25.2 → 7.26.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/README.md +7 -0
- package/docs/dashboard-lifecycle.md +23 -0
- package/docs/images/reported-work-duration-desktop.jpg +0 -0
- package/docs/images/reported-work-duration-mobile.jpg +0 -0
- package/package.json +3 -1
- package/src/dashboard/dist/app.js +41 -46
- package/src/dashboard/dist/app.js.map +1 -1
- package/src/dashboard/dist/styles.css +1 -1
- package/src/dashboard/react/App.test.tsx +112 -0
- package/src/dashboard/react/components/ActivityTimeline.tsx +76 -13
- package/src/dashboard/react/components/TaskDetail.test.tsx +97 -2
- package/src/dashboard/react/components/UsagePanel.tsx +64 -46
- package/src/dashboard/react/presentation.test.tsx +231 -3
- package/src/dashboard/react/presentation.ts +181 -1
- package/src/dashboard/react/styles.css +8 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { act, cleanup, render, screen } from "@testing-library/react";
|
|
2
|
+
import type { ElementType, ReactNode } from "react";
|
|
2
3
|
import { afterEach, beforeEach, expect, test, vi } from "vitest";
|
|
3
4
|
import { DashboardApp } from "./App";
|
|
4
5
|
import { fixtureTask } from "./fixtures";
|
|
@@ -16,6 +17,117 @@ vi.mock("./api", () => apiMocks);
|
|
|
16
17
|
vi.mock("./components/NotificationCenter", () => ({ NotificationCenter: () => null }));
|
|
17
18
|
vi.mock("./components/TaskCard", () => ({ TaskCard: () => null }));
|
|
18
19
|
vi.mock("./components/TaskDetail", () => ({ TaskDetail: () => null }));
|
|
20
|
+
vi.mock("@tabler/icons-react", async () => {
|
|
21
|
+
const { createElement } = await vi.importActual<typeof import("react")>("react");
|
|
22
|
+
const Icon = (props: Record<string, unknown>) => createElement("svg", props);
|
|
23
|
+
return {
|
|
24
|
+
IconAlertTriangle: Icon,
|
|
25
|
+
IconCircleFilled: Icon,
|
|
26
|
+
IconMoon: Icon,
|
|
27
|
+
IconSun: Icon,
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
vi.mock("@mantine/core", async () => {
|
|
31
|
+
const { createElement } = await vi.importActual<typeof import("react")>("react");
|
|
32
|
+
const primitive = (tag: string) => ({
|
|
33
|
+
align: _align,
|
|
34
|
+
c: _color,
|
|
35
|
+
children,
|
|
36
|
+
component,
|
|
37
|
+
fw: _fontWeight,
|
|
38
|
+
gap: _gap,
|
|
39
|
+
justify: _justify,
|
|
40
|
+
mb: _marginBottom,
|
|
41
|
+
mt: _marginTop,
|
|
42
|
+
p: _padding,
|
|
43
|
+
pb: _paddingBottom,
|
|
44
|
+
radius: _radius,
|
|
45
|
+
size: _size,
|
|
46
|
+
ta: _textAlign,
|
|
47
|
+
tt: _textTransform,
|
|
48
|
+
withBorder: _withBorder,
|
|
49
|
+
wrap: _wrap,
|
|
50
|
+
...props
|
|
51
|
+
}: Record<string, unknown> & { children?: ReactNode; component?: ElementType }) => (
|
|
52
|
+
createElement(component ?? tag, props, children)
|
|
53
|
+
);
|
|
54
|
+
const AppShell = Object.assign(primitive("div"), { Main: primitive("main") });
|
|
55
|
+
const Select = ({
|
|
56
|
+
"aria-label": ariaLabel,
|
|
57
|
+
data,
|
|
58
|
+
label,
|
|
59
|
+
onChange,
|
|
60
|
+
value,
|
|
61
|
+
}: {
|
|
62
|
+
"aria-label": string;
|
|
63
|
+
data: Array<string | { label: string; value: string }>;
|
|
64
|
+
label: string;
|
|
65
|
+
onChange: (value: string | null) => void;
|
|
66
|
+
value: string;
|
|
67
|
+
}) => {
|
|
68
|
+
const options = data.map((item) => typeof item === "string"
|
|
69
|
+
? { label: item, value: item }
|
|
70
|
+
: item);
|
|
71
|
+
const selected = options.find((item) => item.value === value)?.label ?? value;
|
|
72
|
+
return createElement("label", null, label, createElement(
|
|
73
|
+
"select",
|
|
74
|
+
{
|
|
75
|
+
"aria-label": ariaLabel,
|
|
76
|
+
onChange: (event: { currentTarget: { value: string } }) => {
|
|
77
|
+
const next = options.find((item) => item.label === event.currentTarget.value);
|
|
78
|
+
onChange(next?.value ?? null);
|
|
79
|
+
},
|
|
80
|
+
value: selected,
|
|
81
|
+
},
|
|
82
|
+
options.map((item) => createElement("option", { key: item.value, value: item.label }, item.label)),
|
|
83
|
+
));
|
|
84
|
+
};
|
|
85
|
+
const SegmentedControl = ({
|
|
86
|
+
"aria-label": ariaLabel,
|
|
87
|
+
data,
|
|
88
|
+
onChange,
|
|
89
|
+
value,
|
|
90
|
+
}: {
|
|
91
|
+
"aria-label": string;
|
|
92
|
+
data: Array<{ label: string; value: string }>;
|
|
93
|
+
onChange: (value: string) => void;
|
|
94
|
+
value: string;
|
|
95
|
+
}) => createElement(
|
|
96
|
+
"div",
|
|
97
|
+
{ "aria-label": ariaLabel, role: "radiogroup" },
|
|
98
|
+
data.map((item) => createElement(
|
|
99
|
+
"label",
|
|
100
|
+
{ key: item.value },
|
|
101
|
+
createElement("input", {
|
|
102
|
+
checked: item.value === value,
|
|
103
|
+
name: ariaLabel,
|
|
104
|
+
onChange: () => onChange(item.value),
|
|
105
|
+
type: "radio",
|
|
106
|
+
}),
|
|
107
|
+
item.label,
|
|
108
|
+
)),
|
|
109
|
+
);
|
|
110
|
+
return {
|
|
111
|
+
ActionIcon: primitive("button"),
|
|
112
|
+
Alert: primitive("div"),
|
|
113
|
+
AppShell,
|
|
114
|
+
Box: primitive("div"),
|
|
115
|
+
CloseButton: primitive("button"),
|
|
116
|
+
Container: primitive("div"),
|
|
117
|
+
Group: primitive("div"),
|
|
118
|
+
MantineProvider: ({ children }: { children?: ReactNode }) => createElement("div", null, children),
|
|
119
|
+
Paper: primitive("div"),
|
|
120
|
+
SegmentedControl,
|
|
121
|
+
Select,
|
|
122
|
+
Stack: primitive("div"),
|
|
123
|
+
Text: primitive("p"),
|
|
124
|
+
Title: primitive("h1"),
|
|
125
|
+
Tooltip: primitive("div"),
|
|
126
|
+
createTheme: (theme: unknown) => theme,
|
|
127
|
+
useComputedColorScheme: () => "light",
|
|
128
|
+
useMantineColorScheme: () => ({ setColorScheme: vi.fn() }),
|
|
129
|
+
};
|
|
130
|
+
});
|
|
19
131
|
|
|
20
132
|
afterEach(cleanup);
|
|
21
133
|
beforeEach(() => {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Box, Paper, Stack, Text } from "@mantine/core";
|
|
2
2
|
import { mergeProjectedTurns, turnPresentation } from "../../state.js";
|
|
3
|
-
import {
|
|
4
|
-
import type { Task } from "../types";
|
|
3
|
+
import { turnReportedWorkView, turnUsageMetricsView } from "../presentation";
|
|
4
|
+
import type { Task, TaskTurn } from "../types";
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
5
6
|
import { RelativeTime } from "./RelativeTime";
|
|
6
7
|
import { LinkedText } from "./LinkedText";
|
|
7
8
|
import { ShimmerText } from "./ShimmerText";
|
|
@@ -16,7 +17,7 @@ export function ActivityTimeline({ highlightTurnRef, task }: { highlightTurnRef:
|
|
|
16
17
|
const presentation = turnPresentation(turn);
|
|
17
18
|
const identity = turn.turnRef ?? turn.turnId ?? `turn-${index}`;
|
|
18
19
|
const highlighted = identity === highlightTurnRef;
|
|
19
|
-
const usage =
|
|
20
|
+
const usage = turnUsageMetricsView(task, turn);
|
|
20
21
|
return (
|
|
21
22
|
<Paper
|
|
22
23
|
className={`taskchef-turn${highlighted ? " taskchef-turn-highlighted" : ""}`}
|
|
@@ -30,7 +31,10 @@ export function ActivityTimeline({ highlightTurnRef, task }: { highlightTurnRef:
|
|
|
30
31
|
<Stack gap={7}>
|
|
31
32
|
<Box className="taskchef-turn-heading">
|
|
32
33
|
<StatusBadge status={presentation.status} />
|
|
33
|
-
<
|
|
34
|
+
<Box className="taskchef-turn-timing">
|
|
35
|
+
<RelativeTime label="Turn update time" value={presentation.updatedAt} />
|
|
36
|
+
<TurnReportedWork turn={turn} />
|
|
37
|
+
</Box>
|
|
34
38
|
</Box>
|
|
35
39
|
{presentation.sourceLabel && <Text c="dimmed" size="xs">{presentation.sourceLabel}</Text>}
|
|
36
40
|
<Box>
|
|
@@ -47,15 +51,23 @@ export function ActivityTimeline({ highlightTurnRef, task }: { highlightTurnRef:
|
|
|
47
51
|
: <LinkedText task={task} text={presentation.summary} />}
|
|
48
52
|
</Text>
|
|
49
53
|
</Box>
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
<Box className="taskchef-turn-metrics">
|
|
55
|
+
<TurnMetric
|
|
56
|
+
accessibleLabel={usage.tokens.accessibleLabel}
|
|
57
|
+
animated={usage.animated}
|
|
58
|
+
label="Tokens"
|
|
59
|
+
title={usage.title}
|
|
60
|
+
value={usage.tokens.value}
|
|
61
|
+
/>
|
|
62
|
+
<TurnMetric
|
|
63
|
+
accessibleLabel={usage.cost.accessibleLabel}
|
|
64
|
+
animated={usage.animated}
|
|
65
|
+
label="Estimated cost"
|
|
66
|
+
title={usage.title}
|
|
67
|
+
value={usage.cost.value}
|
|
68
|
+
/>
|
|
69
|
+
</Box>
|
|
70
|
+
{usage.note && <Text c="dimmed" size="xs">{usage.note}</Text>}
|
|
59
71
|
<Text c="dimmed" className="taskchef-mono" size="xs">Turn ref: {identity ?? "—"}</Text>
|
|
60
72
|
</Stack>
|
|
61
73
|
</Paper>
|
|
@@ -64,3 +76,54 @@ export function ActivityTimeline({ highlightTurnRef, task }: { highlightTurnRef:
|
|
|
64
76
|
</Stack>
|
|
65
77
|
);
|
|
66
78
|
}
|
|
79
|
+
|
|
80
|
+
function TurnReportedWork({ turn }: { turn: TaskTurn }) {
|
|
81
|
+
const now = useLiveNow(turn.result === null);
|
|
82
|
+
const elapsed = turnReportedWorkView(turn, now);
|
|
83
|
+
return (
|
|
84
|
+
<Text
|
|
85
|
+
aria-label={elapsed.accessibleLabel}
|
|
86
|
+
c="dimmed"
|
|
87
|
+
className="taskchef-turn-duration"
|
|
88
|
+
size="xs"
|
|
89
|
+
title={elapsed.title}
|
|
90
|
+
>
|
|
91
|
+
<span className="taskchef-field-label">{elapsed.label}</span>
|
|
92
|
+
<span>{elapsed.value}</span>
|
|
93
|
+
</Text>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function TurnMetric({
|
|
98
|
+
accessibleLabel,
|
|
99
|
+
animated = false,
|
|
100
|
+
label,
|
|
101
|
+
title,
|
|
102
|
+
value,
|
|
103
|
+
}: {
|
|
104
|
+
accessibleLabel?: string;
|
|
105
|
+
animated?: boolean;
|
|
106
|
+
label: string;
|
|
107
|
+
title?: string;
|
|
108
|
+
value: string;
|
|
109
|
+
}) {
|
|
110
|
+
return (
|
|
111
|
+
<Box className="taskchef-turn-metric" title={title}>
|
|
112
|
+
<Text c="dimmed" className="taskchef-field-label" size="xs">{label}</Text>
|
|
113
|
+
<Text aria-label={accessibleLabel} c="dimmed" size="xs">
|
|
114
|
+
{animated ? <ShimmerText>{value}</ShimmerText> : value}
|
|
115
|
+
</Text>
|
|
116
|
+
</Box>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function useLiveNow(active: boolean) {
|
|
121
|
+
const [now, setNow] = useState(() => Date.now());
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (!active) return undefined;
|
|
124
|
+
setNow(Date.now());
|
|
125
|
+
const interval = window.setInterval(() => setNow(Date.now()), 1_000);
|
|
126
|
+
return () => window.clearInterval(interval);
|
|
127
|
+
}, [active]);
|
|
128
|
+
return now;
|
|
129
|
+
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { MantineProvider } from "@mantine/core";
|
|
2
|
-
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import { act, cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { afterEach, expect, test, vi } from "vitest";
|
|
5
|
+
import { fixtureTask } from "../fixtures";
|
|
6
|
+
import { ActivityTimeline } from "./ActivityTimeline";
|
|
5
7
|
import { ManualTransitionConfirmation, type TerminalStatus } from "./TaskDetail";
|
|
6
8
|
|
|
7
|
-
afterEach(
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
cleanup();
|
|
11
|
+
vi.restoreAllMocks();
|
|
12
|
+
});
|
|
8
13
|
|
|
9
14
|
test("clears a manual-transition confirmation only after success", async () => {
|
|
10
15
|
const onTransition = vi.fn().mockResolvedValue({ ok: true });
|
|
@@ -44,6 +49,96 @@ test("reuses an action ID for retryable failures and rotates it after a stale ta
|
|
|
44
49
|
expect(onTransition.mock.calls[2][1]).not.toBe(firstActionId);
|
|
45
50
|
});
|
|
46
51
|
|
|
52
|
+
test("covers terminal, active, and unavailable timeline metrics", () => {
|
|
53
|
+
{
|
|
54
|
+
const task = fixtureTask({
|
|
55
|
+
status: "completed",
|
|
56
|
+
turns: [{
|
|
57
|
+
...fixtureTask().turns![0],
|
|
58
|
+
result: {
|
|
59
|
+
status: "completed",
|
|
60
|
+
summary: "Done.",
|
|
61
|
+
updatedAt: "2026-08-30T08:18:32.000Z",
|
|
62
|
+
},
|
|
63
|
+
}],
|
|
64
|
+
usage: {
|
|
65
|
+
status: "available",
|
|
66
|
+
turns: { "turn-one": { status: "available", totalTokens: 330, estimatedCostUsd: 0.12 } },
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
const { container } = render(
|
|
70
|
+
<MantineProvider><ActivityTimeline highlightTurnRef={null} task={task} /></MantineProvider>,
|
|
71
|
+
);
|
|
72
|
+
const metrics = container.querySelector(".taskchef-turn-metrics") as HTMLElement;
|
|
73
|
+
const timing = container.querySelector(".taskchef-turn-timing") as HTMLElement;
|
|
74
|
+
expect(metrics).not.toBeNull();
|
|
75
|
+
expect(timing).not.toBeNull();
|
|
76
|
+
expect(within(metrics).getByText("Tokens").nextSibling).toHaveTextContent("330");
|
|
77
|
+
expect(within(metrics).getByText("Estimated cost").nextSibling).toHaveTextContent("$0.12");
|
|
78
|
+
expect(within(metrics).queryByText("Elapsed")).not.toBeInTheDocument();
|
|
79
|
+
expect(within(timing).getByLabelText(/^Turn update time:/i)).toBeVisible();
|
|
80
|
+
expect(within(timing).getByLabelText(/completed turn reported wall-clock elapsed time/i))
|
|
81
|
+
.toHaveTextContent("Elapsed18m 32s");
|
|
82
|
+
cleanup();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
{
|
|
86
|
+
let now = Date.parse("2026-08-30T08:18:32.000Z");
|
|
87
|
+
let tick: (() => void) | undefined;
|
|
88
|
+
vi.spyOn(Date, "now").mockImplementation(() => now);
|
|
89
|
+
vi.spyOn(window, "setInterval").mockImplementation(((handler: TimerHandler) => {
|
|
90
|
+
tick = handler as () => void;
|
|
91
|
+
return 1;
|
|
92
|
+
}) as typeof window.setInterval);
|
|
93
|
+
vi.spyOn(window, "clearInterval").mockImplementation(() => undefined);
|
|
94
|
+
const task = fixtureTask();
|
|
95
|
+
render(<MantineProvider><ActivityTimeline highlightTurnRef={null} task={task} /></MantineProvider>);
|
|
96
|
+
expect(screen.getByLabelText(/current turn reported wall-clock elapsed time/i))
|
|
97
|
+
.toHaveTextContent("Elapsed so far18m 32s");
|
|
98
|
+
expect(screen.getByText("Tokens").nextSibling).toHaveTextContent("Pending");
|
|
99
|
+
expect(screen.getByText("Estimated cost").nextSibling).toHaveTextContent("Pending");
|
|
100
|
+
|
|
101
|
+
act(() => {
|
|
102
|
+
now += 2_000;
|
|
103
|
+
tick?.();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(screen.getByLabelText(/current turn reported wall-clock elapsed time/i))
|
|
107
|
+
.toHaveTextContent("Elapsed so far18m 34s");
|
|
108
|
+
cleanup();
|
|
109
|
+
vi.restoreAllMocks();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
{
|
|
113
|
+
const task = fixtureTask({
|
|
114
|
+
status: "completed",
|
|
115
|
+
turns: [{
|
|
116
|
+
...fixtureTask().turns![0],
|
|
117
|
+
startedAt: "2026-08-30T08:18:33.000Z",
|
|
118
|
+
result: {
|
|
119
|
+
status: "completed",
|
|
120
|
+
summary: "Done.",
|
|
121
|
+
updatedAt: "2026-08-30T08:18:32.000Z",
|
|
122
|
+
},
|
|
123
|
+
}],
|
|
124
|
+
usage: {
|
|
125
|
+
status: "unavailable",
|
|
126
|
+
turns: {
|
|
127
|
+
"turn-one": {
|
|
128
|
+
status: "unavailable",
|
|
129
|
+
reason: "Manual dashboard turns do not have usage boundaries.",
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
render(<MantineProvider><ActivityTimeline highlightTurnRef={null} task={task} /></MantineProvider>);
|
|
135
|
+
expect(screen.getByLabelText(/elapsed unavailable.*reported wall-clock/i))
|
|
136
|
+
.toHaveTextContent("ElapsedUnavailable");
|
|
137
|
+
expect(screen.getByText("Manual dashboard turns do not have usage boundaries.")).toBeVisible();
|
|
138
|
+
expect(screen.getByLabelText(/tokens unavailable: manual dashboard turns/i)).toBeVisible();
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
47
142
|
function ConfirmationHarness({ onTransition }: { onTransition: (status: TerminalStatus, actionId: string) => Promise<{ ok: boolean; rotateActionId?: boolean }> }) {
|
|
48
143
|
const [opened, setOpened] = useState(true);
|
|
49
144
|
return (
|
|
@@ -1,73 +1,91 @@
|
|
|
1
|
-
import { Box,
|
|
2
|
-
import { IconCoins } from "@tabler/icons-react";
|
|
1
|
+
import { Box, Paper, Text } from "@mantine/core";
|
|
3
2
|
import type { Task } from "../types";
|
|
4
3
|
import {
|
|
5
4
|
formatCompactTokens,
|
|
6
5
|
formatEstimatedCost,
|
|
7
6
|
formatFullTokens,
|
|
7
|
+
taskReportedWorkView,
|
|
8
8
|
usageView,
|
|
9
9
|
} from "../presentation";
|
|
10
10
|
import { ShimmerText } from "./ShimmerText";
|
|
11
11
|
|
|
12
12
|
export function UsagePanel({ task }: { task: Task }) {
|
|
13
13
|
const usage = usageView(task);
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
const cached = usage.usage.cachedInputTokens ?? 0;
|
|
28
|
-
const input = usage.usage.inputTokens ?? 0;
|
|
29
|
-
const cacheRatio = input + cached > 0 ? `${Math.round((cached / (input + cached)) * 100)}%` : "—";
|
|
14
|
+
const reportedWork = taskReportedWorkView(task);
|
|
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
|
+
: "—";
|
|
21
|
+
const stateValue = usage.kind === "pending"
|
|
22
|
+
? "Pending"
|
|
23
|
+
: usage.kind === "calculating"
|
|
24
|
+
? "Calculating"
|
|
25
|
+
: "Unavailable";
|
|
30
26
|
const values: Array<{
|
|
31
27
|
accessibleValue?: string;
|
|
28
|
+
animated?: boolean;
|
|
32
29
|
label: string;
|
|
33
30
|
title?: string;
|
|
34
31
|
value: string;
|
|
35
32
|
}> = [
|
|
36
33
|
{
|
|
37
|
-
accessibleValue:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
accessibleValue: ready
|
|
35
|
+
? `${formatFullTokens(ready.usage.totalTokens)} tokens`
|
|
36
|
+
: usage.label,
|
|
37
|
+
animated: usage.kind === "pending" || usage.kind === "calculating",
|
|
38
|
+
label: ready?.knownSoFar ? "Tokens · known so far" : "Tokens",
|
|
39
|
+
title: ready ? `${formatFullTokens(ready.usage.totalTokens)} tokens` : usage.label,
|
|
40
|
+
value: ready ? formatCompactTokens(ready.usage.totalTokens) : stateValue,
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
|
-
accessibleValue:
|
|
44
|
-
?
|
|
45
|
-
|
|
43
|
+
accessibleValue: ready
|
|
44
|
+
? ready.usage.estimatedCostUsd == null
|
|
45
|
+
? "Estimated cost unavailable"
|
|
46
|
+
: `Estimated cost ${formatEstimatedCost(ready.usage.estimatedCostUsd)}`
|
|
47
|
+
: `Estimated cost ${stateValue.toLowerCase()}${
|
|
48
|
+
usage.kind === "unavailable" ? `: ${usage.label}` : ""
|
|
49
|
+
}`,
|
|
50
|
+
animated: usage.kind === "pending" || usage.kind === "calculating",
|
|
46
51
|
label: "Estimated cost",
|
|
47
|
-
title: usage.
|
|
48
|
-
?
|
|
49
|
-
: `Unrounded estimate: $${
|
|
50
|
-
value: formatEstimatedCost(
|
|
52
|
+
title: ready?.usage.estimatedCostUsd == null
|
|
53
|
+
? usage.label
|
|
54
|
+
: `Unrounded estimate: $${ready.usage.estimatedCostUsd}`,
|
|
55
|
+
value: ready ? formatEstimatedCost(ready.usage.estimatedCostUsd) : stateValue,
|
|
51
56
|
},
|
|
52
|
-
{
|
|
53
|
-
|
|
57
|
+
{
|
|
58
|
+
accessibleValue: reportedWork.accessibleLabel,
|
|
59
|
+
label: "Total reported work",
|
|
60
|
+
title: reportedWork.title,
|
|
61
|
+
value: reportedWork.value,
|
|
62
|
+
},
|
|
63
|
+
...(ready ? [
|
|
64
|
+
{ label: "Model", value: Object.keys(ready.usage.models ?? {}).join(", ") || "—" },
|
|
65
|
+
{ label: "Cache ratio", value: cacheRatio },
|
|
66
|
+
] : []),
|
|
54
67
|
];
|
|
55
68
|
return (
|
|
56
|
-
<Box aria-live="polite"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
<Box aria-live="polite">
|
|
70
|
+
<Box className="taskchef-metrics">
|
|
71
|
+
{values.map(({ accessibleValue, animated, label, title, value }) => (
|
|
72
|
+
<Paper className="taskchef-metric" key={label} p="sm" radius="md" withBorder>
|
|
73
|
+
<Text c="dimmed" size="xs">{label}</Text>
|
|
74
|
+
<Text
|
|
75
|
+
aria-label={accessibleValue}
|
|
76
|
+
className="taskchef-metric-value"
|
|
77
|
+
fw={650}
|
|
78
|
+
size="sm"
|
|
79
|
+
title={title}
|
|
80
|
+
>
|
|
81
|
+
{animated ? <ShimmerText>{value}</ShimmerText> : value}
|
|
82
|
+
</Text>
|
|
83
|
+
</Paper>
|
|
84
|
+
))}
|
|
85
|
+
</Box>
|
|
86
|
+
{usage.kind === "unavailable" && (
|
|
87
|
+
<Text c="dimmed" mt="xs" size="xs">{usage.label}</Text>
|
|
88
|
+
)}
|
|
71
89
|
</Box>
|
|
72
90
|
);
|
|
73
91
|
}
|