taskchef 8.2.1 → 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.
@@ -59,6 +59,7 @@ export function ActivityTimeline({ highlightTurnRef, task }: { highlightTurnRef:
59
59
  <RelativeTime
60
60
  icon={<IconHistory aria-hidden size={14} />}
61
61
  label="Turn update time"
62
+ tooltipLabel="Updated at"
62
63
  value={presentation.updatedAt}
63
64
  />
64
65
  </StatCell>
@@ -70,7 +71,7 @@ export function ActivityTimeline({ highlightTurnRef, task }: { highlightTurnRef:
70
71
  accessibleLabel={usage.tokens.accessibleLabel}
71
72
  animated={usage.animated}
72
73
  icon={<IconStack2 aria-hidden size={14} />}
73
- title={usage.title}
74
+ title="Tokens"
74
75
  value={usage.tokens.value}
75
76
  />
76
77
  </StatCell>
@@ -79,7 +80,7 @@ export function ActivityTimeline({ highlightTurnRef, task }: { highlightTurnRef:
79
80
  accessibleLabel={usage.cost.accessibleLabel}
80
81
  animated={usage.animated}
81
82
  icon={<IconCurrencyDollar aria-hidden size={14} />}
82
- title={usage.title}
83
+ title="Estimated cost"
83
84
  value={usage.cost.value}
84
85
  />
85
86
  </StatCell>
@@ -100,7 +101,7 @@ function TurnReportedWork({ turn }: { turn: TaskTurn }) {
100
101
  <StaticStat
101
102
  accessibleLabel={elapsed.accessibleLabel}
102
103
  icon={<IconHourglass aria-hidden size={14} />}
103
- title={elapsed.title}
104
+ title="Duration"
104
105
  value={elapsed.value}
105
106
  />
106
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
 
@@ -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,7 +27,7 @@ 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>
@@ -35,7 +36,7 @@ export function TaskCardStats({ task }: { task: Task }) {
35
36
  accessibleLabel={usage.tokens.accessibleLabel}
36
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">
@@ -47,7 +48,7 @@ export function TaskCardStats({ task }: { task: Task }) {
47
48
  accessibleLabel={usage.cost.accessibleLabel}
48
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>
@@ -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">
@@ -154,10 +154,11 @@ describe("token and working presentation", () => {
154
154
 
155
155
  expect(screen.getByText("Tokens").nextSibling).toHaveTextContent("330");
156
156
  expect(screen.getByText("Estimated cost").nextSibling).toHaveTextContent("$0.12");
157
- expect(screen.getByText("Reported work").nextSibling).toHaveTextContent("18m 32s");
157
+ expect(screen.getByText("Duration").nextSibling).toHaveTextContent("18m 32s");
158
158
  expect(screen.getByLabelText(/reported work 18m 32s.*wall-clock/i)).toBeVisible();
159
- expect(screen.getByText("Model").nextSibling).toHaveTextContent("gpt-5.6-sol, gpt-5.6-luna");
160
- 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);
161
162
  expect(screen.queryByText(/API-equivalent/i)).not.toBeInTheDocument();
162
163
  });
163
164
 
@@ -176,10 +177,38 @@ describe("token and working presentation", () => {
176
177
 
177
178
  expect(screen.getByText("Estimated cost").nextSibling).toHaveTextContent("n/a");
178
179
  expect(screen.getByLabelText("Estimated cost unavailable")).toBeVisible();
179
- expect(screen.getByText("Model").nextSibling).toHaveTextContent("n/a");
180
- expect(screen.getByLabelText("Model unavailable")).toBeVisible();
181
- expect(screen.getByText("Cache ratio").nextSibling).toHaveTextContent("n/a");
182
- 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);
183
212
  });
184
213
 
185
214
  test("keeps reported work visible across pending, calculating, and unavailable usage", () => {
@@ -200,7 +229,9 @@ describe("token and working presentation", () => {
200
229
  expect(screen.getByText("Tokens").nextSibling).toHaveTextContent(expected);
201
230
  expect(screen.getByText("Estimated cost").nextSibling).toHaveTextContent(expected);
202
231
  expect(screen.getByLabelText(accessibleCost)).toBeVisible();
203
- 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);
204
235
  cleanup();
205
236
  }
206
237
  });
@@ -458,7 +489,7 @@ describe("reported wall-clock work presentation", () => {
458
489
  reportedWork: { terminalTurns: 1, validTurns: 1, totalMilliseconds: 1_112_000 },
459
490
  })).toMatchObject({ kind: "available", value: "18m 32s" });
460
491
  render(<MantineProvider><UsagePanel task={compactTerminal} /></MantineProvider>);
461
- expect(screen.getByText("Reported work").nextSibling).toHaveTextContent("n/a");
492
+ expect(screen.getByText("Duration").nextSibling).toHaveTextContent("n/a");
462
493
  cleanup();
463
494
  }
464
495
 
@@ -125,7 +125,7 @@ body {
125
125
  .taskchef-toolbar .mantine-SegmentedControl-root { overflow-x: auto; }
126
126
  .taskchef-notifications { right: 16px; bottom: 16px; width: min(330px, calc(100vw - 32px)); }
127
127
  .taskchef-metadata { grid-template-columns: 1fr; }
128
- .taskchef-metrics { grid-template-columns: repeat(3, minmax(0, 1fr)); }
128
+ .taskchef-metrics { grid-template-columns: repeat(4, minmax(0, 1fr)); }
129
129
  .taskchef-metadata dt { padding-bottom: 0; border-bottom: 0; }
130
130
  }
131
131