taskchef 9.7.0 → 9.7.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.
@@ -99,7 +99,7 @@ export function UsageView({ request, refreshKey = 0 }: { request: (args: ReturnT
99
99
  {error && <div role="alert"><p>Usage could not be loaded: {error}</p><button onClick={() => setRetry(value => value + 1)}>Retry</button></div>}
100
100
  {data && <>
101
101
  {data.refreshError && <p role="alert">{data.refreshError} <button onClick={() => setRetry(value => value + 1)}>Retry</button></p>}
102
- <p className="usage-period">{dateLabel(data.start)} – {dateLabel(data.end)} · {data.timeZone} · Daily · Includes archived chats and subagent usage{data.updatedAt && <> · Updated {new Date(data.updatedAt).toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" })}</>}</p>
102
+ <p className="usage-period">{dateLabel(data.start)} – {dateLabel(data.end)} · {data.timeZone} · Daily · All chat sources · Includes archived chats and subagent usage{data.updatedAt && <> · Updated {new Date(data.updatedAt).toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" })}</>}</p>
103
103
  <div className="usage-totals">
104
104
  <article><h3>Recorded tokens</h3><strong>{count(data.totals.tokens)}</strong><p>Input {count(data.totals.inputTokens)} · Cached input {count(data.totals.cachedInputTokens)} · Output {count(data.totals.outputTokens)}</p></article>
105
105
  <article><h3>Estimated cost</h3><strong>{money(data.totals.costUsd)}</strong><p>API-equivalent prices · USD</p></article>
@@ -77,7 +77,7 @@ beforeEach(() => {
77
77
  transition = async () => ({ structuredContent: { task: task("one", "completed") } });
78
78
  detailFailure = null;
79
79
  server.call.mockImplementation(({ name, arguments: args }) => {
80
- if (name === "taskchef_app_snapshot") return Promise.resolve({ structuredContent: { notifications: notificationState, settingsUrl: "codex://plugins/taskchef-next?marketplacePath=%2Fexample", settings: visibility, snapshot: { tasks, projects: registeredProjects, healthy: true } } });
80
+ if (name === "taskchef_app_snapshot") return Promise.resolve({ structuredContent: { notifications: notificationState, settingsUrl: "codex://plugins/taskchef-dev?marketplacePath=%2Fexample", settings: visibility, snapshot: { tasks, projects: registeredProjects, healthy: true } } });
81
81
  if (name === "taskchef_app_task" && detailFailure) return Promise.resolve({ isError: true, content: [{ type: "text", text: detailFailure }] });
82
82
  if (name === "taskchef_app_task") return Promise.resolve({ structuredContent: { task: details.get(args.taskId as string) } });
83
83
  if (name === "taskchef_app_notifications") {
@@ -242,7 +242,7 @@ test("an older selection detail cannot replace a newer refresh detail", async ()
242
242
  const first = new Promise((resolve) => { releaseFirst = resolve; });
243
243
  let detailCalls = 0;
244
244
  server.call.mockImplementation(({ name }) => {
245
- if (name === "taskchef_app_snapshot") return Promise.resolve({ structuredContent: { notifications: notificationState, settingsUrl: "codex://plugins/taskchef-next?marketplacePath=%2Fexample", settings: visibility, snapshot: { tasks, projects: registeredProjects, healthy: true } } });
245
+ if (name === "taskchef_app_snapshot") return Promise.resolve({ structuredContent: { notifications: notificationState, settingsUrl: "codex://plugins/taskchef-dev?marketplacePath=%2Fexample", settings: visibility, snapshot: { tasks, projects: registeredProjects, healthy: true } } });
246
246
  if (name === "taskchef_app_task") return ++detailCalls === 1
247
247
  ? first
248
248
  : Promise.resolve({ structuredContent: { task: task("one", "needs_input") } });
@@ -272,7 +272,7 @@ test("an older selection response cannot replace the current task", async () =>
272
272
  let release!: (value: unknown) => void;
273
273
  const first = new Promise((resolve) => { release = resolve; });
274
274
  server.call.mockImplementation(({ name, arguments: args }) => {
275
- if (name === "taskchef_app_snapshot") return Promise.resolve({ structuredContent: { notifications: notificationState, settingsUrl: "codex://plugins/taskchef-next?marketplacePath=%2Fexample", settings: visibility, snapshot: { tasks, projects: registeredProjects, healthy: true } } });
275
+ if (name === "taskchef_app_snapshot") return Promise.resolve({ structuredContent: { notifications: notificationState, settingsUrl: "codex://plugins/taskchef-dev?marketplacePath=%2Fexample", settings: visibility, snapshot: { tasks, projects: registeredProjects, healthy: true } } });
276
276
  if (name === "taskchef_app_task") return args.taskId === "one" ? first : Promise.resolve({ structuredContent: { task: task("two") } });
277
277
  throw new Error(`Unexpected tool: ${name}`);
278
278
  });
@@ -286,7 +286,7 @@ test("an older selection response cannot replace the current task", async () =>
286
286
 
287
287
  test("keeps scan diagnostics out of the board", async () => {
288
288
  server.call.mockImplementation(({ name }) => {
289
- if (name === "taskchef_app_snapshot") return Promise.resolve({ structuredContent: { notifications: notificationState, settingsUrl: "codex://plugins/taskchef-next?marketplacePath=%2Fexample", settings: visibility, snapshot: {
289
+ if (name === "taskchef_app_snapshot") return Promise.resolve({ structuredContent: { notifications: notificationState, settingsUrl: "codex://plugins/taskchef-dev?marketplacePath=%2Fexample", settings: visibility, snapshot: {
290
290
  tasks: [], healthy: true, revision: 1,
291
291
  scan: { source: "database", mode: "database", checkedAt: "2026-10-05T00:00:00Z", intervalSeconds: 5,
292
292
  indexedFiles: 8620, visibleFiles: 300, unreadFiles: 8320, errors: 0 },
@@ -303,7 +303,7 @@ test("keeps scan diagnostics out of the board", async () => {
303
303
 
304
304
  test("shows an initial inventory failure without undefined scan counts", async () => {
305
305
  server.call.mockImplementation(({ name }) => {
306
- if (name === "taskchef_app_snapshot") return Promise.resolve({ structuredContent: { notifications: notificationState, settingsUrl: "codex://plugins/taskchef-next?marketplacePath=%2Fexample", settings: visibility, snapshot: {
306
+ if (name === "taskchef_app_snapshot") return Promise.resolve({ structuredContent: { notifications: notificationState, settingsUrl: "codex://plugins/taskchef-dev?marketplacePath=%2Fexample", settings: visibility, snapshot: {
307
307
  tasks: [], healthy: false, revision: 0,
308
308
  scan: { mode: "error", checkedAt: "2026-10-05T00:00:00Z", error: "EACCES" },
309
309
  } } });
@@ -351,7 +351,7 @@ test("inline mode loads once and begins polling when expanded", async () => {
351
351
  });
352
352
 
353
353
  test("shows the Node requirement when the scanner rejects its runtime", async () => {
354
- server.call.mockResolvedValue({ structuredContent: { notifications: notificationState, settingsUrl: "codex://plugins/taskchef-next?marketplacePath=%2Fexample", settings: visibility, snapshot: {
354
+ server.call.mockResolvedValue({ structuredContent: { notifications: notificationState, settingsUrl: "codex://plugins/taskchef-dev?marketplacePath=%2Fexample", settings: visibility, snapshot: {
355
355
  tasks: [], healthy: false, revision: 0,
356
356
  scan: { mode: "error", checkedAt: "2026-10-05T00:00:00Z", error: "TaskChef requires Node.js 22.18.0 or later for read-only SQLite (current: 22.17.9)." },
357
357
  } } });
@@ -363,7 +363,7 @@ test("a database failure after a healthy snapshot removes stale tasks", async ()
363
363
  mount();
364
364
  expect(await screen.findByRole("button", { name: "Task one" })).toBeVisible();
365
365
  server.call.mockImplementation(({ name }) => {
366
- if (name === "taskchef_app_snapshot") return Promise.resolve({ structuredContent: { notifications: notificationState, settingsUrl: "codex://plugins/taskchef-next?marketplacePath=%2Fexample", settings: visibility, snapshot: {
366
+ if (name === "taskchef_app_snapshot") return Promise.resolve({ structuredContent: { notifications: notificationState, settingsUrl: "codex://plugins/taskchef-dev?marketplacePath=%2Fexample", settings: visibility, snapshot: {
367
367
  tasks: [], healthy: false, revision: 2,
368
368
  scan: { source: "database", mode: "error", checkedAt: new Date().toISOString() },
369
369
  } } });
@@ -384,7 +384,7 @@ test("Done control calls the local mark tool with the displayed turn and refresh
384
384
  tasks = [next]; details.set(current.id, next);
385
385
  return { structuredContent: { task: next } };
386
386
  }
387
- if (name === "taskchef_app_snapshot") return { structuredContent: { notifications: notificationState, settingsUrl: "codex://plugins/taskchef-next?marketplacePath=%2Fexample", settings: visibility, snapshot: { tasks, projects: registeredProjects, healthy: true } } };
387
+ if (name === "taskchef_app_snapshot") return { structuredContent: { notifications: notificationState, settingsUrl: "codex://plugins/taskchef-dev?marketplacePath=%2Fexample", settings: visibility, snapshot: { tasks, projects: registeredProjects, healthy: true } } };
388
388
  if (name === "taskchef_app_task") return { structuredContent: { task: details.get(current.id) } };
389
389
  throw new Error(name);
390
390
  });
@@ -682,6 +682,25 @@ test("Usage opens on demand and returns to Board without keeping its filters vis
682
682
  expect(screen.getByRole("region",{name:"Task board"})).toBeVisible();
683
683
  });
684
684
 
685
+ test('Usage keeps its project selection when Board source visibility changes', async () => {
686
+ const original = server.call.getMockImplementation()!;
687
+ const usage = { start:'2026-10-15', end:'2026-10-15', timeZone:'UTC', samples:1, incomplete:false,
688
+ totals:{tokens:100,costUsd:2,inputTokens:80,cachedInputTokens:20,outputTokens:20},
689
+ days:[{date:'2026-10-15',tokens:100,costUsd:2,projects:{alpha:2},projectTokens:{alpha:100}}],
690
+ projects:[{id:'alpha',name:'Alpha',tokens:100,costUsd:2}] };
691
+ server.call.mockImplementation(input => input.name === 'taskchef_app_usage' ? Promise.resolve({structuredContent:usage}) : original(input));
692
+ mount(); await screen.findByRole('button',{name:'Task one'});
693
+ fireEvent.click(screen.getByRole('radio',{name:'Usage'}));
694
+ const alpha = await screen.findByRole('button',{name:'Alpha'});
695
+ fireEvent.click(alpha);
696
+ expect(alpha).toHaveAttribute('aria-pressed','true');
697
+ visibility = {...visibility,showCli:true,showExec:true};
698
+ fireEvent.click(screen.getByRole('button',{name:'Refresh'}));
699
+ await waitFor(()=>expect(server.call.mock.calls.filter(([input])=>input.name==='taskchef_app_usage').length).toBeGreaterThan(1));
700
+ expect(screen.getByRole('button',{name:'Alpha'})).toHaveAttribute('aria-pressed','true');
701
+ expect(screen.getByText(/All chat sources/)).toBeVisible();
702
+ });
703
+
685
704
  test("Calendar replaces the update window and opens matching chat details", async () => {
686
705
  tasks = [task('one', 'needs_input', '2020-01-01T00:00:00Z')];
687
706
  details = new Map(tasks.map(item => [item.id, item]));
@@ -461,7 +461,7 @@ export function TaskChefApp() {
461
461
  {view === "list" && <Paper className="taskchef-toolbar" radius={0}>
462
462
  <Box className="taskchef-app-status"><SegmentedControl aria-label="Status" data={statusOptions} onChange={setStatus} size="xs" value={status} withItemsBorders={false} /></Box>
463
463
  </Paper>}
464
- {view === "calendar" ? <CalendarView request={readCalendar} showProject={!project} tasks={boardTasks} refreshKey={`${revisionRef.current}:${calendarHistoryRevision.current}:${new Date(now).toDateString()}:${usageRefreshKey}:${showCli}:${showExec}:${showArchived}`} onOpenDetail={task => void select(task)} /> : view === "usage" ? <UsageView key={`${showCli}:${showExec}`} request={readUsage} refreshKey={usageRefreshKey} /> : <>
464
+ {view === "calendar" ? <CalendarView request={readCalendar} showProject={!project} tasks={boardTasks} refreshKey={`${revisionRef.current}:${calendarHistoryRevision.current}:${new Date(now).toDateString()}:${usageRefreshKey}:${showCli}:${showExec}:${showArchived}`} onOpenDetail={task => void select(task)} /> : view === "usage" ? <UsageView request={readUsage} refreshKey={usageRefreshKey} /> : <>
465
465
  {!!scan?.scheduleErrors && <Alert color="yellow" role="alert">{scan.scheduleErrors} schedule files could not be read; schedule placement may be incomplete.</Alert>}
466
466
  {view === "list" && <Text aria-live="polite" className="taskchef-results-summary" id="task-results-summary">Tasks: {visible.length} of {eligibleTasks.length}</Text>}
467
467
  {error && <Alert color="red" role="alert" mt="sm">{error}</Alert>}
package/src/mcp-app.js CHANGED
@@ -229,10 +229,10 @@ export function registerTaskChefApp(server, {
229
229
  annotations: { readOnlyHint: true, openWorldHint: false },
230
230
  }, async ({ range, timeZone, force, rescan }) => {
231
231
  new Intl.DateTimeFormat("en", { timeZone }).format();
232
- const settings = await readSettings();
233
232
  // History is independent of chart range and timezone. Range changes query
234
233
  // this shared snapshot; only expiry or explicit Refresh starts another read.
235
- const key = JSON.stringify([scanner.codexHome ?? process.env.CODEX_HOME ?? join(homedir(), ".codex"), settings.showCli, settings.showExec]);
234
+ // The new scope cannot hydrate an older, visibility-filtered snapshot.
235
+ const key = JSON.stringify([scanner.codexHome ?? process.env.CODEX_HOME ?? join(homedir(), ".codex"), "all-sources"]);
236
236
  const cacheDirectory = join(dirname(settingsPath), "cache", "usage-history");
237
237
  let job = analyticsJobs.get(key);
238
238
  if (!job) {
@@ -256,7 +256,7 @@ export function registerTaskChefApp(server, {
256
256
  current.refreshAgain = false;
257
257
  const rebuild = current.rescan;
258
258
  current.rescan = false;
259
- const families = await scanner.usageHistory({ settings, rescan: rebuild, onProgress: progress => { current.progress = progress; } });
259
+ const families = await scanner.usageHistory({ rescan: rebuild, onProgress: progress => { current.progress = progress; } });
260
260
  current.families = families;
261
261
  current.finishedAt = Date.now();
262
262
  // Cache failures never hide readable usage or replace a last good snapshot.