claude-code-kanban 4.12.0 → 4.14.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 +23 -77
- package/lib/parsers.js +4 -3
- package/package.json +1 -1
- package/public/app.js +138 -1
- package/public/index.html +24 -0
- package/public/style.css +94 -4
- package/server.js +158 -20
package/README.md
CHANGED
|
@@ -8,9 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
> Watch Claude Code work, in real time.
|
|
10
10
|
|
|
11
|
-

|
|
11
|
+

|
|
14
12
|
|
|
15
13
|
## Getting Started
|
|
16
14
|
|
|
@@ -22,11 +20,7 @@ Hooks enable subagent tracking, waiting-for-user detection, and session activity
|
|
|
22
20
|
npx claude-code-kanban --install
|
|
23
21
|
```
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
- Copy `agent-spy.sh` to `~/.claude/hooks/` (requires `jq`)
|
|
27
|
-
- Add `SubagentStart`, `SubagentStop`, `TeammateIdle`, and `PostToolUse` hooks to `~/.claude/settings.json`
|
|
28
|
-
|
|
29
|
-
All changes are non-destructive — existing settings are preserved.
|
|
23
|
+
Non-destructive — existing settings in `~/.claude/settings.json` are preserved. Uninstall anytime with `npx claude-code-kanban --uninstall`.
|
|
30
24
|
|
|
31
25
|
### 2. Start the dashboard
|
|
32
26
|
|
|
@@ -34,72 +28,34 @@ All changes are non-destructive — existing settings are preserved.
|
|
|
34
28
|
npx claude-code-kanban --open
|
|
35
29
|
```
|
|
36
30
|
|
|
37
|
-
Open http://localhost:3541 (or use `--open` to auto-launch the browser).
|
|
38
|
-
|
|
39
31
|
### 3. Use Claude Code as usual
|
|
40
32
|
|
|
41
|
-
Tasks, agents, and messages appear on the board automatically
|
|
42
|
-
|
|
43
|
-
> To uninstall hooks: `npx claude-code-kanban --uninstall`
|
|
33
|
+
Tasks, agents, and messages appear on the board automatically — Claude Code writes task files and conversation logs to `~/.claude`, the dashboard watches them and streams updates to the browser via SSE. It never directs Claude's work.
|
|
44
34
|
|
|
45
35
|
## Features
|
|
46
36
|
|
|
47
37
|
- **Real-time Kanban board** — Tasks move through Pending → In Progress → Completed as Claude works
|
|
48
|
-
- **
|
|
49
|
-
- **
|
|
38
|
+
- **Session log** — The full conversation timeline: prompts, replies, tool calls and results (`Shift+L`)
|
|
39
|
+
- **Agent log** — Live subagent tracking with prompts, duration, status, and idle detection
|
|
40
|
+
- **Task detail panel** — Full description, notes, blockedBy/blocks dependencies, inline editing
|
|
41
|
+
- **Follow & pin** — Follow the latest message live (`Shift+M`), pin the messages that matter
|
|
42
|
+
- **Tool stats & impact** — Per-session tool usage breakdown and file impact
|
|
50
43
|
- **Waiting-for-user indicators** — Amber highlight on sessions needing permission or input
|
|
51
|
-
- **Plan correlation** — Plan sessions linked to their implementation sessions
|
|
52
44
|
- **Agent teams** — Color-coded team members, owner filtering, member count badges
|
|
53
|
-
- **
|
|
54
|
-
- **
|
|
55
|
-
- **
|
|
56
|
-
- **Cleanup** — Delete tasks (with dependency checks) or bulk-delete entire sessions
|
|
57
|
-
- **Keyboard shortcuts** — Press `?` for help; `Shift+M` for live message follow
|
|
58
|
-
|
|
59
|
-
## Configuration
|
|
60
|
-
|
|
61
|
-
```bash
|
|
62
|
-
PORT=8080 npx claude-code-kanban # Custom port
|
|
63
|
-
npx claude-code-kanban --open # Auto-open browser
|
|
64
|
-
npx claude-code-kanban --dir=~/.claude-work # Custom Claude config dir
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
If port 3541 is in use, the server falls back to a random available port.
|
|
68
|
-
|
|
69
|
-
### Global install
|
|
70
|
-
|
|
71
|
-
```bash
|
|
72
|
-
npm install -g claude-code-kanban
|
|
73
|
-
claude-code-kanban --open
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## How It Works
|
|
45
|
+
- **17 color themes** — Dracula, Nord, Catppuccin, Gruvbox, Tokyo Night, and more — each in light and dark
|
|
46
|
+
- **Storage manager** — Inspect disk usage and clean up stale sessions and tasks
|
|
47
|
+
- **Keyboard-first** — Press `?` for the full shortcut reference
|
|
77
48
|
|
|
78
|
-
|
|
49
|
+

|
|
79
50
|
|
|
80
|
-
|
|
51
|
+

|
|
81
52
|
|
|
82
|
-
|
|
53
|
+

|
|
83
54
|
|
|
84
|
-
| Hook event | What it enables |
|
|
85
|
-
|------------|----------------|
|
|
86
|
-
| `SubagentStart` / `SubagentStop` | Agent log — see subagent spawns, durations, prompts |
|
|
87
|
-
| `TeammateIdle` | Idle detection for team member agents |
|
|
88
|
-
| `PostToolUse` | Waiting-for-user detection (permission prompts, AskUserQuestion) |
|
|
89
55
|
|
|
90
56
|
## Context Window Monitoring
|
|
91
57
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
### Setup
|
|
95
|
-
|
|
96
|
-
The installer copies `context-status.sh` alongside the agent hooks:
|
|
97
|
-
|
|
98
|
-
```bash
|
|
99
|
-
npx claude-code-kanban --install
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
Then add it to your statusline in `~/.claude/settings.json`:
|
|
58
|
+
Per-session context usage bars, token/cost breakdowns, and model info in the sidebar and detail panel. The installer copies `context-status.sh` — wire it into your statusline in `~/.claude/settings.json`:
|
|
103
59
|
|
|
104
60
|
```json
|
|
105
61
|
{
|
|
@@ -111,27 +67,17 @@ Then add it to your statusline in `~/.claude/settings.json`:
|
|
|
111
67
|
}
|
|
112
68
|
```
|
|
113
69
|
|
|
114
|
-
The script pipes through
|
|
115
|
-
|
|
116
|
-
### What you get
|
|
70
|
+
The script pipes through, so your existing statusline keeps working.
|
|
117
71
|
|
|
118
|
-
|
|
119
|
-
- **Detail panel** — input/output token breakdown, cache read tokens, cost, duration, API time, lines added/removed, and model name
|
|
120
|
-
- Only shown for active or pinned sessions
|
|
121
|
-
|
|
122
|
-
## FAQ
|
|
123
|
-
|
|
124
|
-
**Does this control Claude?**
|
|
125
|
-
No. The viewer only observes — it never writes to task files or directs Claude's work.
|
|
126
|
-
|
|
127
|
-
**Does it work with agent teams?**
|
|
128
|
-
Yes. Team sessions are auto-detected with color-coded members and owner filtering.
|
|
72
|
+
## Configuration
|
|
129
73
|
|
|
130
|
-
|
|
131
|
-
|
|
74
|
+
```bash
|
|
75
|
+
PORT=8080 npx claude-code-kanban # Custom port (falls back if busy)
|
|
76
|
+
npx claude-code-kanban --open # Auto-open browser
|
|
77
|
+
npx claude-code-kanban --dir=~/.claude-work # Custom Claude config dir
|
|
78
|
+
```
|
|
132
79
|
|
|
133
|
-
|
|
134
|
-
The Kanban board still shows tasks, but you won't see the agent log, waiting-for-user indicators, or live session activity. Run `npx claude-code-kanban --install` for the full experience.
|
|
80
|
+
Global install: `npm install -g claude-code-kanban`, then `claude-code-kanban --open`.
|
|
135
81
|
|
|
136
82
|
## License
|
|
137
83
|
|
package/lib/parsers.js
CHANGED
|
@@ -1029,14 +1029,15 @@ const CACHED_IMAGE_MIME = {
|
|
|
1029
1029
|
png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp',
|
|
1030
1030
|
};
|
|
1031
1031
|
|
|
1032
|
-
// Serve pasted image N from
|
|
1032
|
+
// Serve pasted image N from <claudeDir>/image-cache/<sessionId>/. The cached file may
|
|
1033
1033
|
// be any image format, so resolve it by index rather than assuming .png. sessionId
|
|
1034
1034
|
// is validated to a bare id to prevent path traversal.
|
|
1035
|
-
function readCachedImage(sessionId, n) {
|
|
1035
|
+
function readCachedImage(sessionId, n, claudeDir) {
|
|
1036
1036
|
const idx = Number(n);
|
|
1037
1037
|
if (!sessionId || !/^[A-Za-z0-9._-]+$/.test(sessionId)) return null;
|
|
1038
1038
|
if (!Number.isInteger(idx) || idx < 0) return null;
|
|
1039
|
-
const
|
|
1039
|
+
const baseDir = claudeDir || path.join(require('os').homedir(), '.claude');
|
|
1040
|
+
const dir = path.join(baseDir, 'image-cache', sessionId);
|
|
1040
1041
|
try {
|
|
1041
1042
|
const match = readdirSync(dir).find((f) => {
|
|
1042
1043
|
const dot = f.lastIndexOf('.');
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -2944,6 +2944,7 @@ function renderSessions() {
|
|
|
2944
2944
|
${isTeam || session.project || showCtx ? `<span class="team-info-btn" onclick="event.stopPropagation(); showSessionInfoModal('${session.id}')" title="View session info"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></span>` : ''}
|
|
2945
2945
|
${session.hasPlan && !session.planSourceSessionId ? `<span class="plan-indicator" onclick="event.stopPropagation(); openPlanForSession('${session.id}')" title="View plan"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg></span>` : ''}
|
|
2946
2946
|
${renderLoopBadge(session)}
|
|
2947
|
+
${renderWorkflowBadge(session)}
|
|
2947
2948
|
${linkedDocsCount > 0 ? `<span class="linked-docs-badge" onclick="event.stopPropagation(); showSessionInfoModal('${session.id}')" title="${linkedDocsCount} linked document${linkedDocsCount > 1 ? 's' : ''}">${linkSvg(10)}${linkedDocsCount}</span>` : ''}
|
|
2948
2949
|
${bookmarksCount > 0 ? `<span class="bookmarks-badge" onclick="event.stopPropagation(); openSessionWithBookmarks('${session.id}')" title="${bookmarksCount} bookmarked message${bookmarksCount > 1 ? 's' : ''}"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>${bookmarksCount}</span>` : ''}
|
|
2949
2950
|
${hasScratchpad ? `<span class="scratchpad-badge" onclick="event.stopPropagation(); openSessionScratchpad('${session.id}')" title="Open scratchpad"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg></span>` : ''}
|
|
@@ -4589,7 +4590,14 @@ function matchKey(e, ...keys) {
|
|
|
4589
4590
|
return keys.some((k) => e.key === k || e.code === k);
|
|
4590
4591
|
}
|
|
4591
4592
|
|
|
4592
|
-
const MODAL_ESC_PRIORITY = [
|
|
4593
|
+
const MODAL_ESC_PRIORITY = [
|
|
4594
|
+
'preview-modal',
|
|
4595
|
+
'msg-detail-modal',
|
|
4596
|
+
'tool-stats-modal',
|
|
4597
|
+
'plan-modal',
|
|
4598
|
+
'loop-modal',
|
|
4599
|
+
'workflow-modal',
|
|
4600
|
+
];
|
|
4593
4601
|
const MODAL_CLOSERS = {
|
|
4594
4602
|
'preview-modal': () => closePreviewModal(),
|
|
4595
4603
|
'msg-detail-modal': () => {
|
|
@@ -4599,6 +4607,7 @@ const MODAL_CLOSERS = {
|
|
|
4599
4607
|
'tool-stats-modal': () => closeToolStatsModal(),
|
|
4600
4608
|
'plan-modal': () => closePlanModal(),
|
|
4601
4609
|
'loop-modal': () => closeLoopModal(),
|
|
4610
|
+
'workflow-modal': () => closeWorkflowModal(),
|
|
4602
4611
|
'team-modal': () => closeTeamModal(),
|
|
4603
4612
|
'agent-modal': () => closeAgentModal(),
|
|
4604
4613
|
'help-modal': () => closeHelpModal(),
|
|
@@ -6477,6 +6486,134 @@ function closeLoopModal() {
|
|
|
6477
6486
|
document.getElementById('loop-modal').classList.remove('visible');
|
|
6478
6487
|
}
|
|
6479
6488
|
|
|
6489
|
+
const WORKFLOW_GEAR_SVG =
|
|
6490
|
+
'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>';
|
|
6491
|
+
|
|
6492
|
+
// Standard "open in editor" glyph — matches the message-detail / plan modals.
|
|
6493
|
+
const ICON_OPEN_EDITOR =
|
|
6494
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>';
|
|
6495
|
+
|
|
6496
|
+
function renderWorkflowBadge(session) {
|
|
6497
|
+
if (!session.hasWorkflow) return '';
|
|
6498
|
+
const n = session.workflowCount || 0;
|
|
6499
|
+
const tip = `${n} workflow${n === 1 ? '' : 's'}`;
|
|
6500
|
+
return `<span class="workflow-badge" onclick="event.stopPropagation(); showWorkflowModal('${session.id}')" title="${escapeHtml(tip)}">${WORKFLOW_GEAR_SVG}</span>`;
|
|
6501
|
+
}
|
|
6502
|
+
|
|
6503
|
+
let _workflowSessionId = null;
|
|
6504
|
+
let _workflowHeaderId = null;
|
|
6505
|
+
|
|
6506
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
6507
|
+
function showWorkflowModal(sessionId) {
|
|
6508
|
+
_workflowSessionId = sessionId;
|
|
6509
|
+
setWorkflowHeader('Workflows', null);
|
|
6510
|
+
const body = document.getElementById('workflow-modal-body');
|
|
6511
|
+
body.innerHTML = '<div style="padding:16px;color:var(--text-secondary);">Loading…</div>';
|
|
6512
|
+
document.getElementById('workflow-modal').classList.add('visible');
|
|
6513
|
+
fetch(`/api/sessions/${sessionId}/workflows`)
|
|
6514
|
+
.then((r) => (r.ok ? r.json() : { workflows: [] }))
|
|
6515
|
+
.catch(() => ({ workflows: [] }))
|
|
6516
|
+
.then((data) => renderWorkflowList(data.workflows || []));
|
|
6517
|
+
}
|
|
6518
|
+
|
|
6519
|
+
function setWorkflowHeader(name, wfId) {
|
|
6520
|
+
const titleText = document.getElementById('workflow-modal-title-text');
|
|
6521
|
+
const headerBtn = document.getElementById('workflow-modal-open-btn');
|
|
6522
|
+
_workflowHeaderId = wfId || null;
|
|
6523
|
+
if (titleText) {
|
|
6524
|
+
titleText.textContent = name;
|
|
6525
|
+
titleText.title = wfId ? name : '';
|
|
6526
|
+
}
|
|
6527
|
+
if (headerBtn) headerBtn.style.display = wfId ? '' : 'none';
|
|
6528
|
+
}
|
|
6529
|
+
|
|
6530
|
+
function renderWorkflowList(workflows) {
|
|
6531
|
+
const body = document.getElementById('workflow-modal-body');
|
|
6532
|
+
if (!workflows.length) {
|
|
6533
|
+
setWorkflowHeader('Workflows', null);
|
|
6534
|
+
body.innerHTML =
|
|
6535
|
+
'<div style="padding:24px;text-align:center;color:var(--text-secondary);">No workflow scripts for this session.</div>';
|
|
6536
|
+
return;
|
|
6537
|
+
}
|
|
6538
|
+
// Single workflow: promote its name to the header and open-button next to the close (X).
|
|
6539
|
+
if (workflows.length === 1) {
|
|
6540
|
+
const w = workflows[0];
|
|
6541
|
+
setWorkflowHeader(w.name || w.id, w.id);
|
|
6542
|
+
const when = w.modifiedAt ? formatDate(w.modifiedAt) : '';
|
|
6543
|
+
body.innerHTML = `<div class="workflow-single">
|
|
6544
|
+
<div class="workflow-row-meta"><code>${escapeHtml(w.id)}</code>${when ? ` · ${escapeHtml(when)}` : ''}</div>
|
|
6545
|
+
<div class="workflow-row-code"></div>
|
|
6546
|
+
</div>`;
|
|
6547
|
+
loadWorkflowCode(w.id, body.querySelector('.workflow-row-code'));
|
|
6548
|
+
return;
|
|
6549
|
+
}
|
|
6550
|
+
// Multiple workflows: generic header, collapsible list with per-row open icons.
|
|
6551
|
+
setWorkflowHeader('Workflows', null);
|
|
6552
|
+
body.innerHTML = workflows
|
|
6553
|
+
.map((w) => {
|
|
6554
|
+
const when = w.modifiedAt ? formatDate(w.modifiedAt) : '';
|
|
6555
|
+
return `<div class="workflow-row" onclick="viewWorkflowScript('${escapeHtml(w.id)}', this)">
|
|
6556
|
+
<div class="workflow-row-head">
|
|
6557
|
+
<span class="workflow-row-name">${escapeHtml(w.name || w.id)}</span>
|
|
6558
|
+
<span class="workflow-row-id"><code>${escapeHtml(w.id)}</code></span>
|
|
6559
|
+
<button class="icon-btn workflow-open-icon" aria-label="Open in editor" title="Open in editor" onclick="event.stopPropagation(); openWorkflowInEditor('${escapeHtml(w.id)}')">${ICON_OPEN_EDITOR}</button>
|
|
6560
|
+
</div>
|
|
6561
|
+
${when ? `<div class="workflow-row-meta">${escapeHtml(when)}</div>` : ''}
|
|
6562
|
+
<div class="workflow-row-code"></div>
|
|
6563
|
+
</div>`;
|
|
6564
|
+
})
|
|
6565
|
+
.join('');
|
|
6566
|
+
}
|
|
6567
|
+
|
|
6568
|
+
function loadWorkflowCode(wfId, codeEl) {
|
|
6569
|
+
if (!codeEl) return;
|
|
6570
|
+
codeEl.innerHTML = '<div style="padding:8px 0;color:var(--text-secondary);">Loading…</div>';
|
|
6571
|
+
fetch(`/api/sessions/${_workflowSessionId}/workflows/${encodeURIComponent(wfId)}`)
|
|
6572
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
6573
|
+
.catch(() => null)
|
|
6574
|
+
.then((data) => {
|
|
6575
|
+
if (!data?.content) {
|
|
6576
|
+
codeEl.innerHTML = '<div style="padding:8px 0;color:var(--text-secondary);">Failed to load script.</div>';
|
|
6577
|
+
return;
|
|
6578
|
+
}
|
|
6579
|
+
let highlighted;
|
|
6580
|
+
if (typeof hljs !== 'undefined' && hljs.getLanguage('javascript')) {
|
|
6581
|
+
highlighted = hljs.highlight(data.content, { language: 'javascript' }).value;
|
|
6582
|
+
} else {
|
|
6583
|
+
highlighted = escapeHtml(data.content);
|
|
6584
|
+
}
|
|
6585
|
+
codeEl.innerHTML = `<pre><code class="hljs language-javascript">${highlighted}</code></pre>`;
|
|
6586
|
+
});
|
|
6587
|
+
}
|
|
6588
|
+
|
|
6589
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
6590
|
+
function viewWorkflowScript(wfId, rowEl) {
|
|
6591
|
+
const codeEl = rowEl.querySelector('.workflow-row-code');
|
|
6592
|
+
if (!codeEl) return;
|
|
6593
|
+
if (rowEl.classList.contains('expanded')) {
|
|
6594
|
+
rowEl.classList.remove('expanded');
|
|
6595
|
+
codeEl.innerHTML = '';
|
|
6596
|
+
return;
|
|
6597
|
+
}
|
|
6598
|
+
rowEl.classList.add('expanded');
|
|
6599
|
+
loadWorkflowCode(wfId, codeEl);
|
|
6600
|
+
}
|
|
6601
|
+
|
|
6602
|
+
function openWorkflowInEditor(wfId) {
|
|
6603
|
+
postAndToast(`/api/sessions/${_workflowSessionId}/workflows/${encodeURIComponent(wfId)}/open`, {}, 'in editor');
|
|
6604
|
+
}
|
|
6605
|
+
|
|
6606
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
6607
|
+
function openWorkflowHeaderScript() {
|
|
6608
|
+
if (_workflowHeaderId) openWorkflowInEditor(_workflowHeaderId);
|
|
6609
|
+
}
|
|
6610
|
+
|
|
6611
|
+
function closeWorkflowModal() {
|
|
6612
|
+
document.getElementById('workflow-modal').classList.remove('visible');
|
|
6613
|
+
_workflowSessionId = null;
|
|
6614
|
+
setWorkflowHeader('Workflows', null);
|
|
6615
|
+
}
|
|
6616
|
+
|
|
6480
6617
|
function openPlanForSession(sid) {
|
|
6481
6618
|
fetch(`/api/sessions/${sid}/plan`)
|
|
6482
6619
|
.then((r) => (r.ok ? r.json() : null))
|
package/public/index.html
CHANGED
|
@@ -604,6 +604,30 @@
|
|
|
604
604
|
</div>
|
|
605
605
|
</div>
|
|
606
606
|
|
|
607
|
+
<!-- Workflow Scripts Modal -->
|
|
608
|
+
<div id="workflow-modal" class="modal-overlay" onclick="closeWorkflowModal()">
|
|
609
|
+
<div class="modal plan-modal" onclick="event.stopPropagation()">
|
|
610
|
+
<div class="modal-header">
|
|
611
|
+
<h3 id="workflow-modal-title" class="modal-title" style="display:flex;align-items:center;gap:8px;min-width:0;">
|
|
612
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:18px;height:18px;flex-shrink:0;"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
|
613
|
+
<span id="workflow-modal-title-text" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">Workflows</span>
|
|
614
|
+
</h3>
|
|
615
|
+
<div style="display:flex;align-items:center;gap:4px;">
|
|
616
|
+
<button id="workflow-modal-open-btn" class="icon-btn" aria-label="Open in editor" title="Open in editor" style="display:none;" onclick="openWorkflowHeaderScript()">
|
|
617
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
|
618
|
+
</button>
|
|
619
|
+
<button class="modal-close" aria-label="Close dialog" onclick="closeWorkflowModal()">
|
|
620
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
|
621
|
+
</button>
|
|
622
|
+
</div>
|
|
623
|
+
</div>
|
|
624
|
+
<div id="workflow-modal-body" class="modal-body" style="overflow-y:auto;flex:1 1 auto;min-height:0;"></div>
|
|
625
|
+
<div class="modal-footer">
|
|
626
|
+
<button class="btn btn-secondary" onclick="closeWorkflowModal()">Close</button>
|
|
627
|
+
</div>
|
|
628
|
+
</div>
|
|
629
|
+
</div>
|
|
630
|
+
|
|
607
631
|
<div id="agent-modal" class="modal-overlay plan-modal-overlay" onclick="closeAgentModal()">
|
|
608
632
|
<div class="modal plan-modal" onclick="event.stopPropagation()">
|
|
609
633
|
<div class="modal-header">
|
package/public/style.css
CHANGED
|
@@ -1573,7 +1573,7 @@ body::before {
|
|
|
1573
1573
|
.session-indicators {
|
|
1574
1574
|
display: flex;
|
|
1575
1575
|
align-items: center;
|
|
1576
|
-
gap:
|
|
1576
|
+
gap: 2px;
|
|
1577
1577
|
flex-shrink: 0;
|
|
1578
1578
|
}
|
|
1579
1579
|
|
|
@@ -1640,26 +1640,116 @@ body::before {
|
|
|
1640
1640
|
border-color: var(--plan);
|
|
1641
1641
|
}
|
|
1642
1642
|
|
|
1643
|
+
/* Match .team-info-btn so the wakeups clock blends with the other session-card icons. */
|
|
1643
1644
|
.loop-badge {
|
|
1644
1645
|
width: 24px;
|
|
1645
1646
|
height: 24px;
|
|
1646
1647
|
display: inline-flex;
|
|
1647
1648
|
align-items: center;
|
|
1648
1649
|
justify-content: center;
|
|
1649
|
-
background:
|
|
1650
|
+
background: transparent;
|
|
1650
1651
|
border: 1px solid transparent;
|
|
1651
1652
|
border-radius: 4px;
|
|
1652
|
-
color: var(--text-
|
|
1653
|
+
color: var(--text-muted);
|
|
1653
1654
|
cursor: pointer;
|
|
1654
1655
|
flex-shrink: 0;
|
|
1655
1656
|
transition: all 0.15s ease;
|
|
1656
1657
|
}
|
|
1657
1658
|
|
|
1658
1659
|
.loop-badge:hover {
|
|
1660
|
+
background: var(--bg-hover);
|
|
1661
|
+
border-color: var(--border);
|
|
1662
|
+
color: var(--text-secondary);
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
/* #region WORKFLOW */
|
|
1666
|
+
/* Match .team-info-btn so the gear blends with the other session-card icons. */
|
|
1667
|
+
.workflow-badge {
|
|
1668
|
+
width: 24px;
|
|
1669
|
+
height: 24px;
|
|
1670
|
+
display: inline-flex;
|
|
1671
|
+
align-items: center;
|
|
1672
|
+
justify-content: center;
|
|
1673
|
+
background: transparent;
|
|
1674
|
+
border: 1px solid transparent;
|
|
1675
|
+
border-radius: 4px;
|
|
1676
|
+
color: var(--text-muted);
|
|
1677
|
+
cursor: pointer;
|
|
1678
|
+
flex-shrink: 0;
|
|
1679
|
+
transition: all 0.15s ease;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
.workflow-badge:hover {
|
|
1683
|
+
background: var(--bg-hover);
|
|
1684
|
+
border-color: var(--border);
|
|
1685
|
+
color: var(--text-secondary);
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
.workflow-row {
|
|
1689
|
+
border: 1px solid var(--border);
|
|
1690
|
+
border-radius: 6px;
|
|
1691
|
+
padding: 10px 12px;
|
|
1692
|
+
margin-bottom: 8px;
|
|
1693
|
+
cursor: pointer;
|
|
1694
|
+
transition: border-color 0.15s ease;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
.workflow-row:hover {
|
|
1659
1698
|
border-color: var(--accent, var(--plan));
|
|
1660
|
-
color: var(--accent, var(--plan));
|
|
1661
1699
|
}
|
|
1662
1700
|
|
|
1701
|
+
.workflow-row.expanded {
|
|
1702
|
+
cursor: default;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
.workflow-row-head {
|
|
1706
|
+
display: flex;
|
|
1707
|
+
align-items: center;
|
|
1708
|
+
gap: 8px;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
.workflow-open-icon {
|
|
1712
|
+
margin-left: auto;
|
|
1713
|
+
flex-shrink: 0;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
.workflow-row-name {
|
|
1717
|
+
font-weight: 600;
|
|
1718
|
+
color: var(--text-primary);
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
.workflow-row-id code {
|
|
1722
|
+
font-size: 11px;
|
|
1723
|
+
color: var(--text-secondary);
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
.workflow-row-meta {
|
|
1727
|
+
margin-top: 2px;
|
|
1728
|
+
font-size: 11px;
|
|
1729
|
+
color: var(--text-secondary);
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
.workflow-single .workflow-row-meta {
|
|
1733
|
+
margin: 0 0 10px;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
.workflow-row-code:not(:empty) {
|
|
1737
|
+
margin-top: 10px;
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
.workflow-single .workflow-row-code:not(:empty) {
|
|
1741
|
+
margin-top: 0;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
.workflow-row-code pre {
|
|
1745
|
+
margin: 0;
|
|
1746
|
+
max-height: 60vh;
|
|
1747
|
+
overflow: auto;
|
|
1748
|
+
border-radius: 6px;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
/* #endregion */
|
|
1752
|
+
|
|
1663
1753
|
.loop-section-title {
|
|
1664
1754
|
margin: 14px 0 8px;
|
|
1665
1755
|
font-size: 12px;
|
package/server.js
CHANGED
|
@@ -54,7 +54,7 @@ function getClaudeDir() {
|
|
|
54
54
|
return dir.startsWith('~') ? dir.replace('~', os.homedir()) : dir;
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
-
return process.env.CLAUDE_DIR || path.join(os.homedir(), '.claude');
|
|
57
|
+
return process.env.CLAUDE_CONFIG_DIR || process.env.CLAUDE_DIR || path.join(os.homedir(), '.claude');
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
function getArgUrl(argName, envName) {
|
|
@@ -743,6 +743,72 @@ function loadSessionMetadata() {
|
|
|
743
743
|
return metadata;
|
|
744
744
|
}
|
|
745
745
|
|
|
746
|
+
// Workflow (Workflow tool) scripts are persisted at
|
|
747
|
+
// projects/<projEnc>/<sessionId>/workflows/scripts/<name>-<wf_id>.js
|
|
748
|
+
// The script's projEnc can differ from the session's own JSONL dir (the workflow
|
|
749
|
+
// may run from a different cwd), so we scan every project dir into a TTL-cached
|
|
750
|
+
// index rather than deriving the path from meta.jsonlPath. Isolated from the
|
|
751
|
+
// session-scan hot path: a single Map lookup per buildSessionObject call.
|
|
752
|
+
const WORKFLOW_INDEX_TTL_MS = 5000;
|
|
753
|
+
let workflowIndexCache = null; // Map<sessionId, Array<{id,name,path,mtimeMs}>>
|
|
754
|
+
let workflowIndexBuiltAt = 0;
|
|
755
|
+
|
|
756
|
+
function buildWorkflowIndex() {
|
|
757
|
+
const index = new Map();
|
|
758
|
+
if (!existsSync(PROJECTS_DIR)) return index;
|
|
759
|
+
let projs;
|
|
760
|
+
try { projs = readdirSync(PROJECTS_DIR, { withFileTypes: true }); } catch { return index; }
|
|
761
|
+
for (const proj of projs) {
|
|
762
|
+
if (!proj.isDirectory()) continue;
|
|
763
|
+
const projPath = path.join(PROJECTS_DIR, proj.name);
|
|
764
|
+
let sessDirs;
|
|
765
|
+
try { sessDirs = readdirSync(projPath, { withFileTypes: true }); } catch { continue; }
|
|
766
|
+
for (const sess of sessDirs) {
|
|
767
|
+
if (!sess.isDirectory() || !isUUID(sess.name)) continue;
|
|
768
|
+
const scriptsDir = path.join(projPath, sess.name, 'workflows', 'scripts');
|
|
769
|
+
let scripts;
|
|
770
|
+
try { scripts = readdirSync(scriptsDir); } catch { continue; } // ENOENT for most — skip fast
|
|
771
|
+
for (const f of scripts) {
|
|
772
|
+
if (!f.endsWith('.js')) continue;
|
|
773
|
+
const full = path.join(scriptsDir, f);
|
|
774
|
+
let mtimeMs = 0;
|
|
775
|
+
try { mtimeMs = statSync(full).mtimeMs; } catch {}
|
|
776
|
+
const base = f.slice(0, -3);
|
|
777
|
+
const m = base.match(/^(.*)-(wf_[a-z0-9-]+)$/i);
|
|
778
|
+
const entry = { id: m ? m[2] : base, name: m ? m[1] : base, path: full, mtimeMs };
|
|
779
|
+
if (!index.has(sess.name)) index.set(sess.name, []);
|
|
780
|
+
index.get(sess.name).push(entry);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return index;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function getWorkflowIndex() {
|
|
788
|
+
const now = Date.now();
|
|
789
|
+
if (workflowIndexCache && now - workflowIndexBuiltAt < WORKFLOW_INDEX_TTL_MS) return workflowIndexCache;
|
|
790
|
+
workflowIndexCache = buildWorkflowIndex();
|
|
791
|
+
workflowIndexBuiltAt = now;
|
|
792
|
+
return workflowIndexCache;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Resolve a session's workflow scripts, newest first. Tries the raw id then the
|
|
796
|
+
// team-lead resolution (a team session's scripts live under the lead's dir).
|
|
797
|
+
function getWorkflowScripts(sessionId) {
|
|
798
|
+
const idx = getWorkflowIndex();
|
|
799
|
+
let list = idx.get(sessionId);
|
|
800
|
+
if (!list || !list.length) {
|
|
801
|
+
const alt = resolveSessionId(sessionId);
|
|
802
|
+
if (alt && alt !== sessionId) list = idx.get(alt);
|
|
803
|
+
}
|
|
804
|
+
return list ? [...list].sort((a, b) => b.mtimeMs - a.mtimeMs) : [];
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function getWorkflowInfoSummary(sessionId) {
|
|
808
|
+
const list = getWorkflowIndex().get(sessionId);
|
|
809
|
+
return { hasWorkflow: !!(list && list.length), workflowCount: list ? list.length : 0 };
|
|
810
|
+
}
|
|
811
|
+
|
|
746
812
|
function getPlanInfo(slug) {
|
|
747
813
|
if (!slug) return { hasPlan: false, planTitle: null, planPath: null };
|
|
748
814
|
const planPath = path.join(PLANS_DIR, `${slug}.md`);
|
|
@@ -839,6 +905,7 @@ function buildSessionObject(id, meta, overrides = {}) {
|
|
|
839
905
|
projectDir: meta.jsonlPath ? path.dirname(meta.jsonlPath) : null,
|
|
840
906
|
contextStatus: getContextStatus(id, meta),
|
|
841
907
|
...getPlanInfo(meta.slug),
|
|
908
|
+
...getWorkflowInfoSummary(id),
|
|
842
909
|
loopInfo: getLoopInfoSummary(meta),
|
|
843
910
|
...overrides,
|
|
844
911
|
// Remove internal-only field
|
|
@@ -1344,11 +1411,53 @@ app.get('/api/sessions/:sessionId/loop', (req, res) => {
|
|
|
1344
1411
|
}
|
|
1345
1412
|
});
|
|
1346
1413
|
|
|
1414
|
+
// API: List workflow scripts for a session
|
|
1415
|
+
app.get('/api/sessions/:sessionId/workflows', (req, res) => {
|
|
1416
|
+
try {
|
|
1417
|
+
const workflows = getWorkflowScripts(req.params.sessionId).map((w) => ({
|
|
1418
|
+
id: w.id,
|
|
1419
|
+
name: w.name,
|
|
1420
|
+
modifiedAt: w.mtimeMs ? new Date(w.mtimeMs).toISOString() : null,
|
|
1421
|
+
}));
|
|
1422
|
+
res.json({ workflows });
|
|
1423
|
+
} catch (error) {
|
|
1424
|
+
console.error('Error listing workflows:', error);
|
|
1425
|
+
res.status(500).json({ error: 'Failed to list workflows' });
|
|
1426
|
+
}
|
|
1427
|
+
});
|
|
1428
|
+
|
|
1429
|
+
// API: Get a single workflow script's source. Looked up by id from the session's
|
|
1430
|
+
// own script list (never built from the param), so the id can't traverse paths.
|
|
1431
|
+
app.get('/api/sessions/:sessionId/workflows/:wfId', async (req, res) => {
|
|
1432
|
+
try {
|
|
1433
|
+
const wf = getWorkflowScripts(req.params.sessionId).find((w) => w.id === req.params.wfId);
|
|
1434
|
+
if (!wf) return res.status(404).json({ error: 'Workflow script not found' });
|
|
1435
|
+
const content = await fs.readFile(wf.path, 'utf8');
|
|
1436
|
+
res.json({ id: wf.id, name: wf.name, content });
|
|
1437
|
+
} catch (error) {
|
|
1438
|
+
console.error('Error reading workflow script:', error);
|
|
1439
|
+
res.status(500).json({ error: 'Failed to read workflow script' });
|
|
1440
|
+
}
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1347
1443
|
function openInEditor(...targets) {
|
|
1348
1444
|
const editor = process.env.EDITOR || 'code';
|
|
1349
1445
|
spawn(editor, ['-n', ...targets], { shell: true, stdio: 'ignore', detached: true }).unref();
|
|
1350
1446
|
}
|
|
1351
1447
|
|
|
1448
|
+
// API: Open a workflow script in VS Code
|
|
1449
|
+
app.post('/api/sessions/:sessionId/workflows/:wfId/open', (req, res) => {
|
|
1450
|
+
try {
|
|
1451
|
+
const wf = getWorkflowScripts(req.params.sessionId).find((w) => w.id === req.params.wfId);
|
|
1452
|
+
if (!wf) return res.status(404).json({ error: 'Workflow script not found' });
|
|
1453
|
+
openInEditor(wf.path);
|
|
1454
|
+
res.json({ success: true });
|
|
1455
|
+
} catch (error) {
|
|
1456
|
+
console.error('Error opening workflow script in editor:', error);
|
|
1457
|
+
res.status(500).json({ error: 'Failed to open workflow script' });
|
|
1458
|
+
}
|
|
1459
|
+
});
|
|
1460
|
+
|
|
1352
1461
|
// API: Open session plan in VS Code
|
|
1353
1462
|
app.post('/api/sessions/:sessionId/plan/open', (req, res) => {
|
|
1354
1463
|
try {
|
|
@@ -1526,7 +1635,7 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
|
|
|
1526
1635
|
flag: 'promptUnavailable',
|
|
1527
1636
|
lookup: (a) => {
|
|
1528
1637
|
if (byAgentId[a.agentId]) return byAgentId[a.agentId];
|
|
1529
|
-
try { return extractPromptFromTranscript(
|
|
1638
|
+
try { return extractPromptFromTranscript(subagentJsonlForExtraction(meta, a.agentId)); } catch (_) { return null; }
|
|
1530
1639
|
},
|
|
1531
1640
|
},
|
|
1532
1641
|
{ field: 'agentName', flag: 'agentNameUnavailable', lookup: (a) => nameByAgentId[a.agentId] || null },
|
|
@@ -1536,7 +1645,10 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
|
|
|
1536
1645
|
for (const { field, flag, lookup } of reconcileFields) {
|
|
1537
1646
|
for (const agent of agents) {
|
|
1538
1647
|
if (agent[field]) continue;
|
|
1539
|
-
|
|
1648
|
+
// Workflow subagents nest their transcript under subagents/workflows/;
|
|
1649
|
+
// pre-fix builds latched *Unavailable using the flat path, so always
|
|
1650
|
+
// re-attempt for them (the extractor resolves the nested path now).
|
|
1651
|
+
if (agent[flag] && !isAgentLive(agent) && agent.type !== 'workflow-subagent') continue;
|
|
1540
1652
|
const value = lookup(agent);
|
|
1541
1653
|
if (value) {
|
|
1542
1654
|
agent[field] = value;
|
|
@@ -1556,7 +1668,7 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
|
|
|
1556
1668
|
if (agentsNeedingModel.length && meta.jsonlPath) {
|
|
1557
1669
|
for (const agent of agentsNeedingModel) {
|
|
1558
1670
|
let model = null;
|
|
1559
|
-
try { model = extractModelFromTranscript(
|
|
1671
|
+
try { model = extractModelFromTranscript(subagentJsonlForExtraction(meta, agent.agentId)); } catch (_) {}
|
|
1560
1672
|
if (model) {
|
|
1561
1673
|
agent.model = model;
|
|
1562
1674
|
delete agent.modelUnavailable;
|
|
@@ -1651,13 +1763,38 @@ function sanitizeAgentId(raw) {
|
|
|
1651
1763
|
return path.basename(raw).replace(/[^a-zA-Z0-9_-]/g, '');
|
|
1652
1764
|
}
|
|
1653
1765
|
|
|
1766
|
+
function sessionDirFromMeta(meta) {
|
|
1767
|
+
return path.join(path.dirname(meta.jsonlPath), path.basename(meta.jsonlPath, '.jsonl'));
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1654
1770
|
function subagentJsonlPath(meta, agentId) {
|
|
1655
|
-
return path.join(
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1771
|
+
return path.join(sessionDirFromMeta(meta), 'subagents', 'agent-' + agentId + '.jsonl');
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// A subagent transcript is normally at <sessionDir>/subagents/agent-<id>.jsonl.
|
|
1775
|
+
// Workflow-tool subagents nest one level deeper, under
|
|
1776
|
+
// <sessionDir>/subagents/workflows/<wf_id>/agent-<id>.jsonl. Return whichever
|
|
1777
|
+
// exists (flat first), else null.
|
|
1778
|
+
function subagentJsonlInDir(sessionDir, agentId) {
|
|
1779
|
+
const flat = path.join(sessionDir, 'subagents', 'agent-' + agentId + '.jsonl');
|
|
1780
|
+
if (existsSync(flat)) return flat;
|
|
1781
|
+
const wfRoot = path.join(sessionDir, 'subagents', 'workflows');
|
|
1782
|
+
let wfDirs;
|
|
1783
|
+
try { wfDirs = readdirSync(wfRoot, { withFileTypes: true }); } catch { return null; }
|
|
1784
|
+
for (const wf of wfDirs) {
|
|
1785
|
+
if (!wf.isDirectory()) continue;
|
|
1786
|
+
const nested = path.join(wfRoot, wf.name, 'agent-' + agentId + '.jsonl');
|
|
1787
|
+
if (existsSync(nested)) return nested;
|
|
1788
|
+
}
|
|
1789
|
+
return null;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
// Path for prompt/model extraction in agent enrichment: prefer the nested
|
|
1793
|
+
// workflow-subagent transcript when the flat path is absent. Falls back to the
|
|
1794
|
+
// flat path (which extract* handle gracefully when missing).
|
|
1795
|
+
function subagentJsonlForExtraction(meta, agentId) {
|
|
1796
|
+
const resolved = meta.jsonlPath ? subagentJsonlInDir(sessionDirFromMeta(meta), agentId) : null;
|
|
1797
|
+
return resolved || subagentJsonlPath(meta, agentId);
|
|
1661
1798
|
}
|
|
1662
1799
|
|
|
1663
1800
|
// Claude Code can scatter a session's records across multiple project dirs
|
|
@@ -1666,20 +1803,21 @@ function subagentJsonlPath(meta, agentId) {
|
|
|
1666
1803
|
// parent sessionId. Fall back to scanning when the derived path is missing.
|
|
1667
1804
|
const subagentPathCache = new Map();
|
|
1668
1805
|
function findSubagentJsonlInProject(projPath, sessionId, agentId) {
|
|
1669
|
-
const sameSid = path.join(projPath, sessionId,
|
|
1670
|
-
if (
|
|
1806
|
+
const sameSid = subagentJsonlInDir(path.join(projPath, sessionId), agentId);
|
|
1807
|
+
if (sameSid) return sameSid;
|
|
1671
1808
|
let sessions;
|
|
1672
1809
|
try { sessions = readdirSync(projPath, { withFileTypes: true }); } catch { return null; }
|
|
1673
1810
|
for (const sess of sessions) {
|
|
1674
1811
|
if (!sess.isDirectory() || sess.name === sessionId) continue;
|
|
1675
|
-
const candidate = path.join(projPath, sess.name,
|
|
1676
|
-
if (
|
|
1812
|
+
const candidate = subagentJsonlInDir(path.join(projPath, sess.name), agentId);
|
|
1813
|
+
if (candidate) return candidate;
|
|
1677
1814
|
}
|
|
1678
1815
|
return null;
|
|
1679
1816
|
}
|
|
1680
1817
|
function resolveSubagentJsonl(meta, sessionId, agentId) {
|
|
1681
|
-
|
|
1682
|
-
|
|
1818
|
+
// Same session dir: flat path or nested under subagents/workflows/ (both checked here).
|
|
1819
|
+
const local = meta.jsonlPath ? subagentJsonlInDir(sessionDirFromMeta(meta), agentId) : null;
|
|
1820
|
+
if (local) return local;
|
|
1683
1821
|
const key = sessionId + '/' + agentId;
|
|
1684
1822
|
const cached = subagentPathCache.get(key);
|
|
1685
1823
|
if (cached) return cached;
|
|
@@ -1687,8 +1825,8 @@ function resolveSubagentJsonl(meta, sessionId, agentId) {
|
|
|
1687
1825
|
const parent = lookupParentSession(sessionId);
|
|
1688
1826
|
if (parent.parentSessionId && parent.parentJsonlPath) {
|
|
1689
1827
|
const projDir = path.dirname(parent.parentJsonlPath);
|
|
1690
|
-
const candidate = path.join(projDir, parent.parentSessionId,
|
|
1691
|
-
if (
|
|
1828
|
+
const candidate = subagentJsonlInDir(path.join(projDir, parent.parentSessionId), agentId);
|
|
1829
|
+
if (candidate) found = candidate;
|
|
1692
1830
|
}
|
|
1693
1831
|
if (!found) {
|
|
1694
1832
|
try {
|
|
@@ -1700,7 +1838,7 @@ function resolveSubagentJsonl(meta, sessionId, agentId) {
|
|
|
1700
1838
|
} catch (_) { /* projects dir missing */ }
|
|
1701
1839
|
}
|
|
1702
1840
|
if (found) subagentPathCache.set(key, found);
|
|
1703
|
-
return found ||
|
|
1841
|
+
return found || subagentJsonlPath(meta, agentId);
|
|
1704
1842
|
}
|
|
1705
1843
|
|
|
1706
1844
|
// Claude Code creates child sessions in two ways:
|
|
@@ -2110,7 +2248,7 @@ app.get('/api/sessions/:sessionId/tool-result-image/:toolUseId/:n', (req, res) =
|
|
|
2110
2248
|
});
|
|
2111
2249
|
|
|
2112
2250
|
app.get('/api/sessions/:sessionId/cached-image/:n', (req, res) => {
|
|
2113
|
-
const img = readCachedImage(req.params.sessionId, req.params.n);
|
|
2251
|
+
const img = readCachedImage(req.params.sessionId, req.params.n, CLAUDE_DIR);
|
|
2114
2252
|
if (!img) return res.status(404).end();
|
|
2115
2253
|
res.setHeader('Content-Type', img.mediaType);
|
|
2116
2254
|
res.setHeader('Cache-Control', 'no-store');
|