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.
@@ -36,8 +36,10 @@ export function ActivityTimeline({ highlightTurnRef, task }: { highlightTurnRef:
36
36
  withBorder
37
37
  >
38
38
  <Stack gap={7}>
39
- <StatusBadge status={presentation.status} />
40
- {presentation.sourceLabel && <Text c="dimmed" size="xs">{presentation.sourceLabel}</Text>}
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={usage.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={usage.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={elapsed.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 metric details to keyboard users on focus", () => {
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("Unrounded estimate: $12.3449");
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 · Updating…");
96
- expect(screen.getByLabelText(/estimated cost \$12\.34; updating/i)).toHaveTextContent("12.34 · Updating…");
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={reportedWork.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={usage.tokens.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={usage.cost.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: 330, estimatedCostUsd: 0.12 } },
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("330 tokens")).toHaveTextContent("330");
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: ready?.knownSoFar ? "Tokens · known so far" : "Tokens",
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" || usage.kind === "calculating",
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: "Reported work",
62
+ label: "Duration",
64
63
  title: reportedWork.title,
65
64
  value: reportedWork.value,
66
65
  },
67
- ...(ready ? [
68
- {
69
- accessibleValue: models || "Model unavailable",
70
- label: "Model",
71
- title: models || "Model unavailable",
72
- value: models || "n/a",
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("Reported work").nextSibling).toHaveTextContent("18m 32s");
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("Model").nextSibling).toHaveTextContent("gpt-5.6-sol, gpt-5.6-luna");
145
- expect(screen.getByText("Cache ratio").nextSibling).toHaveTextContent("67%");
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("Model").nextSibling).toHaveTextContent("n/a");
165
- expect(screen.getByLabelText("Model unavailable")).toBeVisible();
166
- expect(screen.getByText("Cache ratio").nextSibling).toHaveTextContent("n/a");
167
- expect(screen.getByLabelText("Cache ratio unavailable")).toBeVisible();
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("Reported work").nextSibling).toHaveTextContent("n/a");
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("Reported work").nextSibling).toHaveTextContent("n/a");
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 tokens = typeof usage?.totalTokens === "number" ? formatFullTokens(usage.totalTokens) : NOT_AVAILABLE;
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: `${tokens} tokens`, value: tokens },
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 qualifier = view.knownSoFar ? " · Updating…" : "";
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"}${accessibleQualifier}.${freshnessLabel}`,
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}`}${qualifier}.${freshnessLabel}`,
316
- value: `${formattedCostAmount ?? NOT_AVAILABLE}${qualifier}`,
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
- title: `${fullTokens} tokens${qualifier}.${freshnessLabel}`,
322
- value: `${compactTokens} tokens${qualifier}`,
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 { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: start; gap: var(--mantine-spacing-sm); }
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(3, minmax(0, 1fr)); }
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
 
@@ -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}${detail}`
36
- : `${hourText}${detail} ago`;
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
- return new Intl.DateTimeFormat(locale, {
67
- dateStyle: "medium",
68
- timeZone,
69
- }).format(parsed.date);
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, {