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 +9 -4
- package/lib/pi-parsers.js +186 -87
- package/lib/task-store.js +19 -3
- package/package.json +8 -2
- package/public/app.js +5854 -5750
- package/public/style.css +144 -0
- package/server.js +125 -46
package/README.md
CHANGED
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
[](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
|

|
|
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
|
-
-
|
|
46
|
-
- [Theming](https://github.com/NikiforovAll/pi-kanban/blob/main/docs/theming.md) — built-in themes, configuration, custom theme authoring.
|
|
51
|
+

|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
44
|
-
try {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const full = path.join(projPath, f);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
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 === '
|
|
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 === '
|
|
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'
|
|
271
|
+
} else if (m.toolName === 'subagent') {
|
|
211
272
|
const pendingArr = m.toolCallId ? subagentByToolCallId.get(m.toolCallId) : null;
|
|
212
|
-
m.details.results.
|
|
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 (
|
|
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
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
|
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
|
|
571
|
-
const s =
|
|
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.
|
|
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:
|
|
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
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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
|
|
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
|
|
735
|
-
else if (params.url) { full = params.url; detail = full
|
|
736
|
-
else if (params.task) { full = params.task; detail = 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
|
|
739
|
-
else if (params.
|
|
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
|
|
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
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
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
|
|
28
|
-
try { return fs.
|
|
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
|
-
|
|
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.
|
|
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",
|