taskchef 7.25.2 → 7.26.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 +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 +69 -12
- package/src/dashboard/react/components/TaskDetail.test.tsx +91 -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 +4 -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" : ""}`}
|
|
@@ -47,15 +48,24 @@ export function ActivityTimeline({ highlightTurnRef, task }: { highlightTurnRef:
|
|
|
47
48
|
: <LinkedText task={task} text={presentation.summary} />}
|
|
48
49
|
</Text>
|
|
49
50
|
</Box>
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
51
|
+
<Box className="taskchef-turn-metrics">
|
|
52
|
+
<TurnMetric
|
|
53
|
+
accessibleLabel={usage.tokens.accessibleLabel}
|
|
54
|
+
animated={usage.animated}
|
|
55
|
+
label="Tokens"
|
|
56
|
+
title={usage.title}
|
|
57
|
+
value={usage.tokens.value}
|
|
58
|
+
/>
|
|
59
|
+
<TurnMetric
|
|
60
|
+
accessibleLabel={usage.cost.accessibleLabel}
|
|
61
|
+
animated={usage.animated}
|
|
62
|
+
label="Estimated cost"
|
|
63
|
+
title={usage.title}
|
|
64
|
+
value={usage.cost.value}
|
|
65
|
+
/>
|
|
66
|
+
<TurnReportedWorkMetric turn={turn} />
|
|
67
|
+
</Box>
|
|
68
|
+
{usage.note && <Text c="dimmed" size="xs">{usage.note}</Text>}
|
|
59
69
|
<Text c="dimmed" className="taskchef-mono" size="xs">Turn ref: {identity ?? "—"}</Text>
|
|
60
70
|
</Stack>
|
|
61
71
|
</Paper>
|
|
@@ -64,3 +74,50 @@ export function ActivityTimeline({ highlightTurnRef, task }: { highlightTurnRef:
|
|
|
64
74
|
</Stack>
|
|
65
75
|
);
|
|
66
76
|
}
|
|
77
|
+
|
|
78
|
+
function TurnReportedWorkMetric({ turn }: { turn: TaskTurn }) {
|
|
79
|
+
const now = useLiveNow(turn.result === null);
|
|
80
|
+
const elapsed = turnReportedWorkView(turn, now);
|
|
81
|
+
return (
|
|
82
|
+
<TurnMetric
|
|
83
|
+
accessibleLabel={elapsed.accessibleLabel}
|
|
84
|
+
label={elapsed.label}
|
|
85
|
+
title={elapsed.title}
|
|
86
|
+
value={elapsed.value}
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function TurnMetric({
|
|
92
|
+
accessibleLabel,
|
|
93
|
+
animated = false,
|
|
94
|
+
label,
|
|
95
|
+
title,
|
|
96
|
+
value,
|
|
97
|
+
}: {
|
|
98
|
+
accessibleLabel?: string;
|
|
99
|
+
animated?: boolean;
|
|
100
|
+
label: string;
|
|
101
|
+
title?: string;
|
|
102
|
+
value: string;
|
|
103
|
+
}) {
|
|
104
|
+
return (
|
|
105
|
+
<Box className="taskchef-turn-metric" title={title}>
|
|
106
|
+
<Text c="dimmed" className="taskchef-field-label" size="xs">{label}</Text>
|
|
107
|
+
<Text aria-label={accessibleLabel} c="dimmed" size="xs">
|
|
108
|
+
{animated ? <ShimmerText>{value}</ShimmerText> : value}
|
|
109
|
+
</Text>
|
|
110
|
+
</Box>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function useLiveNow(active: boolean) {
|
|
115
|
+
const [now, setNow] = useState(() => Date.now());
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (!active) return undefined;
|
|
118
|
+
setNow(Date.now());
|
|
119
|
+
const interval = window.setInterval(() => setNow(Date.now()), 1_000);
|
|
120
|
+
return () => window.clearInterval(interval);
|
|
121
|
+
}, [active]);
|
|
122
|
+
return now;
|
|
123
|
+
}
|
|
@@ -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,90 @@ 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
|
+
expect(metrics).not.toBeNull();
|
|
74
|
+
expect(within(metrics).getByText("Tokens").nextSibling).toHaveTextContent("330");
|
|
75
|
+
expect(within(metrics).getByText("Estimated cost").nextSibling).toHaveTextContent("$0.12");
|
|
76
|
+
expect(within(metrics).getByText("Elapsed").nextSibling).toHaveTextContent("18m 32s");
|
|
77
|
+
expect(within(metrics).getByLabelText(/completed turn reported wall-clock elapsed time/i)).toBeVisible();
|
|
78
|
+
cleanup();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
{
|
|
82
|
+
let now = Date.parse("2026-08-30T08:18:32.000Z");
|
|
83
|
+
let tick: (() => void) | undefined;
|
|
84
|
+
vi.spyOn(Date, "now").mockImplementation(() => now);
|
|
85
|
+
vi.spyOn(window, "setInterval").mockImplementation(((handler: TimerHandler) => {
|
|
86
|
+
tick = handler as () => void;
|
|
87
|
+
return 1;
|
|
88
|
+
}) as typeof window.setInterval);
|
|
89
|
+
vi.spyOn(window, "clearInterval").mockImplementation(() => undefined);
|
|
90
|
+
const task = fixtureTask();
|
|
91
|
+
render(<MantineProvider><ActivityTimeline highlightTurnRef={null} task={task} /></MantineProvider>);
|
|
92
|
+
expect(screen.getByText("Elapsed so far").nextSibling).toHaveTextContent("18m 32s");
|
|
93
|
+
expect(screen.getByText("Tokens").nextSibling).toHaveTextContent("Pending");
|
|
94
|
+
expect(screen.getByText("Estimated cost").nextSibling).toHaveTextContent("Pending");
|
|
95
|
+
|
|
96
|
+
act(() => {
|
|
97
|
+
now += 2_000;
|
|
98
|
+
tick?.();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(screen.getByText("Elapsed so far").nextSibling).toHaveTextContent("18m 34s");
|
|
102
|
+
cleanup();
|
|
103
|
+
vi.restoreAllMocks();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
{
|
|
107
|
+
const task = fixtureTask({
|
|
108
|
+
status: "completed",
|
|
109
|
+
turns: [{
|
|
110
|
+
...fixtureTask().turns![0],
|
|
111
|
+
startedAt: "2026-08-30T08:18:33.000Z",
|
|
112
|
+
result: {
|
|
113
|
+
status: "completed",
|
|
114
|
+
summary: "Done.",
|
|
115
|
+
updatedAt: "2026-08-30T08:18:32.000Z",
|
|
116
|
+
},
|
|
117
|
+
}],
|
|
118
|
+
usage: {
|
|
119
|
+
status: "unavailable",
|
|
120
|
+
turns: {
|
|
121
|
+
"turn-one": {
|
|
122
|
+
status: "unavailable",
|
|
123
|
+
reason: "Manual dashboard turns do not have usage boundaries.",
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
render(<MantineProvider><ActivityTimeline highlightTurnRef={null} task={task} /></MantineProvider>);
|
|
129
|
+
expect(screen.getByText("Elapsed").nextSibling).toHaveTextContent("Unavailable");
|
|
130
|
+
expect(screen.getByLabelText(/elapsed unavailable.*reported wall-clock/i)).toBeVisible();
|
|
131
|
+
expect(screen.getByText("Manual dashboard turns do not have usage boundaries.")).toBeVisible();
|
|
132
|
+
expect(screen.getByLabelText(/tokens unavailable: manual dashboard turns/i)).toBeVisible();
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
47
136
|
function ConfirmationHarness({ onTransition }: { onTransition: (status: TerminalStatus, actionId: string) => Promise<{ ok: boolean; rotateActionId?: boolean }> }) {
|
|
48
137
|
const [opened, setOpened] = useState(true);
|
|
49
138
|
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
|
}
|