taskchef 7.24.1 → 7.25.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/index.js +2 -0
- package/package.json +1 -1
- package/src/dashboard/dist/app.js +34 -34
- package/src/dashboard/dist/app.js.map +1 -1
- package/src/dashboard/dist/styles.css +1 -1
- package/src/dashboard/react/App.tsx +27 -8
- package/src/dashboard/react/components/NotificationCenter.test.tsx +6 -1
- package/src/dashboard/react/components/TaskCard.tsx +8 -4
- package/src/dashboard/react/components/TaskUsageSummary.test.tsx +85 -0
- package/src/dashboard/react/components/TaskUsageSummary.tsx +24 -0
- package/src/dashboard/react/overlay-state.test.ts +31 -0
- package/src/dashboard/react/overlay-state.ts +23 -1
- package/src/dashboard/react/presentation.ts +47 -0
- package/src/dashboard/react/styles.css +7 -0
- package/src/dashboard/react/types.ts +3 -0
- package/src/dashboard.js +243 -15
- package/src/usage.js +3 -3
|
@@ -30,7 +30,12 @@ import {
|
|
|
30
30
|
STATUS_FILTERS,
|
|
31
31
|
} from "../state.js";
|
|
32
32
|
import { connectDashboard, dashboardVersion, manualTransition, openInCodex, taskDetail } from "./api";
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
clearNotificationHistory,
|
|
35
|
+
listUsageSignature,
|
|
36
|
+
mergeListTaskIntoDetail,
|
|
37
|
+
notificationClicked,
|
|
38
|
+
} from "./overlay-state";
|
|
34
39
|
import { usageStillCalculating } from "./presentation";
|
|
35
40
|
import type { DashboardSnapshot, NotificationSnapshot, Task } from "./types";
|
|
36
41
|
import { NotificationCenter } from "./components/NotificationCenter";
|
|
@@ -105,6 +110,7 @@ export function DashboardApp({
|
|
|
105
110
|
selectedIdRef.current = selectedId;
|
|
106
111
|
const detailGeneration = useRef(0);
|
|
107
112
|
const detailListUpdatedAt = useRef<string | null>(null);
|
|
113
|
+
const detailListUsageSignature = useRef(listUsageSignature(null));
|
|
108
114
|
|
|
109
115
|
const applySnapshot = useCallback((snapshot: DashboardSnapshot) => {
|
|
110
116
|
setTasks(snapshot.tasks);
|
|
@@ -124,7 +130,9 @@ export function DashboardApp({
|
|
|
124
130
|
const currentSelectedId = selectedIdRef.current;
|
|
125
131
|
if (currentSelectedId) {
|
|
126
132
|
const updated = snapshot.tasks.find((task) => task.id === currentSelectedId);
|
|
127
|
-
if (updated)
|
|
133
|
+
if (updated) {
|
|
134
|
+
setSelectedTask((current) => current ? mergeListTaskIntoDetail(current, updated) : updated);
|
|
135
|
+
}
|
|
128
136
|
}
|
|
129
137
|
}, []);
|
|
130
138
|
|
|
@@ -147,7 +155,10 @@ export function DashboardApp({
|
|
|
147
155
|
const loadDetail = useCallback(async (task: Task, focusTurnRef: string | null = null) => {
|
|
148
156
|
const generation = ++detailGeneration.current;
|
|
149
157
|
detailListUpdatedAt.current = task.updatedAt;
|
|
150
|
-
|
|
158
|
+
detailListUsageSignature.current = listUsageSignature(task);
|
|
159
|
+
setSelectedTask((current) => current?.id === task.id
|
|
160
|
+
? mergeListTaskIntoDetail(current, task)
|
|
161
|
+
: task);
|
|
151
162
|
setDetailOpened(true);
|
|
152
163
|
setHighlightTurnRef(focusTurnRef);
|
|
153
164
|
setDetailError(null);
|
|
@@ -157,6 +168,8 @@ export function DashboardApp({
|
|
|
157
168
|
if (generation !== detailGeneration.current) return;
|
|
158
169
|
const detail = await taskDetail(task.id);
|
|
159
170
|
if (generation !== detailGeneration.current) return;
|
|
171
|
+
detailListUpdatedAt.current = detail.updatedAt;
|
|
172
|
+
detailListUsageSignature.current = listUsageSignature(detail);
|
|
160
173
|
setSelectedTask(detail);
|
|
161
174
|
if (!usageStillCalculating(detail) || attempt === 40) return;
|
|
162
175
|
await new Promise((resolve) => window.setTimeout(resolve, 1_500));
|
|
@@ -168,14 +181,20 @@ export function DashboardApp({
|
|
|
168
181
|
}
|
|
169
182
|
}, []);
|
|
170
183
|
|
|
184
|
+
const selectedListTask = tasks.find((task) => task.id === selectedId) ?? null;
|
|
185
|
+
const selectedListUsageSignature = listUsageSignature(selectedListTask);
|
|
186
|
+
|
|
171
187
|
useEffect(() => {
|
|
172
188
|
if (!detailOpened || !selectedId || !connect) return;
|
|
173
|
-
|
|
174
|
-
if (
|
|
175
|
-
|
|
176
|
-
|
|
189
|
+
if (!selectedListTask) return;
|
|
190
|
+
if (
|
|
191
|
+
selectedListTask.updatedAt === detailListUpdatedAt.current
|
|
192
|
+
&& selectedListUsageSignature === detailListUsageSignature.current
|
|
193
|
+
) return;
|
|
194
|
+
void loadDetail(selectedListTask, highlightTurnRef);
|
|
195
|
+
// Task or cached usage changes while detail is open refresh the full projection.
|
|
177
196
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
178
|
-
}, [
|
|
197
|
+
}, [selectedListTask?.updatedAt, selectedListUsageSignature]);
|
|
179
198
|
|
|
180
199
|
const projects = useMemo(() => [
|
|
181
200
|
{ label: "All projects", value: "" },
|
|
@@ -17,9 +17,14 @@ const notification: NotificationSnapshot = {
|
|
|
17
17
|
summary: "Review completed.",
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
afterEach(
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
cleanup();
|
|
22
|
+
vi.useRealTimers();
|
|
23
|
+
});
|
|
21
24
|
|
|
22
25
|
test("notification portal stays non-modal until the operator chooses an action", () => {
|
|
26
|
+
vi.useFakeTimers();
|
|
27
|
+
vi.setSystemTime(new Date("2026-08-30T12:10:00.000Z"));
|
|
23
28
|
const onOpen = vi.fn();
|
|
24
29
|
const onDismiss = vi.fn();
|
|
25
30
|
render(
|
|
@@ -7,6 +7,7 @@ import { LinkedText } from "./LinkedText";
|
|
|
7
7
|
import { RelativeTime } from "./RelativeTime";
|
|
8
8
|
import { ShimmerText } from "./ShimmerText";
|
|
9
9
|
import { StatusBadge } from "./StatusBadge";
|
|
10
|
+
import { TaskUsageSummary } from "./TaskUsageSummary";
|
|
10
11
|
|
|
11
12
|
export function TaskCard({
|
|
12
13
|
onOpenCodex,
|
|
@@ -22,14 +23,17 @@ export function TaskCard({
|
|
|
22
23
|
<Paper className="taskchef-task-row" component="article" p="md" radius="md" withBorder>
|
|
23
24
|
<Stack className="taskchef-task-main" gap="sm">
|
|
24
25
|
<Box>
|
|
25
|
-
<
|
|
26
|
+
<Box className="taskchef-card-heading">
|
|
26
27
|
<Title order={3} size="h5">
|
|
27
28
|
<button className="taskchef-title-button" onClick={() => onOpenDetail(task)} type="button">
|
|
28
29
|
{task.title}
|
|
29
30
|
</button>
|
|
30
31
|
</Title>
|
|
31
|
-
<
|
|
32
|
-
|
|
32
|
+
<Stack align="flex-end" className="taskchef-card-metadata" gap={4}>
|
|
33
|
+
<StatusBadge status={task.status} />
|
|
34
|
+
<TaskUsageSummary task={task} />
|
|
35
|
+
</Stack>
|
|
36
|
+
</Box>
|
|
33
37
|
<Text c="dimmed" mt={2} size="xs">{task.project.name}</Text>
|
|
34
38
|
</Box>
|
|
35
39
|
|
|
@@ -47,7 +51,7 @@ export function TaskCard({
|
|
|
47
51
|
</Box>
|
|
48
52
|
|
|
49
53
|
<GitHubLinks task={task} />
|
|
50
|
-
<Group gap="md" justify="space-between">
|
|
54
|
+
<Group className="taskchef-card-footer" gap="md" justify="space-between">
|
|
51
55
|
<RelativeTime label={`Updated time for ${task.title}`} value={task.meaningfulUpdatedAt ?? task.updatedAt} />
|
|
52
56
|
<Button
|
|
53
57
|
aria-label={`Open chat for ${task.title}`}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { MantineProvider } from "@mantine/core";
|
|
2
|
+
import { cleanup, render, screen } from "@testing-library/react";
|
|
3
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
4
|
+
import { fixtureTask } from "../fixtures";
|
|
5
|
+
import { TaskCard } from "./TaskCard";
|
|
6
|
+
import { TaskUsageSummary } from "./TaskUsageSummary";
|
|
7
|
+
|
|
8
|
+
afterEach(cleanup);
|
|
9
|
+
|
|
10
|
+
function renderUsage(task = fixtureTask()) {
|
|
11
|
+
return render(<MantineProvider><TaskUsageSummary task={task} /></MantineProvider>);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("task list usage summary", () => {
|
|
15
|
+
test("uses shared compact token and currency formatting with full-value accessibility", () => {
|
|
16
|
+
renderUsage(fixtureTask({
|
|
17
|
+
status: "completed",
|
|
18
|
+
usage: {
|
|
19
|
+
status: "available",
|
|
20
|
+
updatedAt: "2026-08-30T09:00:00.000Z",
|
|
21
|
+
task: {
|
|
22
|
+
totalTokens: 1_324_567,
|
|
23
|
+
estimatedCostUsd: 12.3449,
|
|
24
|
+
sourceUpdatedAt: "2026-08-30T09:00:00.000Z",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
}));
|
|
28
|
+
const summary = screen.getByText("1.32M tokens · est. $12.34");
|
|
29
|
+
expect(summary).toHaveAttribute("data-usage-state", "ready");
|
|
30
|
+
expect(summary).toHaveAccessibleName(/1,324,567 tokens; estimated cost \$12\.34/i);
|
|
31
|
+
expect(summary).toHaveAttribute("title", expect.stringContaining("unrounded estimate $12.3449"));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("states cost unavailability without replacing a known token total with zero", () => {
|
|
35
|
+
renderUsage(fixtureTask({
|
|
36
|
+
status: "completed",
|
|
37
|
+
usage: { status: "available", task: { totalTokens: 1_324_567, estimatedCostUsd: null } },
|
|
38
|
+
}));
|
|
39
|
+
expect(screen.getByText("1.32M tokens · cost unavailable")).toHaveAccessibleName(
|
|
40
|
+
/1,324,567 tokens; estimated cost unavailable/i,
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("keeps pending and calculating wording distinct and uses the restrained shimmer", () => {
|
|
45
|
+
const { rerender } = renderUsage();
|
|
46
|
+
expect(screen.getByText("Token usage pending")).toHaveClass("taskchef-shimmer");
|
|
47
|
+
rerender(
|
|
48
|
+
<MantineProvider>
|
|
49
|
+
<TaskUsageSummary task={fixtureTask({
|
|
50
|
+
status: "completed",
|
|
51
|
+
usage: { generationTurnRef: "turn-one", status: "calculating" },
|
|
52
|
+
})} />
|
|
53
|
+
</MantineProvider>,
|
|
54
|
+
);
|
|
55
|
+
expect(screen.getByText("Calculating token usage")).toHaveClass("taskchef-shimmer");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("shows a quiet explicit unavailable state with the cache reason in its tooltip", () => {
|
|
59
|
+
renderUsage(fixtureTask({
|
|
60
|
+
status: "completed",
|
|
61
|
+
usage: { status: "unavailable", reason: "No matching cached boundary." },
|
|
62
|
+
}));
|
|
63
|
+
const summary = screen.getByText("Token usage unavailable");
|
|
64
|
+
expect(summary).toHaveAttribute("data-usage-state", "unavailable");
|
|
65
|
+
expect(summary).toHaveAttribute("title", "No matching cached boundary.");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("places usage directly beneath the primary status badge in a responsive metadata block", () => {
|
|
69
|
+
const { container } = render(
|
|
70
|
+
<MantineProvider>
|
|
71
|
+
<TaskCard
|
|
72
|
+
onOpenCodex={vi.fn()}
|
|
73
|
+
onOpenDetail={vi.fn()}
|
|
74
|
+
task={fixtureTask()}
|
|
75
|
+
/>
|
|
76
|
+
</MantineProvider>,
|
|
77
|
+
);
|
|
78
|
+
const metadata = container.querySelector(".taskchef-card-metadata");
|
|
79
|
+
expect(metadata).not.toBeNull();
|
|
80
|
+
expect(metadata?.children[0]).toHaveClass("taskchef-status-badge");
|
|
81
|
+
expect(metadata?.children[1]).toHaveClass("taskchef-task-usage");
|
|
82
|
+
expect(screen.getByRole("button", { name: /^review checkout reconciliation$/i })).toBeVisible();
|
|
83
|
+
expect(screen.getByRole("button", { name: /open chat/i })).toBeVisible();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Text } from "@mantine/core";
|
|
2
|
+
import type { Task } from "../types";
|
|
3
|
+
import { listUsageView } from "../presentation";
|
|
4
|
+
import { ShimmerText } from "./ShimmerText";
|
|
5
|
+
|
|
6
|
+
export function TaskUsageSummary({ task }: { task: Task }) {
|
|
7
|
+
const usage = listUsageView(task);
|
|
8
|
+
const content = usage.kind === "pending" || usage.kind === "calculating"
|
|
9
|
+
? <ShimmerText>{usage.label}</ShimmerText>
|
|
10
|
+
: usage.label;
|
|
11
|
+
return (
|
|
12
|
+
<Text
|
|
13
|
+
aria-label={usage.accessibleLabel}
|
|
14
|
+
aria-live="polite"
|
|
15
|
+
c="dimmed"
|
|
16
|
+
className="taskchef-task-usage"
|
|
17
|
+
data-usage-state={usage.kind}
|
|
18
|
+
size="xs"
|
|
19
|
+
title={usage.title}
|
|
20
|
+
>
|
|
21
|
+
{content}
|
|
22
|
+
</Text>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { fixtureTask } from "./fixtures";
|
|
2
3
|
import type { NotificationSnapshot } from "./types";
|
|
3
4
|
import {
|
|
4
5
|
clearNotificationHistory,
|
|
6
|
+
listUsageSignature,
|
|
7
|
+
mergeListTaskIntoDetail,
|
|
5
8
|
notificationAppeared,
|
|
6
9
|
notificationClicked,
|
|
7
10
|
type OverlayState,
|
|
@@ -76,4 +79,32 @@ describe("overlay coordination", () => {
|
|
|
76
79
|
const current = state({ detailTaskId: "task-one", detailOpened: true, notifications: [taskUpdate] });
|
|
77
80
|
expect(clearNotificationHistory(current)).toEqual({ ...current, notifications: [] });
|
|
78
81
|
});
|
|
82
|
+
|
|
83
|
+
test("a cache-only list update preserves rich detail usage and requests a refresh", () => {
|
|
84
|
+
const detail = fixtureTask({
|
|
85
|
+
usage: {
|
|
86
|
+
generationTurnRef: "turn-one",
|
|
87
|
+
status: "available",
|
|
88
|
+
task: { totalTokens: 1200, estimatedCostUsd: 0.24, models: { "gpt-5": {} } },
|
|
89
|
+
turns: { "turn-one": { status: "available", totalTokens: 1200 } },
|
|
90
|
+
updatedAt: "2026-08-30T08:00:00.000Z",
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
const list = fixtureTask({
|
|
94
|
+
turns: undefined,
|
|
95
|
+
results: undefined,
|
|
96
|
+
usage: {
|
|
97
|
+
generationTurnRef: "turn-one",
|
|
98
|
+
status: "available",
|
|
99
|
+
task: { totalTokens: 1800, estimatedCostUsd: 0.36 },
|
|
100
|
+
updatedAt: "2026-08-30T08:01:00.000Z",
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const merged = mergeListTaskIntoDetail(detail, list);
|
|
105
|
+
expect(merged.usage).toBe(detail.usage);
|
|
106
|
+
expect(merged.turns).toBe(detail.turns);
|
|
107
|
+
expect(merged.results).toBe(detail.results);
|
|
108
|
+
expect(listUsageSignature(list)).not.toBe(listUsageSignature(detail));
|
|
109
|
+
});
|
|
79
110
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { NotificationSnapshot } from "./types";
|
|
1
|
+
import type { NotificationSnapshot, Task } from "./types";
|
|
2
2
|
|
|
3
3
|
export interface OverlayState {
|
|
4
4
|
detailTaskId: string | null;
|
|
@@ -36,3 +36,25 @@ export function notificationClicked(
|
|
|
36
36
|
export function clearNotificationHistory(state: OverlayState): OverlayState {
|
|
37
37
|
return { ...state, notifications: [] };
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
export function mergeListTaskIntoDetail(current: Task, updated: Task): Task {
|
|
41
|
+
const currentIsDetail = Array.isArray(current.turns) && Array.isArray(current.results);
|
|
42
|
+
const updatedIsList = !Array.isArray(updated.turns) && !Array.isArray(updated.results);
|
|
43
|
+
return {
|
|
44
|
+
...current,
|
|
45
|
+
...updated,
|
|
46
|
+
...(currentIsDetail && updatedIsList
|
|
47
|
+
? { results: current.results, turns: current.turns, usage: current.usage }
|
|
48
|
+
: {}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function listUsageSignature(task: Task | null | undefined): string {
|
|
53
|
+
return JSON.stringify([
|
|
54
|
+
task?.usage?.generationTurnRef ?? null,
|
|
55
|
+
task?.usage?.status ?? null,
|
|
56
|
+
task?.usage?.updatedAt ?? null,
|
|
57
|
+
task?.usage?.task?.totalTokens ?? null,
|
|
58
|
+
task?.usage?.task?.estimatedCostUsd ?? null,
|
|
59
|
+
]);
|
|
60
|
+
}
|
|
@@ -89,6 +89,53 @@ export function usageStillCalculating(task: Task) {
|
|
|
89
89
|
|| Object.values(task.usage?.turns ?? {}).some((turn) => turn.status === "calculating"));
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
export type ListUsageView = {
|
|
93
|
+
accessibleLabel: string;
|
|
94
|
+
kind: "pending" | "calculating" | "ready" | "unavailable";
|
|
95
|
+
label: string;
|
|
96
|
+
title: string;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export function listUsageView(task: Task): ListUsageView {
|
|
100
|
+
const view = usageView(task);
|
|
101
|
+
if (view.kind === "pending" || view.kind === "calculating") {
|
|
102
|
+
return {
|
|
103
|
+
accessibleLabel: view.label,
|
|
104
|
+
kind: view.kind,
|
|
105
|
+
label: view.label,
|
|
106
|
+
title: view.label,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (view.kind === "unavailable") {
|
|
110
|
+
return {
|
|
111
|
+
accessibleLabel: `Token usage unavailable${view.label === "Token usage unavailable" ? "" : `: ${view.label}`}`,
|
|
112
|
+
kind: "unavailable",
|
|
113
|
+
label: "Token usage unavailable",
|
|
114
|
+
title: view.label,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const compactTokens = formatCompactTokens(view.usage.totalTokens);
|
|
118
|
+
const fullTokens = formatFullTokens(view.usage.totalTokens);
|
|
119
|
+
const cost = view.usage.estimatedCostUsd == null
|
|
120
|
+
? "cost unavailable"
|
|
121
|
+
: `est. ${formatEstimatedCost(view.usage.estimatedCostUsd)}`;
|
|
122
|
+
const qualifier = view.knownSoFar ? " · known so far" : "";
|
|
123
|
+
const freshness = view.usage.sourceUpdatedAt ?? view.usage.sampledAt ?? task.usage?.updatedAt ?? null;
|
|
124
|
+
const freshnessLabel = freshness && !Number.isNaN(Date.parse(freshness))
|
|
125
|
+
? ` Cached usage updated ${new Date(freshness).toLocaleString()}.`
|
|
126
|
+
: "";
|
|
127
|
+
return {
|
|
128
|
+
accessibleLabel: `${fullTokens} tokens; ${view.usage.estimatedCostUsd == null
|
|
129
|
+
? "estimated cost unavailable"
|
|
130
|
+
: `estimated cost ${formatEstimatedCost(view.usage.estimatedCostUsd)}`}${view.knownSoFar ? "; known so far" : ""}.${freshnessLabel}`,
|
|
131
|
+
kind: "ready",
|
|
132
|
+
label: `${compactTokens} tokens · ${cost}${qualifier}`,
|
|
133
|
+
title: `${fullTokens} tokens · ${view.usage.estimatedCostUsd == null
|
|
134
|
+
? "estimated cost unavailable"
|
|
135
|
+
: `unrounded estimate $${view.usage.estimatedCostUsd}`}${view.knownSoFar ? " · known so far" : ""}.${freshnessLabel}`,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
92
139
|
export function statusColor(status: Task["status"] | "interrupted") {
|
|
93
140
|
if (status === "completed") return "teal";
|
|
94
141
|
if (status === "needs_input") return "yellow";
|
|
@@ -33,7 +33,10 @@ body {
|
|
|
33
33
|
.taskchef-task-row { background: light-dark(#fff, #202421); box-shadow: 0 1px 0 light-dark(rgb(22 31 25 / 3%), transparent); }
|
|
34
34
|
.taskchef-task-row:hover { border-color: light-dark(var(--mantine-color-teal-4), var(--mantine-color-teal-8)); }
|
|
35
35
|
.taskchef-task-main { min-width: 0; flex: 1; }
|
|
36
|
+
.taskchef-card-heading { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: start; gap: var(--mantine-spacing-sm); }
|
|
37
|
+
.taskchef-card-metadata { width: max-content; min-width: 0; max-width: min(25rem, 45vw); }
|
|
36
38
|
.taskchef-status-badge { flex: none; }
|
|
39
|
+
.taskchef-task-usage { max-width: 100%; text-align: right; line-height: 1.25; overflow-wrap: anywhere; }
|
|
37
40
|
.taskchef-title-button {
|
|
38
41
|
all: unset;
|
|
39
42
|
border-radius: 3px;
|
|
@@ -125,4 +128,8 @@ body {
|
|
|
125
128
|
@media (max-width: 34em) {
|
|
126
129
|
.taskchef-header { padding-top: 22px; padding-bottom: 18px; }
|
|
127
130
|
.taskchef-brand-icon, .taskchef-brand-icon img { width: 36px; height: 36px; }
|
|
131
|
+
.taskchef-card-heading { grid-template-columns: minmax(0, 1fr); }
|
|
132
|
+
.taskchef-card-metadata { align-items: flex-start; width: 100%; max-width: 100%; }
|
|
133
|
+
.taskchef-task-usage { text-align: left; }
|
|
134
|
+
.taskchef-card-footer { align-items: flex-start; flex-direction: column; }
|
|
128
135
|
}
|
|
@@ -41,6 +41,8 @@ export interface UsageNumbers {
|
|
|
41
41
|
reasoningOutputTokens?: number;
|
|
42
42
|
totalTokens: number;
|
|
43
43
|
estimatedCostUsd?: number | null;
|
|
44
|
+
sampledAt?: string | null;
|
|
45
|
+
sourceUpdatedAt?: string | null;
|
|
44
46
|
models?: Record<string, unknown>;
|
|
45
47
|
}
|
|
46
48
|
|
|
@@ -54,6 +56,7 @@ export interface UsageProjection {
|
|
|
54
56
|
}>;
|
|
55
57
|
reason?: string;
|
|
56
58
|
message?: string;
|
|
59
|
+
updatedAt?: string | null;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
export interface Task {
|