pi-kanban 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -2,10 +2,14 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/pi-kanban.svg)](https://www.npmjs.com/package/pi-kanban)
4
4
 
5
- Web dashboard for the [pi coding agent](https://pi.dev) — sessions, todos, subagents, observability.
5
+ Web dashboard for the [pi coding agent](https://pi.dev/packages/pi-kanban?name=kanban) — sessions, todos, subagents, observability.
6
6
 
7
7
  ![pi-kanban dashboard](https://raw.githubusercontent.com/NikiforovAll/pi-kanban/main/assets/pi-kanban-poster.png)
8
8
 
9
+ ## Documentation
10
+
11
+ **[→ See the documentation](https://nikiforovall.github.io/pi-kanban/docs/)**
12
+
9
13
  ## Try it now
10
14
 
11
15
  **[→ Open the live demo](https://nikiforovall.blog/pi-kanban/)**
@@ -39,8 +43,9 @@ pi install npm:@juicesharp/rpiv-todo
39
43
 
40
44
  - [`pi-subagents`](https://www.npmjs.com/package/pi-subagents) — spawns named subagent sessions; pi-kanban renders them under each parent session.
41
45
  - [`@juicesharp/rpiv-todo`](https://www.npmjs.com/package/@juicesharp/rpiv-todo) — task tracking that pi-kanban displays as kanban columns.
46
+ - [`@juicesharp/rpiv-ask-user-question`](https://www.npmjs.com/package/@juicesharp/rpiv-ask-user-question) — pi-kanban surfaces these as interactive Q&A cards.
47
+
48
+ ## Demo
42
49
 
43
- ## Documentation
44
50
 
45
- - [User guide](https://github.com/NikiforovAll/pi-kanban/blob/main/docs/user-guide.md) — slash commands, layout, sessions, subagents, storage manager, keyboard shortcuts.
46
- - [Theming](https://github.com/NikiforovAll/pi-kanban/blob/main/docs/theming.md) — built-in themes, configuration, custom theme authoring.
51
+ ![pi-kanban demo](https://raw.githubusercontent.com/NikiforovAll/pi-kanban/main/assets/pi-kanban-demo.gif)
package/lib/pi-parsers.js CHANGED
@@ -35,30 +35,28 @@ function decodeProjectDir(name) {
35
35
  return s.replace(/-/g, path.sep);
36
36
  }
37
37
 
38
- function listSessionFiles() {
39
- if (!fs.existsSync(SESSIONS_DIR)) return [];
40
- const out = [];
41
- for (const projDir of fs.readdirSync(SESSIONS_DIR)) {
38
+ async function listSessionFiles() {
39
+ let projDirs;
40
+ try { projDirs = await fs.promises.readdir(SESSIONS_DIR, { withFileTypes: true }); }
41
+ catch { return []; }
42
+ const projects = projDirs.filter((d) => d.isDirectory());
43
+ const perProject = await Promise.all(projects.map(async (d) => {
44
+ const projDir = d.name;
42
45
  const projPath = path.join(SESSIONS_DIR, projDir);
43
- let stat;
44
- try { stat = fs.statSync(projPath); } catch { continue; }
45
- if (!stat.isDirectory()) continue;
46
- for (const f of fs.readdirSync(projPath)) {
47
- if (!f.endsWith('.jsonl')) continue;
48
- const full = path.join(projPath, f);
49
- let s;
50
- try { s = fs.statSync(full); } catch { continue; }
51
- if (!s.isFile()) continue;
52
- out.push({
53
- file: full,
54
- projectDir: projDir,
55
- cwd: decodeProjectDir(projDir),
56
- mtime: s.mtime,
57
- size: s.size,
58
- });
59
- }
60
- }
61
- return out;
46
+ let files;
47
+ try { files = await fs.promises.readdir(projPath, { withFileTypes: true }); }
48
+ catch { return []; }
49
+ const jsonls = files.filter((f) => f.isFile() && f.name.endsWith('.jsonl'));
50
+ const stats = await Promise.all(jsonls.map(async (f) => {
51
+ const full = path.join(projPath, f.name);
52
+ try {
53
+ const s = await fs.promises.stat(full);
54
+ return { file: full, projectDir: projDir, cwd: decodeProjectDir(projDir), mtime: s.mtime, mtimeMs: s.mtimeMs, size: s.size };
55
+ } catch { return null; }
56
+ }));
57
+ return stats.filter(Boolean);
58
+ }));
59
+ return perProject.flat();
62
60
  }
63
61
 
64
62
  async function readJsonlLines(file) {
@@ -76,7 +74,7 @@ function parseLine(line) {
76
74
  try { return JSON.parse(line); } catch { return null; }
77
75
  }
78
76
 
79
- async function readSessionEntries(file) {
77
+ async function readSessionEntriesUncached(file) {
80
78
  const lines = await readJsonlLines(file);
81
79
  const entries = [];
82
80
  for (const l of lines) {
@@ -86,6 +84,57 @@ async function readSessionEntries(file) {
86
84
  return entries;
87
85
  }
88
86
 
87
+ // Parsed entries + summary + tool/subagent indices keyed by file path; (mtimeMs, size) is
88
+ // the validity tag. Invalidated by chokidar events via invalidateSessionCache().
89
+ const sessionCache = new Map();
90
+
91
+ function buildToolMaps(entries) {
92
+ const toolResultByCallId = new Map();
93
+ const subagentInfoByCallId = new Map();
94
+ for (const e of entries) {
95
+ if (e.type !== 'message') continue;
96
+ const m = e.message;
97
+ if (m && m.role === 'toolResult' && m.toolCallId) {
98
+ toolResultByCallId.set(m.toolCallId, m);
99
+ if (m.toolName === 'subagent' && m.details && Array.isArray(m.details.results) && m.details.results[0]) {
100
+ const r = m.details.results[0];
101
+ subagentInfoByCallId.set(m.toolCallId, {
102
+ runId: m.details.runId || null,
103
+ agent: r.agent || null,
104
+ runIndex: 0,
105
+ outputPath: r.artifactPaths && r.artifactPaths.outputPath ? r.artifactPaths.outputPath : null,
106
+ inputPath: r.artifactPaths && r.artifactPaths.inputPath ? r.artifactPaths.inputPath : null,
107
+ });
108
+ }
109
+ }
110
+ }
111
+ return { toolResultByCallId, subagentInfoByCallId };
112
+ }
113
+
114
+ async function getCachedSession(file, knownStat = null) {
115
+ const mtimeMs = knownStat ? knownStat.mtimeMs : null;
116
+ const size = knownStat ? knownStat.size : null;
117
+ const cached = sessionCache.get(file);
118
+ if (cached && knownStat && cached.mtimeMs === mtimeMs && cached.size === size) return cached;
119
+ const stat = knownStat || await fs.promises.stat(file);
120
+ if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) return cached;
121
+ const entries = await readSessionEntriesUncached(file);
122
+ const summary = summarize(entries);
123
+ const { toolResultByCallId, subagentInfoByCallId } = buildToolMaps(entries);
124
+ const entry = { mtimeMs: stat.mtimeMs, size: stat.size, entries, summary, toolResultByCallId, subagentInfoByCallId };
125
+ sessionCache.set(file, entry);
126
+ return entry;
127
+ }
128
+
129
+ async function readSessionEntries(file) {
130
+ return (await getCachedSession(file)).entries;
131
+ }
132
+
133
+ function invalidateSessionCache(file) {
134
+ if (file) sessionCache.delete(file);
135
+ else sessionCache.clear();
136
+ }
137
+
89
138
  // Extract concrete agent spawns from a `subagent` toolCall arguments.
90
139
  // Returns [] for management calls (action:"list" etc) so they don't pollute the agent list.
91
140
  // Single spawn: {agent, task} -> [{agent, task}]
@@ -120,6 +169,12 @@ function expandSubagentSpawns(args) {
120
169
  return [];
121
170
  }
122
171
 
172
+ function lastMapValue(map) {
173
+ let v;
174
+ for (v of map.values());
175
+ return v;
176
+ }
177
+
123
178
  function summarize(entries) {
124
179
  let sessionEntry = null;
125
180
  let firstModel = null;
@@ -144,6 +199,8 @@ function summarize(entries) {
144
199
  const subagentByToolCallId = new Map();
145
200
  // `${runId}_${index}` -> rec, for /run slash-invoked subagents (custom_message)
146
201
  const subagentByRunKey = new Map();
202
+ // toolCallId -> { timestamp } for unanswered ask_user_question calls
203
+ const pendingAskUsers = new Map();
147
204
 
148
205
  for (const e of entries) {
149
206
  if (e.type === 'session') sessionEntry = e;
@@ -178,7 +235,9 @@ function summarize(entries) {
178
235
  for (const c of m.content) {
179
236
  if (c && c.type === 'toolCall') {
180
237
  toolCalls++;
181
- if (c.name === 'subagent') {
238
+ if (c.name === 'ask_user_question' && c.id) {
239
+ pendingAskUsers.set(c.id, { timestamp: e.timestamp || null });
240
+ } else if (c.name === 'subagent') {
182
241
  const items = expandSubagentSpawns(c.arguments);
183
242
  if (items.length) {
184
243
  const recs = items.map((it, i) => ({
@@ -205,11 +264,25 @@ function summarize(entries) {
205
264
  }
206
265
  } else if (m.role === 'toolResult') {
207
266
  toolResults++;
208
- if (m.toolName === 'todo' && m.details && Array.isArray(m.details.tasks)) {
267
+ if (m.toolName === 'ask_user_question' && m.toolCallId) {
268
+ pendingAskUsers.delete(m.toolCallId);
269
+ } else if (m.toolName === 'todo' && m.details && Array.isArray(m.details.tasks)) {
209
270
  lastTodoTasks = m.details.tasks;
210
- } else if (m.toolName === 'subagent' && m.details && Array.isArray(m.details.results)) {
271
+ } else if (m.toolName === 'subagent') {
211
272
  const pendingArr = m.toolCallId ? subagentByToolCallId.get(m.toolCallId) : null;
212
- m.details.results.forEach((r, idx) => {
273
+ const results = m.details && Array.isArray(m.details.results) ? m.details.results : null;
274
+ if (!results && pendingArr && pendingArr.length) {
275
+ // Validation/spawn-time failure: pi wrote an error toolResult with no results.
276
+ const errText = Array.isArray(m.content)
277
+ ? (m.content.find((c) => c && c.type === 'text')?.text || '')
278
+ : '';
279
+ for (const rec of pendingArr) {
280
+ rec.stoppedAt = e.timestamp || null;
281
+ rec._error = rec._error || (errText ? errText.split('\n')[0].slice(0, 200) : 'Spawn failed');
282
+ if (rec.exitCode == null) rec.exitCode = -1;
283
+ }
284
+ }
285
+ (results || []).forEach((r, idx) => {
213
286
  let target = pendingArr && pendingArr[idx];
214
287
  if (!target) {
215
288
  const base = pendingArr && pendingArr[0];
@@ -222,7 +295,7 @@ function summarize(entries) {
222
295
  };
223
296
  subagents.push(target);
224
297
  }
225
- target.runId = m.details.runId || target.runId;
298
+ target.runId = (m.details && m.details.runId) || target.runId;
226
299
  target.runIndex = idx;
227
300
  target.agent = r.agent || target.agent;
228
301
  target.prompt = target.prompt || r.task || null;
@@ -237,7 +310,7 @@ function summarize(entries) {
237
310
  target._cancelled = true;
238
311
  }
239
312
  });
240
- if (m.details.results.length === 0 && pendingArr && pendingArr.length) {
313
+ if (results && results.length === 0 && pendingArr && pendingArr.length) {
241
314
  const text = Array.isArray(m.content) ? (m.content.find((c) => c && c.type === 'text')?.text || '') : '';
242
315
  const cancelled = /cancel/i.test(text);
243
316
  for (const rec of pendingArr) {
@@ -322,6 +395,7 @@ function summarize(entries) {
322
395
  lastAssistantUsage,
323
396
  lastTodoTasks,
324
397
  subagents,
398
+ pendingAskUser: pendingAskUsers.size > 0 ? lastMapValue(pendingAskUsers) : null,
325
399
  };
326
400
  }
327
401
 
@@ -480,28 +554,35 @@ async function matchLiveSubagentRuns(meta, pending, allSubagents) {
480
554
  rec.sessionFile = lr.jsonl;
481
555
  }
482
556
 
483
- let parentMtime = 0;
484
- try { parentMtime = (await fs.promises.stat(meta.file)).mtimeMs; } catch {}
485
- const STALE_MS = 60_000;
486
- const now = Date.now();
487
- const parentStale = parentMtime && (now - parentMtime > STALE_MS);
557
+ // Structural staleness: while a subagent is running, the parent assistant is blocked
558
+ // waiting on its toolResult, so the parent jsonl can't have any later message other
559
+ // than that toolResult. If we see a newer message that isn't this subagent's own
560
+ // toolResult, pi has moved on and the spawn is orphaned. Tradeoff: a real pi crash
561
+ // with no further parent activity stays as "active" until the user restarts pi.
562
+ const parentEntries = (await getCachedSession(meta.file)).entries;
488
563
  for (const rec of pending) {
489
564
  if (rec.stoppedAt) continue;
490
- let runMtime = 0;
491
- if (rec.sessionFile) {
492
- try { runMtime = (await fs.promises.stat(rec.sessionFile)).mtimeMs; } catch {}
493
- }
494
- const runStale = runMtime ? (now - runMtime > STALE_MS) : true;
495
- if (parentStale && runStale) {
496
- rec.stoppedAt = new Date(runMtime || parentMtime || now).toISOString();
565
+ if (!rec.startedAt) continue;
566
+ const startMs = new Date(rec.startedAt).getTime();
567
+ if (!startMs) continue;
568
+ const movedOn = parentEntries.some((e) => {
569
+ if (e.type !== 'message') return false;
570
+ const t = e.timestamp ? new Date(e.timestamp).getTime() : 0;
571
+ if (t <= startMs) return false;
572
+ const m = e.message;
573
+ if (m && m.role === 'toolResult' && m.toolName === 'subagent'
574
+ && rec.toolCallId && m.toolCallId === rec.toolCallId) return false;
575
+ return true;
576
+ });
577
+ if (movedOn) {
578
+ rec.stoppedAt = new Date().toISOString();
497
579
  rec._error = rec._error || 'Interrupted (no result recorded)';
498
580
  }
499
581
  }
500
582
  }
501
583
 
502
- async function listAgentsForSession(meta) {
503
- const entries = await readSessionEntries(meta.file);
504
- const s = summarize(entries);
584
+ async function listAgentsForSession(meta, summary = null) {
585
+ const s = summary || (await getCachedSession(meta.file)).summary;
505
586
  const recs = s.subagents || [];
506
587
  await enrichPendingSubagents(meta, recs);
507
588
  const out = [];
@@ -567,8 +648,8 @@ function slugFromFile(file) {
567
648
  const RECENT_MS = 5 * 60 * 1000;
568
649
 
569
650
  async function buildSessionSummary(meta) {
570
- const entries = await readSessionEntries(meta.file);
571
- const s = summarize(entries);
651
+ const cached = await getCachedSession(meta.file, meta.mtimeMs != null ? { mtimeMs: meta.mtimeMs, size: meta.size } : null);
652
+ const s = cached.summary;
572
653
  const id = (s.sessionEntry && s.sessionEntry.id) || slugFromFile(meta.file);
573
654
  const cwd = (s.sessionEntry && s.sessionEntry.cwd) || meta.cwd;
574
655
  const projectName = path.basename(cwd) || cwd;
@@ -586,13 +667,13 @@ async function buildSessionSummary(meta) {
586
667
  }),
587
668
  customTitle: s.customTitle || null,
588
669
  parentSession: (s.sessionEntry && s.sessionEntry.parentSession) || null,
589
- ...tallyTasks(taskStore.listTasks(id)),
670
+ ...tallyTasks(await taskStore.listTasksAsync(id)),
590
671
  createdAt: s.sessionEntry && s.sessionEntry.timestamp,
591
672
  modifiedAt: meta.mtime.toISOString(),
592
673
  hasMessages: s.messageCount > 0,
593
674
  hasActiveAgents: false,
594
675
  hasRunningAgents: false,
595
- hasWaitingForUser: false,
676
+ hasWaitingForUser: !!s.pendingAskUser,
596
677
  hasRecentLog: isRecent,
597
678
  jsonlPath: meta.file,
598
679
  tasksDir: null,
@@ -649,26 +730,26 @@ async function buildSessionSummary(meta) {
649
730
  }
650
731
 
651
732
  async function listSessions() {
652
- const files = listSessionFiles();
733
+ const files = await listSessionFiles();
653
734
  files.sort((a, b) => b.mtime - a.mtime);
735
+ const summaries = await Promise.all(files.map(async (f) => {
736
+ try { return await buildSessionSummary(f); }
737
+ catch { return null; }
738
+ }));
654
739
  const out = [];
655
740
  const seen = new Set();
656
- for (const f of files) {
657
- try {
658
- const summary = await buildSessionSummary(f);
659
- if (summary.parentSession) continue;
660
- if (seen.has(summary.id)) continue;
661
- seen.add(summary.id);
662
- out.push(summary);
663
- } catch (err) {
664
- // skip corrupt files
665
- }
741
+ for (const summary of summaries) {
742
+ if (!summary) continue;
743
+ if (summary.parentSession) continue;
744
+ if (seen.has(summary.id)) continue;
745
+ seen.add(summary.id);
746
+ out.push(summary);
666
747
  }
667
748
  return out;
668
749
  }
669
750
 
670
751
  async function findSessionFileById(id) {
671
- const files = listSessionFiles();
752
+ const files = await listSessionFiles();
672
753
  files.sort((a, b) => b.mtime - a.mtime);
673
754
  // First try filename match (faster)
674
755
  for (const f of files) {
@@ -726,22 +807,36 @@ function computeToolDetail(name, params) {
726
807
  }
727
808
  let detail = null;
728
809
  let full = null;
810
+ const trunc = (s, n = 80) => (s.length > n ? s.slice(0, n) + '...' : s);
729
811
  if (params.file_path) { detail = path.basename(params.file_path); full = params.file_path; }
730
812
  else if (params.path) { detail = path.basename(params.path); full = params.path; }
731
- else if (params.command) { full = params.command; detail = full.length > 80 ? full.slice(0, 80) + '...' : full; }
813
+ else if (params.command) { full = params.command; detail = trunc(full); }
732
814
  else if (params.pattern) { full = params.pattern; detail = full; }
733
815
  else if (params.query) { full = params.query; detail = full; }
734
- else if (params.queries) { full = Array.isArray(params.queries) ? params.queries.join(' | ') : String(params.queries); detail = full.length > 80 ? full.slice(0, 80) + '...' : full; }
735
- else if (params.url) { full = params.url; detail = full.length > 80 ? full.slice(0, 80) + '...' : full; }
736
- else if (params.task) { full = params.task; detail = full.length > 80 ? full.slice(0, 80) + '...' : full; }
816
+ else if (params.queries) { full = Array.isArray(params.queries) ? params.queries.join(' | ') : String(params.queries); detail = trunc(full); }
817
+ else if (params.url) { full = params.url; detail = trunc(full); }
818
+ else if (params.task) { full = params.task; detail = trunc(full); }
737
819
  else if (params.subject) { full = params.subject; detail = full; }
738
- else if (params.description) { full = params.description; detail = full.length > 80 ? full.slice(0, 80) + '...' : full; }
739
- else if (params.question) { full = params.question; detail = full.length > 80 ? full.slice(0, 80) + '...' : full; }
820
+ else if (params.description) { full = params.description; detail = trunc(full); }
821
+ else if (params.tasks && Array.isArray(params.tasks)) {
822
+ const agents = [...new Set(params.tasks.map((t) => t.agent).filter(Boolean))];
823
+ const n = params.tasks.length;
824
+ const agentStr = agents.length ? ` · ${agents.join(', ')}` : '';
825
+ detail = `${n} task${n > 1 ? 's' : ''}${agentStr}`;
826
+ full = detail;
827
+ }
828
+ else if (params.question) { full = params.question; detail = trunc(full); }
829
+ else if (params.questions && Array.isArray(params.questions)) {
830
+ const first = params.questions[0]?.question || '';
831
+ const extra = params.questions.length > 1 ? ` (+${params.questions.length - 1} more)` : '';
832
+ full = params.questions.map((q) => q.question).join(' | ');
833
+ detail = trunc(first, 60) + extra;
834
+ }
740
835
  else if (params.action) { detail = params.action; full = detail; }
741
836
  else if (params.operation) { detail = params.operation; full = detail; }
742
837
  else {
743
838
  try { full = JSON.stringify(params); } catch { full = ''; }
744
- detail = full.length > 80 ? full.slice(0, 80) + '...' : full;
839
+ detail = trunc(full);
745
840
  }
746
841
  return { detail: detail || '', fullDetail: full && full !== detail ? full : null };
747
842
  }
@@ -779,26 +874,18 @@ async function readFileSafe(p) {
779
874
  }
780
875
 
781
876
  async function buildMessages(file, limit = 50, before = null) {
782
- const entries = await readSessionEntries(file);
783
- const toolResultByCallId = new Map();
784
- const subagentInfoByCallId = new Map();
785
- for (const e of entries) {
786
- if (e.type !== 'message') continue;
787
- const m = e.message;
788
- if (m && m.role === 'toolResult' && m.toolCallId) {
789
- toolResultByCallId.set(m.toolCallId, m);
790
- if (m.toolName === 'subagent' && m.details && Array.isArray(m.details.results) && m.details.results[0]) {
791
- const r = m.details.results[0];
792
- subagentInfoByCallId.set(m.toolCallId, {
793
- runId: m.details.runId || null,
794
- agent: r.agent || null,
795
- runIndex: 0,
796
- outputPath: r.artifactPaths && r.artifactPaths.outputPath ? r.artifactPaths.outputPath : null,
797
- inputPath: r.artifactPaths && r.artifactPaths.inputPath ? r.artifactPaths.inputPath : null,
798
- });
799
- }
800
- }
877
+ const cached = await getCachedSession(file);
878
+ if (cached.allMessages) {
879
+ const filtered = before
880
+ ? cached.allMessages.filter((m) => m.timestamp && m.timestamp < before)
881
+ : cached.allMessages;
882
+ const total = filtered.length;
883
+ const sliced = limit && limit < total ? filtered.slice(-limit) : filtered;
884
+ return { messages: sliced, hasMore: sliced.length < total, sessionId: null };
801
885
  }
886
+ const entries = cached.entries;
887
+ const toolResultByCallId = cached.toolResultByCallId;
888
+ const subagentInfoByCallId = cached.subagentInfoByCallId;
802
889
 
803
890
  const messages = [];
804
891
  for (const e of entries) {
@@ -852,11 +939,15 @@ async function buildMessages(file, limit = 50, before = null) {
852
939
  toolResult: trText ? trText.slice(0, 1500) : null,
853
940
  toolResultTruncated: trText ? trText.length > 1500 : false,
854
941
  };
942
+ if (c.name === 'ask_user_question' && tr && tr.details && !tr.isError) {
943
+ msg.toolResultDetails = tr.details;
944
+ }
855
945
  if (c.name === 'subagent') {
856
946
  const info = subagentInfoByCallId.get(c.id);
857
947
  const agent = (c.arguments && c.arguments.agent) || (info && info.agent) || null;
858
948
  const runId = info && info.runId ? info.runId : null;
859
949
  const isSpawn = !!(c.arguments && c.arguments.task);
950
+ const isParallel = !!(c.arguments && Array.isArray(c.arguments.tasks));
860
951
  if (isSpawn) {
861
952
  msg.tool = 'Agent';
862
953
  msg.agentId = c.id ? `tc_${c.id}_${(info && info.runIndex) || 0}` : (runId && agent ? `${runId}_${agent}_${(info && info.runIndex) || 0}` : `pending_${Math.random().toString(36).slice(2, 10)}`);
@@ -865,6 +956,11 @@ async function buildMessages(file, limit = 50, before = null) {
865
956
  msg.agentPrompt = c.arguments.task || null;
866
957
  msg.agentLastMessage = info && info.outputPath ? await readFileSafe(info.outputPath) : null;
867
958
  if (!msg.agentLastMessage && trText) msg.agentLastMessage = trText;
959
+ } else if (isParallel) {
960
+ msg.agentTasks = c.arguments.tasks;
961
+ if (tr && tr.details && Array.isArray(tr.details.results)) {
962
+ msg.parallelResults = tr.details.results;
963
+ }
868
964
  }
869
965
  }
870
966
  messages.push(msg);
@@ -875,6 +971,7 @@ async function buildMessages(file, limit = 50, before = null) {
875
971
  // toolResult handled via map above.
876
972
  }
877
973
 
974
+ cached.allMessages = messages;
878
975
  const filtered = before
879
976
  ? messages.filter((m) => m.timestamp && m.timestamp < before)
880
977
  : messages;
@@ -898,4 +995,6 @@ module.exports = {
898
995
  findAgentRecord,
899
996
  slugFromFile,
900
997
  setOnBranchResolved,
998
+ flattenContentToText,
999
+ invalidateSessionCache,
901
1000
  };
package/lib/task-store.js CHANGED
@@ -24,8 +24,8 @@ function readTask(sessionId, taskId) {
24
24
  } catch { return null; }
25
25
  }
26
26
 
27
- function listSessionIds() {
28
- try { return fs.readdirSync(TASKS_DIR); }
27
+ async function listSessionIdsAsync() {
28
+ try { return await fs.promises.readdir(TASKS_DIR); }
29
29
  catch { return []; }
30
30
  }
31
31
 
@@ -45,6 +45,21 @@ function listTasks(sessionId) {
45
45
  return out;
46
46
  }
47
47
 
48
+ async function listTasksAsync(sessionId) {
49
+ const dir = sessionDir(sessionId);
50
+ let entries;
51
+ try { entries = await fs.promises.readdir(dir); }
52
+ catch { return []; }
53
+ const jsons = entries.filter((n) => n.endsWith('.json'));
54
+ const tasks = await Promise.all(jsons.map(async (name) => {
55
+ try {
56
+ const raw = await fs.promises.readFile(path.join(dir, name), 'utf8');
57
+ return JSON.parse(raw);
58
+ } catch { return null; }
59
+ }));
60
+ return tasks.filter(Boolean);
61
+ }
62
+
48
63
  function writeTask(sessionId, task) {
49
64
  const dir = sessionDir(sessionId);
50
65
  fs.mkdirSync(dir, { recursive: true });
@@ -126,8 +141,9 @@ function reconcileFromSnapshot(sessionId, snapshot) {
126
141
  module.exports = {
127
142
  getTasksDir,
128
143
  sessionDir,
129
- listSessionIds,
144
+ listSessionIdsAsync,
130
145
  listTasks,
146
+ listTasksAsync,
131
147
  readTask,
132
148
  writeTask,
133
149
  deleteTask,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-kanban",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Workspace for the pi coding agent — sessions, todos, subagents, and more.",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -10,7 +10,13 @@
10
10
  "start": "node server.js",
11
11
  "dev": "node --watch server.js",
12
12
  "build:demo": "node scripts/build-demo.js",
13
- "preview:demo": "npm run build:demo && npx --yes http-server dist -p 8765 -c-1"
13
+ "preview:demo": "npm run build:demo && npx --yes http-server dist -p 8765 -c-1",
14
+ "docs:dev": "vitepress dev docs",
15
+ "docs:build": "vitepress build docs",
16
+ "docs:preview": "vitepress preview docs"
17
+ },
18
+ "devDependencies": {
19
+ "vitepress": "^1.6.4"
14
20
  },
15
21
  "keywords": [
16
22
  "pi-package",