taskchef 8.1.1 → 8.2.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/package.json +1 -1
- package/src/dashboard/dist/app.js +40 -40
- package/src/dashboard/dist/app.js.map +1 -1
- package/src/dashboard/dist/styles.css +1 -1
- package/src/dashboard/react/components/ActivityTimeline.tsx +43 -55
- package/src/dashboard/react/components/StatsGrid.tsx +60 -0
- package/src/dashboard/react/components/TaskCardStats.test.tsx +5 -4
- package/src/dashboard/react/components/TaskCardStats.tsx +12 -42
- package/src/dashboard/react/components/TaskDetail.test.tsx +19 -15
- package/src/dashboard/react/presentation.ts +18 -9
- package/src/dashboard/react/styles.css +12 -20
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { Box, Paper, Stack, Text } from "@mantine/core";
|
|
2
|
+
import {
|
|
3
|
+
IconCurrencyDollar,
|
|
4
|
+
IconHistory,
|
|
5
|
+
IconHourglass,
|
|
6
|
+
IconStack2,
|
|
7
|
+
} from "@tabler/icons-react";
|
|
2
8
|
import { mergeProjectedTurns, turnPresentation } from "../../state.js";
|
|
3
9
|
import { turnReportedWorkView, turnUsageMetricsView } from "../presentation";
|
|
4
10
|
import type { Task, TaskTurn } from "../types";
|
|
@@ -6,6 +12,7 @@ import { useEffect, useState } from "react";
|
|
|
6
12
|
import { RelativeTime } from "./RelativeTime";
|
|
7
13
|
import { LinkedText } from "./LinkedText";
|
|
8
14
|
import { ShimmerText } from "./ShimmerText";
|
|
15
|
+
import { StatCell, StaticStat, StatsGrid } from "./StatsGrid";
|
|
9
16
|
import { StatusBadge } from "./StatusBadge";
|
|
10
17
|
|
|
11
18
|
export function ActivityTimeline({ highlightTurnRef, task }: { highlightTurnRef: string | null; task: Task }) {
|
|
@@ -29,13 +36,7 @@ export function ActivityTimeline({ highlightTurnRef, task }: { highlightTurnRef:
|
|
|
29
36
|
withBorder
|
|
30
37
|
>
|
|
31
38
|
<Stack gap={7}>
|
|
32
|
-
<
|
|
33
|
-
<StatusBadge status={presentation.status} />
|
|
34
|
-
<Box className="taskchef-turn-timing">
|
|
35
|
-
<RelativeTime label="Turn update time" value={presentation.updatedAt} />
|
|
36
|
-
<TurnReportedWork turn={turn} />
|
|
37
|
-
</Box>
|
|
38
|
-
</Box>
|
|
39
|
+
<StatusBadge status={presentation.status} />
|
|
39
40
|
{presentation.sourceLabel && <Text c="dimmed" size="xs">{presentation.sourceLabel}</Text>}
|
|
40
41
|
<Box>
|
|
41
42
|
<Text c="dimmed" className="taskchef-field-label" size="xs">Request</Text>
|
|
@@ -51,22 +52,36 @@ export function ActivityTimeline({ highlightTurnRef, task }: { highlightTurnRef:
|
|
|
51
52
|
: <LinkedText task={task} text={presentation.summary} />}
|
|
52
53
|
</Text>
|
|
53
54
|
</Box>
|
|
54
|
-
<
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
55
|
+
<StatsGrid variant="turn">
|
|
56
|
+
<StatCell>
|
|
57
|
+
<RelativeTime
|
|
58
|
+
icon={<IconHistory aria-hidden size={14} />}
|
|
59
|
+
label="Turn update time"
|
|
60
|
+
value={presentation.updatedAt}
|
|
61
|
+
/>
|
|
62
|
+
</StatCell>
|
|
63
|
+
<StatCell>
|
|
64
|
+
<TurnReportedWork turn={turn} />
|
|
65
|
+
</StatCell>
|
|
66
|
+
<StatCell usageState={usage.kind}>
|
|
67
|
+
<StaticStat
|
|
68
|
+
accessibleLabel={usage.tokens.accessibleLabel}
|
|
69
|
+
animated={usage.animated}
|
|
70
|
+
icon={<IconStack2 aria-hidden size={14} />}
|
|
71
|
+
title={usage.title}
|
|
72
|
+
value={usage.tokens.value}
|
|
73
|
+
/>
|
|
74
|
+
</StatCell>
|
|
75
|
+
<StatCell usageState={usage.kind}>
|
|
76
|
+
<StaticStat
|
|
77
|
+
accessibleLabel={usage.cost.accessibleLabel}
|
|
78
|
+
animated={usage.animated}
|
|
79
|
+
icon={<IconCurrencyDollar aria-hidden size={14} />}
|
|
80
|
+
title={usage.title}
|
|
81
|
+
value={usage.cost.value}
|
|
82
|
+
/>
|
|
83
|
+
</StatCell>
|
|
84
|
+
</StatsGrid>
|
|
70
85
|
{usage.note && <Text c="dimmed" size="xs">{usage.note}</Text>}
|
|
71
86
|
<Text c="dimmed" className="taskchef-mono" size="xs">Turn ref: {identity ?? "—"}</Text>
|
|
72
87
|
</Stack>
|
|
@@ -81,39 +96,12 @@ function TurnReportedWork({ turn }: { turn: TaskTurn }) {
|
|
|
81
96
|
const now = useLiveNow(turn.result === null);
|
|
82
97
|
const elapsed = turnReportedWorkView(turn, now);
|
|
83
98
|
return (
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
className="taskchef-turn-duration"
|
|
88
|
-
size="xs"
|
|
99
|
+
<StaticStat
|
|
100
|
+
accessibleLabel={elapsed.accessibleLabel}
|
|
101
|
+
icon={<IconHourglass aria-hidden size={14} />}
|
|
89
102
|
title={elapsed.title}
|
|
90
|
-
|
|
91
|
-
|
|
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>
|
|
103
|
+
value={elapsed.value}
|
|
104
|
+
/>
|
|
117
105
|
);
|
|
118
106
|
}
|
|
119
107
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Box, Text, Tooltip } from "@mantine/core";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { ShimmerText } from "./ShimmerText";
|
|
4
|
+
|
|
5
|
+
export function StatsGrid({
|
|
6
|
+
children,
|
|
7
|
+
variant,
|
|
8
|
+
}: {
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
variant: "card" | "turn";
|
|
11
|
+
}) {
|
|
12
|
+
return (
|
|
13
|
+
<Box className={`taskchef-stats taskchef-${variant}-stats`} component="div">
|
|
14
|
+
{children}
|
|
15
|
+
</Box>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function StatCell({
|
|
20
|
+
children,
|
|
21
|
+
usageState,
|
|
22
|
+
}: {
|
|
23
|
+
children: ReactNode;
|
|
24
|
+
usageState?: string;
|
|
25
|
+
}) {
|
|
26
|
+
return (
|
|
27
|
+
<Box className="taskchef-stat" data-usage-state={usageState}>
|
|
28
|
+
{children}
|
|
29
|
+
</Box>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function StaticStat({
|
|
34
|
+
accessibleLabel,
|
|
35
|
+
animated = false,
|
|
36
|
+
icon,
|
|
37
|
+
title,
|
|
38
|
+
value,
|
|
39
|
+
}: {
|
|
40
|
+
accessibleLabel: string;
|
|
41
|
+
animated?: boolean;
|
|
42
|
+
icon: ReactNode;
|
|
43
|
+
title: string;
|
|
44
|
+
value: string;
|
|
45
|
+
}) {
|
|
46
|
+
return (
|
|
47
|
+
<Tooltip events={{ focus: true, hover: true, touch: false }} label={title} multiline>
|
|
48
|
+
<Text
|
|
49
|
+
aria-label={accessibleLabel}
|
|
50
|
+
className="taskchef-stat-content"
|
|
51
|
+
component="span"
|
|
52
|
+
size="xs"
|
|
53
|
+
tabIndex={0}
|
|
54
|
+
>
|
|
55
|
+
{icon}
|
|
56
|
+
<bdi>{animated ? <ShimmerText>{value}</ShimmerText> : value}</bdi>
|
|
57
|
+
</Text>
|
|
58
|
+
</Tooltip>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -34,7 +34,8 @@ describe("task card stats", () => {
|
|
|
34
34
|
expect(within(cells[0]).getByLabelText(/updated time/i)).toBeVisible();
|
|
35
35
|
expect(within(cells[1]).getByLabelText(/reported work 18m 32s/i)).toBeVisible();
|
|
36
36
|
expect(within(cells[2]).getByLabelText(/1,324,567 tokens/i)).toHaveTextContent("1.32M tokens");
|
|
37
|
-
expect(within(cells[3]).getByLabelText(/estimated cost \$12\.34/i)).toHaveTextContent("
|
|
37
|
+
expect(within(cells[3]).getByLabelText(/estimated cost \$12\.34/i)).toHaveTextContent("12.34");
|
|
38
|
+
expect(within(cells[3]).getByLabelText(/estimated cost \$12\.34/i)).not.toHaveTextContent("$");
|
|
38
39
|
expect(within(cells[2]).getByRole("status")).toHaveTextContent(/1,324,567 tokens.*estimated cost \$12\.34/i);
|
|
39
40
|
});
|
|
40
41
|
|
|
@@ -44,7 +45,7 @@ describe("task card stats", () => {
|
|
|
44
45
|
usage: { status: "available", task: { totalTokens: 1_324_567, estimatedCostUsd: null } },
|
|
45
46
|
}));
|
|
46
47
|
expect(screen.getByLabelText(/1,324,567 tokens/i)).toHaveTextContent("1.32M tokens");
|
|
47
|
-
expect(screen.getByLabelText(/estimated cost unavailable/i)).toHaveTextContent("
|
|
48
|
+
expect(screen.getByLabelText(/estimated cost unavailable/i)).toHaveTextContent("n/a");
|
|
48
49
|
});
|
|
49
50
|
|
|
50
51
|
test("shows metric details to keyboard users on focus", () => {
|
|
@@ -78,7 +79,7 @@ describe("task card stats", () => {
|
|
|
78
79
|
usage: { status: "unavailable", reason: "No matching cached boundary." },
|
|
79
80
|
})} /></MantineProvider>);
|
|
80
81
|
expect(screen.getByLabelText(/token usage unavailable: no matching/i)).toHaveTextContent("n/a tokens");
|
|
81
|
-
expect(screen.getByLabelText(/estimated cost unavailable: no matching/i)).toHaveTextContent("
|
|
82
|
+
expect(screen.getByLabelText(/estimated cost unavailable: no matching/i)).toHaveTextContent("n/a");
|
|
82
83
|
});
|
|
83
84
|
|
|
84
85
|
test("retains known totals and marks both usage values as updating", () => {
|
|
@@ -92,7 +93,7 @@ describe("task card stats", () => {
|
|
|
92
93
|
},
|
|
93
94
|
}));
|
|
94
95
|
expect(screen.getByLabelText(/1,324,567 tokens; updating/i)).toHaveTextContent("1.32M tokens · Updating…");
|
|
95
|
-
expect(screen.getByLabelText(/estimated cost \$12\.34; updating/i)).toHaveTextContent("
|
|
96
|
+
expect(screen.getByLabelText(/estimated cost \$12\.34; updating/i)).toHaveTextContent("12.34 · Updating…");
|
|
96
97
|
});
|
|
97
98
|
|
|
98
99
|
test("places Open chat beneath the status badge and keeps callbacks working", () => {
|
|
@@ -1,66 +1,36 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Text } from "@mantine/core";
|
|
2
2
|
import {
|
|
3
3
|
IconCurrencyDollar,
|
|
4
4
|
IconHistory,
|
|
5
5
|
IconHourglass,
|
|
6
6
|
IconStack2,
|
|
7
7
|
} from "@tabler/icons-react";
|
|
8
|
-
import type { ReactNode } from "react";
|
|
9
8
|
import type { Task } from "../types";
|
|
10
9
|
import { listUsageMetricsView, taskReportedWorkView } from "../presentation";
|
|
11
10
|
import { RelativeTime } from "./RelativeTime";
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
function StaticStat({
|
|
15
|
-
accessibleLabel,
|
|
16
|
-
animated = false,
|
|
17
|
-
icon,
|
|
18
|
-
title,
|
|
19
|
-
value,
|
|
20
|
-
}: {
|
|
21
|
-
accessibleLabel: string;
|
|
22
|
-
animated?: boolean;
|
|
23
|
-
icon: ReactNode;
|
|
24
|
-
title: string;
|
|
25
|
-
value: string;
|
|
26
|
-
}) {
|
|
27
|
-
return (
|
|
28
|
-
<Tooltip events={{ focus: true, hover: true, touch: false }} label={title} multiline>
|
|
29
|
-
<Text
|
|
30
|
-
aria-label={accessibleLabel}
|
|
31
|
-
className="taskchef-card-stat-content"
|
|
32
|
-
component="span"
|
|
33
|
-
size="xs"
|
|
34
|
-
tabIndex={0}
|
|
35
|
-
>
|
|
36
|
-
{icon}
|
|
37
|
-
<bdi>{animated ? <ShimmerText>{value}</ShimmerText> : value}</bdi>
|
|
38
|
-
</Text>
|
|
39
|
-
</Tooltip>
|
|
40
|
-
);
|
|
41
|
-
}
|
|
11
|
+
import { StatCell, StaticStat, StatsGrid } from "./StatsGrid";
|
|
42
12
|
|
|
43
13
|
export function TaskCardStats({ task }: { task: Task }) {
|
|
44
14
|
const reportedWork = taskReportedWorkView(task);
|
|
45
15
|
const usage = listUsageMetricsView(task);
|
|
46
16
|
return (
|
|
47
|
-
<
|
|
48
|
-
<
|
|
17
|
+
<StatsGrid variant="card">
|
|
18
|
+
<StatCell>
|
|
49
19
|
<RelativeTime
|
|
50
20
|
icon={<IconHistory aria-hidden size={14} />}
|
|
51
21
|
label={`Updated time for ${task.title}`}
|
|
52
22
|
value={task.meaningfulUpdatedAt ?? task.updatedAt}
|
|
53
23
|
/>
|
|
54
|
-
</
|
|
55
|
-
<
|
|
24
|
+
</StatCell>
|
|
25
|
+
<StatCell>
|
|
56
26
|
<StaticStat
|
|
57
27
|
accessibleLabel={reportedWork.accessibleLabel}
|
|
58
28
|
icon={<IconHourglass aria-hidden size={14} />}
|
|
59
29
|
title={reportedWork.title}
|
|
60
30
|
value={reportedWork.value}
|
|
61
31
|
/>
|
|
62
|
-
</
|
|
63
|
-
<
|
|
32
|
+
</StatCell>
|
|
33
|
+
<StatCell usageState={usage.kind}>
|
|
64
34
|
<StaticStat
|
|
65
35
|
accessibleLabel={usage.tokens.accessibleLabel}
|
|
66
36
|
animated={usage.animated}
|
|
@@ -71,8 +41,8 @@ export function TaskCardStats({ task }: { task: Task }) {
|
|
|
71
41
|
<Text className="taskchef-visually-hidden" component="span" role="status">
|
|
72
42
|
{usage.tokens.accessibleLabel}; {usage.cost.accessibleLabel}
|
|
73
43
|
</Text>
|
|
74
|
-
</
|
|
75
|
-
<
|
|
44
|
+
</StatCell>
|
|
45
|
+
<StatCell usageState={usage.kind}>
|
|
76
46
|
<StaticStat
|
|
77
47
|
accessibleLabel={usage.cost.accessibleLabel}
|
|
78
48
|
animated={usage.animated}
|
|
@@ -80,7 +50,7 @@ export function TaskCardStats({ task }: { task: Task }) {
|
|
|
80
50
|
title={usage.cost.title}
|
|
81
51
|
value={usage.cost.value}
|
|
82
52
|
/>
|
|
83
|
-
</
|
|
84
|
-
</
|
|
53
|
+
</StatCell>
|
|
54
|
+
</StatsGrid>
|
|
85
55
|
);
|
|
86
56
|
}
|
|
@@ -69,16 +69,20 @@ test("covers terminal, active, and unavailable timeline metrics", () => {
|
|
|
69
69
|
const { container } = render(
|
|
70
70
|
<MantineProvider><ActivityTimeline highlightTurnRef={null} task={task} /></MantineProvider>,
|
|
71
71
|
);
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
expect(
|
|
76
|
-
expect(within(
|
|
77
|
-
expect(
|
|
78
|
-
expect(within(
|
|
79
|
-
|
|
80
|
-
expect(
|
|
81
|
-
|
|
72
|
+
const stats = container.querySelector(".taskchef-turn-stats") as HTMLElement;
|
|
73
|
+
expect(stats).not.toBeNull();
|
|
74
|
+
const cells = [...stats.children] as HTMLElement[];
|
|
75
|
+
expect(cells).toHaveLength(4);
|
|
76
|
+
expect(within(cells[0]).getByLabelText(/^Turn update time:/i)).toBeVisible();
|
|
77
|
+
expect(cells[0].querySelector(".tabler-icon-history")).not.toBeNull();
|
|
78
|
+
expect(within(cells[1]).getByLabelText(/completed turn reported wall-clock elapsed time/i))
|
|
79
|
+
.toHaveTextContent("18m 32s");
|
|
80
|
+
expect(cells[1].querySelector(".tabler-icon-hourglass")).not.toBeNull();
|
|
81
|
+
expect(within(cells[2]).getByLabelText("330 tokens")).toHaveTextContent("330");
|
|
82
|
+
expect(cells[2].querySelector(".tabler-icon-stack-2")).not.toBeNull();
|
|
83
|
+
expect(within(cells[3]).getByLabelText("Estimated cost $0.12")).toHaveTextContent("0.12");
|
|
84
|
+
expect(within(cells[3]).getByLabelText("Estimated cost $0.12")).not.toHaveTextContent("$");
|
|
85
|
+
expect(cells[3].querySelector(".tabler-icon-currency-dollar")).not.toBeNull();
|
|
82
86
|
cleanup();
|
|
83
87
|
}
|
|
84
88
|
|
|
@@ -94,9 +98,9 @@ test("covers terminal, active, and unavailable timeline metrics", () => {
|
|
|
94
98
|
const task = fixtureTask();
|
|
95
99
|
render(<MantineProvider><ActivityTimeline highlightTurnRef={null} task={task} /></MantineProvider>);
|
|
96
100
|
expect(screen.getByLabelText(/current turn reported wall-clock elapsed time/i))
|
|
97
|
-
.toHaveTextContent("
|
|
98
|
-
expect(screen.
|
|
99
|
-
expect(screen.
|
|
101
|
+
.toHaveTextContent("18m 32s");
|
|
102
|
+
expect(screen.getByLabelText("Tokens pending")).toHaveTextContent("Pending");
|
|
103
|
+
expect(screen.getByLabelText("Estimated cost pending")).toHaveTextContent("Pending");
|
|
100
104
|
|
|
101
105
|
act(() => {
|
|
102
106
|
now += 2_000;
|
|
@@ -104,7 +108,7 @@ test("covers terminal, active, and unavailable timeline metrics", () => {
|
|
|
104
108
|
});
|
|
105
109
|
|
|
106
110
|
expect(screen.getByLabelText(/current turn reported wall-clock elapsed time/i))
|
|
107
|
-
.toHaveTextContent("
|
|
111
|
+
.toHaveTextContent("18m 34s");
|
|
108
112
|
cleanup();
|
|
109
113
|
vi.restoreAllMocks();
|
|
110
114
|
}
|
|
@@ -133,7 +137,7 @@ test("covers terminal, active, and unavailable timeline metrics", () => {
|
|
|
133
137
|
});
|
|
134
138
|
render(<MantineProvider><ActivityTimeline highlightTurnRef={null} task={task} /></MantineProvider>);
|
|
135
139
|
expect(screen.getByLabelText(/elapsed unavailable.*reported wall-clock/i))
|
|
136
|
-
.toHaveTextContent("
|
|
140
|
+
.toHaveTextContent("n/a");
|
|
137
141
|
expect(screen.getByText("Manual dashboard turns do not have usage boundaries.")).toBeVisible();
|
|
138
142
|
expect(screen.getByLabelText(/tokens unavailable: manual dashboard turns/i)).toBeVisible();
|
|
139
143
|
}
|
|
@@ -127,6 +127,13 @@ export function formatEstimatedCost(value: number | null | undefined, locales?:
|
|
|
127
127
|
}).format(value);
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
function formatEstimatedCostAmount(value: number, locales?: Intl.LocalesArgument) {
|
|
131
|
+
return new Intl.NumberFormat(locales, {
|
|
132
|
+
maximumFractionDigits: 2,
|
|
133
|
+
minimumFractionDigits: 2,
|
|
134
|
+
}).format(value);
|
|
135
|
+
}
|
|
136
|
+
|
|
130
137
|
export type UsageView =
|
|
131
138
|
| { kind: "pending"; label: "Token usage pending" }
|
|
132
139
|
| { kind: "calculating"; label: "Calculating token usage" }
|
|
@@ -194,13 +201,14 @@ export function turnUsageMetricsView(task: Task, turn: NonNullable<Task["turns"]
|
|
|
194
201
|
const identity = turn.turnRef ?? turn.turnId;
|
|
195
202
|
const usage = identity ? task.usage?.turns?.[identity] : null;
|
|
196
203
|
const tokens = typeof usage?.totalTokens === "number" ? formatFullTokens(usage.totalTokens) : NOT_AVAILABLE;
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
204
|
+
const estimatedCost = usage?.estimatedCostUsd;
|
|
205
|
+
const hasCost = typeof estimatedCost === "number" && Number.isFinite(estimatedCost);
|
|
206
|
+
const cost = hasCost ? formatEstimatedCostAmount(estimatedCost) : NOT_AVAILABLE;
|
|
207
|
+
const accessibleCost = hasCost ? formatEstimatedCost(estimatedCost).toLowerCase() : "unavailable";
|
|
200
208
|
return {
|
|
201
209
|
animated: false,
|
|
202
210
|
cost: {
|
|
203
|
-
accessibleLabel: `Estimated cost ${
|
|
211
|
+
accessibleLabel: `Estimated cost ${accessibleCost}`,
|
|
204
212
|
value: cost,
|
|
205
213
|
},
|
|
206
214
|
kind: view.kind,
|
|
@@ -276,7 +284,7 @@ export function listUsageMetricsView(task: Task): ListUsageMetricsView {
|
|
|
276
284
|
cost: {
|
|
277
285
|
accessibleLabel: `Estimated cost unavailable${reason}`,
|
|
278
286
|
title: view.label,
|
|
279
|
-
value:
|
|
287
|
+
value: NOT_AVAILABLE,
|
|
280
288
|
},
|
|
281
289
|
kind: "unavailable",
|
|
282
290
|
tokens: {
|
|
@@ -288,9 +296,10 @@ export function listUsageMetricsView(task: Task): ListUsageMetricsView {
|
|
|
288
296
|
}
|
|
289
297
|
const compactTokens = formatCompactTokens(view.usage.totalTokens);
|
|
290
298
|
const fullTokens = formatFullTokens(view.usage.totalTokens);
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
const formattedCost = hasCost ? formatEstimatedCost(
|
|
299
|
+
const estimatedCost = view.usage.estimatedCostUsd;
|
|
300
|
+
const hasCost = typeof estimatedCost === "number" && Number.isFinite(estimatedCost);
|
|
301
|
+
const formattedCost = hasCost ? formatEstimatedCost(estimatedCost) : null;
|
|
302
|
+
const formattedCostAmount = hasCost ? formatEstimatedCostAmount(estimatedCost) : null;
|
|
294
303
|
const qualifier = view.knownSoFar ? " · Updating…" : "";
|
|
295
304
|
const accessibleQualifier = view.knownSoFar ? "; updating" : "";
|
|
296
305
|
const freshness = view.usage.sourceUpdatedAt ?? view.usage.sampledAt ?? task.usage?.updatedAt ?? null;
|
|
@@ -304,7 +313,7 @@ export function listUsageMetricsView(task: Task): ListUsageMetricsView {
|
|
|
304
313
|
title: `${formattedCost === null
|
|
305
314
|
? "Estimated cost unavailable"
|
|
306
315
|
: `Unrounded estimate: $${view.usage.estimatedCostUsd}`}${qualifier}.${freshnessLabel}`,
|
|
307
|
-
value: `${
|
|
316
|
+
value: `${formattedCostAmount ?? NOT_AVAILABLE}${qualifier}`,
|
|
308
317
|
},
|
|
309
318
|
kind: "ready",
|
|
310
319
|
tokens: {
|
|
@@ -56,15 +56,15 @@ body {
|
|
|
56
56
|
.taskchef-github-group { min-width: 0; }
|
|
57
57
|
.taskchef-github-reference-ambiguous { border-bottom: 2px dotted var(--mantine-color-yellow-7); cursor: help; }
|
|
58
58
|
.taskchef-github-reference-ambiguous:focus-visible { outline: 2px solid var(--mantine-color-yellow-7); outline-offset: 2px; }
|
|
59
|
-
.taskchef-time, .taskchef-
|
|
59
|
+
.taskchef-time, .taskchef-stat-content { display: inline-flex; align-items: center; gap: 6px; width: fit-content; max-width: 100%; padding: 3px 4px; border-radius: 4px; color: var(--taskchef-muted); font-size: var(--mantine-font-size-xs); font-weight: 400; letter-spacing: normal; line-height: var(--mantine-line-height-xs); text-transform: none; }
|
|
60
60
|
.taskchef-time:hover { color: var(--taskchef-text); background: var(--taskchef-surface-muted); }
|
|
61
61
|
.taskchef-time:focus-visible { outline: 2px solid var(--mantine-color-teal-6); outline-offset: 2px; }
|
|
62
|
-
.taskchef-time-label, .taskchef-
|
|
63
|
-
.taskchef-
|
|
64
|
-
.taskchef-
|
|
65
|
-
.taskchef-
|
|
66
|
-
.taskchef-
|
|
67
|
-
.taskchef-
|
|
62
|
+
.taskchef-time-label, .taskchef-stat-content bdi { min-width: 0; overflow-wrap: anywhere; unicode-bidi: isolate; }
|
|
63
|
+
.taskchef-stat-content:focus-visible { outline: 2px solid var(--mantine-color-teal-6); outline-offset: 2px; }
|
|
64
|
+
.taskchef-stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin-top: 2px; }
|
|
65
|
+
.taskchef-stat { display: flex; align-items: center; min-width: 0; padding: 4px 10px; }
|
|
66
|
+
.taskchef-stat:first-child { padding-inline-start: 0; }
|
|
67
|
+
.taskchef-stat + .taskchef-stat { border-inline-start: 1px solid var(--taskchef-border); }
|
|
68
68
|
.taskchef-empty { border-style: dashed; background: transparent; }
|
|
69
69
|
|
|
70
70
|
.taskchef-notifications {
|
|
@@ -84,13 +84,8 @@ body {
|
|
|
84
84
|
.taskchef-metric { min-width: 0; background: transparent; }
|
|
85
85
|
.taskchef-metric-value { overflow-wrap: anywhere; }
|
|
86
86
|
.taskchef-metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: var(--mantine-spacing-xs); }
|
|
87
|
-
.taskchef-turn { background: var(--taskchef-surface-muted); }
|
|
87
|
+
.taskchef-turn { container-type: inline-size; background: var(--taskchef-surface-muted); }
|
|
88
88
|
.taskchef-turn-highlighted { border-color: var(--mantine-color-teal-6); box-shadow: inset 3px 0 var(--mantine-color-teal-6); outline: 3px solid color-mix(in srgb, var(--mantine-color-teal-6) 22%, transparent); }
|
|
89
|
-
.taskchef-turn-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
|
90
|
-
.taskchef-turn-timing { display: flex; align-items: center; justify-content: flex-end; gap: var(--mantine-spacing-sm); min-width: 0; }
|
|
91
|
-
.taskchef-turn-duration { display: inline-flex; align-items: baseline; gap: 4px; white-space: nowrap; }
|
|
92
|
-
.taskchef-turn-metrics { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--mantine-spacing-xs); }
|
|
93
|
-
.taskchef-turn-metric { min-width: 0; overflow-wrap: anywhere; }
|
|
94
89
|
.taskchef-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; overflow-wrap: anywhere; }
|
|
95
90
|
.taskchef-code-panel { max-height: 280px; margin: 0; padding: 12px; overflow: auto; border-radius: var(--mantine-radius-md); background: var(--taskchef-surface-muted); white-space: pre-wrap; overflow-wrap: anywhere; font: 0.78rem/1.55 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
96
91
|
.taskchef-metadata { display: grid; grid-template-columns: minmax(120px, 150px) minmax(0, 1fr); margin: 0; font-size: var(--mantine-font-size-sm); }
|
|
@@ -130,9 +125,6 @@ body {
|
|
|
130
125
|
.taskchef-notifications { right: 16px; bottom: 16px; width: min(330px, calc(100vw - 32px)); }
|
|
131
126
|
.taskchef-metadata { grid-template-columns: 1fr; }
|
|
132
127
|
.taskchef-metrics { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
133
|
-
.taskchef-turn-metrics { grid-template-columns: 1fr; }
|
|
134
|
-
.taskchef-turn-heading { align-items: flex-start; flex-direction: column; gap: 6px; }
|
|
135
|
-
.taskchef-turn-timing { flex-wrap: wrap; justify-content: flex-start; }
|
|
136
128
|
.taskchef-metadata dt { padding-bottom: 0; border-bottom: 0; }
|
|
137
129
|
}
|
|
138
130
|
|
|
@@ -142,10 +134,10 @@ body {
|
|
|
142
134
|
}
|
|
143
135
|
|
|
144
136
|
@container (max-width: 27.5rem) {
|
|
145
|
-
.taskchef-
|
|
146
|
-
.taskchef-
|
|
147
|
-
.taskchef-
|
|
148
|
-
.taskchef-
|
|
137
|
+
.taskchef-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
138
|
+
.taskchef-stat { padding-block: 8px; }
|
|
139
|
+
.taskchef-stat:nth-child(3) { border-inline-start: 0; padding-inline-start: 0; }
|
|
140
|
+
.taskchef-stat:nth-child(n + 3) { border-top: 1px solid var(--taskchef-border); }
|
|
149
141
|
}
|
|
150
142
|
|
|
151
143
|
.taskchef-refresh-spinning {
|