taskchef 9.0.4 → 9.1.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskchef",
3
- "version": "9.0.4",
3
+ "version": "9.1.0",
4
4
  "description": "Native Codex dashboard with lightweight project delegation.",
5
5
  "author": {
6
6
  "name": "Favo Yang",
package/docs/mcp-app.md CHANGED
@@ -298,3 +298,9 @@ its cards, controls, borders, and elevated surfaces from those seeds. Links and
298
298
  small muted text are adjusted when needed to meet a 4.5:1 contrast ratio on the
299
299
  UI surfaces. Native font families use locally available fonts with system fallbacks. Font assets are not copied from the desktop application. Palette definitions ship with TaskChef; opening the board does not
300
300
  read the installed desktop application or fetch theme files from the network.
301
+
302
+ The compact **Pinned** toggle button shows chats pinned in Codex and combines with project,
303
+ update window, and search filters. It starts off when the dashboard opens. Pin
304
+ changes appear on the next data refresh. The current desktop pin list takes
305
+ precedence over the database `is_pinned` field. TaskChef ignores old migration
306
+ snapshots and never writes pin state. Without either source, chats are unpinned.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskchef",
3
- "version": "9.0.4",
3
+ "version": "9.1.0",
4
4
  "description": "A native Codex dashboard for chats, usage, pull requests, and schedules.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Favo Yang",
@@ -8,7 +8,7 @@ const contains = (root, cwd) => {
8
8
  return child === "" || (!child.startsWith(`..${sep}`) && child !== ".." && !isAbsolute(child));
9
9
  };
10
10
 
11
- // Only project metadata and unread chat IDs leave this reader. Never return the desktop's other saved settings.
11
+ // Only project metadata, pin state, and unread chat IDs leave this reader. Never return the desktop's other saved settings.
12
12
  export class CodexProjects {
13
13
  constructor(codexHome) { this.codexHome = codexHome; this.saved = null; }
14
14
  async desktopProjects() {
@@ -20,7 +20,9 @@ export class CodexProjects {
20
20
  } catch (error) { if (error.code === "ENOENT") return {}; throw error; }
21
21
  if (this.saved?.stamp === stamp) return this.saved.data;
22
22
  const state = JSON.parse(await readFile(file, "utf8"));
23
+ const pinned = state["electron-persisted-atom-state"]?.["app-server-pinned-thread-order-v1"];
23
24
  const data = {
25
+ pinned: Array.isArray(pinned) ? new Set(pinned.filter(id => typeof id === "string")) : null,
24
26
  unread: localUnreadChats(state["electron-thread-read-state-v1"]),
25
27
  assignments: state["thread-project-assignments"] ?? {},
26
28
  hints: state["thread-workspace-root-hints"] ?? {}, projectless: new Set(state["projectless-thread-ids"] ?? []),
@@ -71,7 +73,7 @@ export class CodexProjects {
71
73
  if (!explicit && !noProject && !hinted && !origins.has(cwd)) origins.set(cwd, this.worktreeRoot(cwd));
72
74
  const origin = origins.has(cwd) ? await origins.get(cwd) : null;
73
75
  const project = noProject ? null : explicit ?? hinted ?? matchRoot(origin || cwd);
74
- return { ...task, ...(task.observed ? { observed: { ...task.observed, unread: saved.unread?.has(task.id) ?? false } } : {}), project: { ...task.project, id: project?.id ?? NO_PROJECT, name: project?.name ?? "No project" } };
76
+ return { ...task, ...(task.observed ? { observed: { ...task.observed, pinned: saved.pinned ? saved.pinned.has(task.id) : task.observed.pinned ?? false, unread: saved.unread?.has(task.id) ?? false } } : {}), project: { ...task.project, id: project?.id ?? NO_PROJECT, name: project?.name ?? "No project" } };
75
77
  }));
76
78
  return { tasks: grouped, projects: projects.map(({ id, name, roots }) => ({ id, name, path: roots[0] ?? "", githubRepos: [] })) };
77
79
  }
@@ -233,12 +233,13 @@ function readDatabaseRecords(state, history) {
233
233
  const eligible = `coalesce(thread_source, '') NOT IN ('subagent', 'guardian_review')
234
234
  AND CASE WHEN json_valid(source) THEN json_type(source, '$.subagent') IS NULL ELSE 1 END
235
235
  AND NOT EXISTS (SELECT 1 FROM thread_spawn_edges WHERE child_thread_id = threads.id)`;
236
+ const hasPinned = state.prepare("PRAGMA table_info(threads)").all().some(column => column.name === "is_pinned");
236
237
  const hasTurnError = history.prepare("PRAGMA table_info(thread_turns)").all().some(column => column.name === "error_json");
237
238
  const latestTurn = history.prepare(`SELECT turn_id, status, started_at, duration_ms, first_user_item_id, final_agent_item_id, ${hasTurnError ? "error_json" : "NULL AS error_json"}
238
239
  FROM thread_turns WHERE thread_id = ? ORDER BY rollout_ordinal DESC LIMIT 1`);
239
240
  const turns = new Map();
240
241
  const needsAnswers = new Map();
241
- const rows = state.prepare(`SELECT id, name, title, cwd, archived, source, created_at_ms, updated_at_ms, recency_at_ms, rollout_path, history_mode, project_id,
242
+ const rows = state.prepare(`SELECT id, name, title, cwd, archived, ${hasPinned ? "is_pinned" : "0 AS is_pinned"}, source, created_at_ms, updated_at_ms, recency_at_ms, rollout_path, history_mode, project_id,
242
243
  (SELECT count(*) FROM thread_spawn_edges WHERE parent_thread_id = threads.id) AS child_count
243
244
  FROM threads WHERE ${eligible} ORDER BY recency_at_ms DESC`).all().filter((row) => {
244
245
  const key = historyId(row);
@@ -361,7 +362,7 @@ function buildDatabaseSnapshot(records, now, doneMarks, schedules, scheduleError
361
362
  createdAt: iso(row.created_at_ms || updatedMs), updatedAt: iso(updatedMs),
362
363
  updatedBy: "Local Codex database", project: { id: row.project_id, name: basename(cwd) || cwd || "Unknown project", path: cwd, githubRepos: [] },
363
364
  threadId: row.id, turnRef: turn.turn_id, turnId: turn.turn_id, lastResult: null, latestTurn: null,
364
- observed: { source: row.source, archive: Boolean(row.archived), lastTurnEvent: turn.status, lastTurnEventAt: turn.started_at ? iso(turn.started_at * 1000) : null, latestTurnDurationMs: turn.status !== "inProgress" && Number.isFinite(turn.duration_ms) && turn.duration_ms >= 0 ? turn.duration_ms : null, recentFileActivity: !row.archived && now - updatedMs < ACTIVE_WINDOW_MS, directChildCount: row.child_count },
365
+ observed: { pinned: Boolean(row.is_pinned), source: row.source, archive: Boolean(row.archived), lastTurnEvent: turn.status, lastTurnEventAt: turn.started_at ? iso(turn.started_at * 1000) : null, latestTurnDurationMs: turn.status !== "inProgress" && Number.isFinite(turn.duration_ms) && turn.duration_ms >= 0 ? turn.duration_ms : null, recentFileActivity: !row.archived && now - updatedMs < ACTIVE_WINDOW_MS, directChildCount: row.child_count },
365
366
  };
366
367
  });
367
368
  return { tasks, rolloutPaths: new Map(rows.map((row) => [row.id, row.rollout_path])), scan: {
@@ -571,7 +572,7 @@ export class CodexSessionScanner {
571
572
  const nextTasks = new Map(grouped.tasks.map((task) => [task.id, task]));
572
573
  const signature = (tasks) => JSON.stringify([...tasks.values()].map((task) => [
573
574
  task.id, task.title, task.project.path, task.project.id, task.project.name, task.updatedAt, task.status, task.summary, task.turnError,
574
- task.observed.archive, task.observed.unread, task.observed.lastTurnEvent, task.observed.directChildCount, task.observed.latestTurnDurationMs,
575
+ task.observed.archive, task.observed.pinned, task.observed.unread, task.observed.lastTurnEvent, task.observed.directChildCount, task.observed.latestTurnDurationMs,
575
576
  task.scheduled, task.nextRunAt, task.inputSource, task.turnId, task.promptExcerpt, task.replyExcerpt, task.annotations, task.promptImages, task.promptImage, task.replyImage, task.queuedPrompts, task.needsAnswer, task.pendingQuestions, task.pullRequests,
576
577
  ]));
577
578
  if (this.stats?.mode !== "database" || signature(this.tasks) !== signature(nextTasks) || JSON.stringify(this.projects) !== JSON.stringify(grouped.projects)) this.revision += 1;