taskchef 8.2.0 → 8.2.1

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">
@@ -83,7 +85,6 @@ export function ActivityTimeline({ highlightTurnRef, task }: { highlightTurnRef:
83
85
  </StatCell>
84
86
  </StatsGrid>
85
87
  {usage.note && <Text c="dimmed" size="xs">{usage.note}</Text>}
86
- <Text c="dimmed" className="taskchef-mono" size="xs">Turn ref: {identity ?? "—"}</Text>
87
88
  </Stack>
88
89
  </Paper>
89
90
  );
@@ -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", () => {
@@ -33,7 +33,7 @@ export function TaskCardStats({ task }: { task: Task }) {
33
33
  <StatCell usageState={usage.kind}>
34
34
  <StaticStat
35
35
  accessibleLabel={usage.tokens.accessibleLabel}
36
- animated={usage.animated}
36
+ animated={usage.tokens.animated}
37
37
  icon={<IconStack2 aria-hidden size={14} />}
38
38
  title={usage.tokens.title}
39
39
  value={usage.tokens.value}
@@ -45,7 +45,7 @@ export function TaskCardStats({ task }: { task: Task }) {
45
45
  <StatCell usageState={usage.kind}>
46
46
  <StaticStat
47
47
  accessibleLabel={usage.cost.accessibleLabel}
48
- animated={usage.animated}
48
+ animated={usage.cost.animated}
49
49
  icon={<IconCurrencyDollar aria-hidden size={14} />}
50
50
  title={usage.cost.title}
51
51
  value={usage.cost.value}
@@ -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("$");
@@ -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: {
@@ -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 {
@@ -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, {