claude-code-kanban 1.17.0 → 1.18.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 +18 -0
- package/hooks/agent-spy.sh +61 -0
- package/install.js +193 -0
- package/package.json +3 -1
- package/public/index.html +271 -0
- package/server.js +128 -26
package/README.md
CHANGED
|
@@ -35,6 +35,24 @@ npm install -g claude-code-kanban
|
|
|
35
35
|
claude-code-kanban --open
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
## Agent Log (subagent tracking)
|
|
39
|
+
|
|
40
|
+
The Kanban footer can show live subagent activity (start/stop/idle) when hooks are installed. A one-time setup installs a lightweight shell script and adds hook entries to `~/.claude/settings.json`.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx claude-code-kanban --install
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This will:
|
|
47
|
+
1. Copy `agent-spy.sh` to `~/.claude/hooks/` (requires `jq`)
|
|
48
|
+
2. Add `SubagentStart`, `SubagentStop`, and `TeammateIdle` hooks to settings.json
|
|
49
|
+
|
|
50
|
+
All changes are non-destructive — existing settings are preserved. To remove:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npx claude-code-kanban --uninstall
|
|
54
|
+
```
|
|
55
|
+
|
|
38
56
|
## Configuration
|
|
39
57
|
|
|
40
58
|
```bash
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tracks subagent lifecycle: one JSON file per agent, grouped by session
|
|
3
|
+
# Layout: ~/.claude/agent-activity/{sessionId}/{agentId}.json
|
|
4
|
+
|
|
5
|
+
INPUT=$(cat)
|
|
6
|
+
|
|
7
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty')
|
|
8
|
+
AGENT_ID=$(echo "$INPUT" | jq -r '.agent_id // empty')
|
|
9
|
+
EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // empty')
|
|
10
|
+
|
|
11
|
+
[ -z "$SESSION_ID" ] || [ -z "$AGENT_ID" ] && exit 0
|
|
12
|
+
|
|
13
|
+
AGENT_TYPE_RAW=$(echo "$INPUT" | jq -r '.agent_type // empty')
|
|
14
|
+
DIR="$HOME/.claude/agent-activity/$SESSION_ID"
|
|
15
|
+
FILE="$DIR/$AGENT_ID.json"
|
|
16
|
+
|
|
17
|
+
# On Start: skip if no type (internal agents like AskUserQuestion)
|
|
18
|
+
# On Stop/Idle: only skip if no existing file (never tracked)
|
|
19
|
+
if [ -z "$AGENT_TYPE_RAW" ]; then
|
|
20
|
+
if [ "$EVENT" = "SubagentStart" ]; then
|
|
21
|
+
exit 0
|
|
22
|
+
elif [ ! -f "$FILE" ]; then
|
|
23
|
+
exit 0
|
|
24
|
+
fi
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
mkdir -p "$DIR"
|
|
28
|
+
TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
29
|
+
|
|
30
|
+
if [ "$EVENT" = "SubagentStart" ]; then
|
|
31
|
+
cat > "$FILE" <<EOF
|
|
32
|
+
{"agentId":"$AGENT_ID","type":"$AGENT_TYPE_RAW","status":"active","startedAt":"$TS","updatedAt":"$TS"}
|
|
33
|
+
EOF
|
|
34
|
+
|
|
35
|
+
elif [ "$EVENT" = "SubagentStop" ]; then
|
|
36
|
+
# Read type and startedAt from existing file if available
|
|
37
|
+
AGENT_TYPE="$AGENT_TYPE_RAW"
|
|
38
|
+
STARTED_AT="$TS"
|
|
39
|
+
if [ -f "$FILE" ]; then
|
|
40
|
+
[ -z "$AGENT_TYPE" ] && AGENT_TYPE=$(jq -r '.type // "unknown"' "$FILE")
|
|
41
|
+
STARTED_AT=$(jq -r '.startedAt // empty' "$FILE")
|
|
42
|
+
[ -z "$STARTED_AT" ] && STARTED_AT="$TS"
|
|
43
|
+
fi
|
|
44
|
+
LAST_MSG=$(echo "$INPUT" | jq -r '.last_assistant_message // ""')
|
|
45
|
+
LAST_MSG_ESC=$(echo "$LAST_MSG" | jq -Rs '.')
|
|
46
|
+
cat > "$FILE" <<EOF
|
|
47
|
+
{"agentId":"$AGENT_ID","type":"$AGENT_TYPE","status":"stopped","startedAt":"$STARTED_AT","lastMessage":$LAST_MSG_ESC,"stoppedAt":"$TS","updatedAt":"$TS"}
|
|
48
|
+
EOF
|
|
49
|
+
|
|
50
|
+
elif [ "$EVENT" = "TeammateIdle" ]; then
|
|
51
|
+
AGENT_TYPE="$AGENT_TYPE_RAW"
|
|
52
|
+
STARTED_AT="$TS"
|
|
53
|
+
if [ -f "$FILE" ]; then
|
|
54
|
+
[ -z "$AGENT_TYPE" ] && AGENT_TYPE=$(jq -r '.type // "unknown"' "$FILE")
|
|
55
|
+
STARTED_AT=$(jq -r '.startedAt // empty' "$FILE")
|
|
56
|
+
[ -z "$STARTED_AT" ] && STARTED_AT="$TS"
|
|
57
|
+
fi
|
|
58
|
+
cat > "$FILE" <<EOF
|
|
59
|
+
{"agentId":"$AGENT_ID","type":"$AGENT_TYPE","status":"idle","startedAt":"$STARTED_AT","updatedAt":"$TS"}
|
|
60
|
+
EOF
|
|
61
|
+
fi
|
package/install.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
const { execSync } = require('child_process');
|
|
8
|
+
|
|
9
|
+
const CLAUDE_DIR = path.join(os.homedir(), '.claude');
|
|
10
|
+
const HOOKS_DIR = path.join(CLAUDE_DIR, 'hooks');
|
|
11
|
+
const SETTINGS_PATH = path.join(CLAUDE_DIR, 'settings.json');
|
|
12
|
+
const HOOK_SCRIPT_DEST = path.join(HOOKS_DIR, 'agent-spy.sh');
|
|
13
|
+
const HOOK_SCRIPT_SRC = path.join(__dirname, 'hooks', 'agent-spy.sh');
|
|
14
|
+
const AGENT_ACTIVITY_DIR = path.join(CLAUDE_DIR, 'agent-activity');
|
|
15
|
+
|
|
16
|
+
const HOOK_COMMAND = '~/.claude/hooks/agent-spy.sh';
|
|
17
|
+
const HOOK_EVENTS = ['SubagentStart', 'SubagentStop', 'TeammateIdle'];
|
|
18
|
+
|
|
19
|
+
// ANSI helpers
|
|
20
|
+
const green = s => `\x1b[32m${s}\x1b[0m`;
|
|
21
|
+
const yellow = s => `\x1b[33m${s}\x1b[0m`;
|
|
22
|
+
const red = s => `\x1b[31m${s}\x1b[0m`;
|
|
23
|
+
const bold = s => `\x1b[1m${s}\x1b[0m`;
|
|
24
|
+
const dim = s => `\x1b[2m${s}\x1b[0m`;
|
|
25
|
+
|
|
26
|
+
function prompt(question) {
|
|
27
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
28
|
+
return new Promise(resolve => {
|
|
29
|
+
rl.question(question, answer => {
|
|
30
|
+
rl.close();
|
|
31
|
+
resolve(!answer || answer.trim().toLowerCase() !== 'n');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function runInstall() {
|
|
37
|
+
console.log(`\n ${bold('claude-code-kanban')} — Agent Log hook installer\n`);
|
|
38
|
+
|
|
39
|
+
// 1. Check jq
|
|
40
|
+
process.stdout.write(' Checking jq... ');
|
|
41
|
+
try {
|
|
42
|
+
const ver = execSync('jq --version', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
43
|
+
console.log(green(`✓ found (${ver})`));
|
|
44
|
+
} catch {
|
|
45
|
+
console.log(yellow('⚠ not found — hook script requires jq for JSON parsing'));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 2. Hook script
|
|
49
|
+
console.log(`\n Hook script: ${dim(HOOK_SCRIPT_DEST)}`);
|
|
50
|
+
let hookInstalled = false;
|
|
51
|
+
if (fs.existsSync(HOOK_SCRIPT_DEST)) {
|
|
52
|
+
const existing = fs.readFileSync(HOOK_SCRIPT_DEST, 'utf8');
|
|
53
|
+
const bundled = fs.readFileSync(HOOK_SCRIPT_SRC, 'utf8');
|
|
54
|
+
if (existing === bundled) {
|
|
55
|
+
console.log(` ${green('✓')} Up to date`);
|
|
56
|
+
hookInstalled = true;
|
|
57
|
+
} else {
|
|
58
|
+
if (await prompt(` Different version found. Update? [Y/n] `)) {
|
|
59
|
+
fs.mkdirSync(HOOKS_DIR, { recursive: true });
|
|
60
|
+
fs.copyFileSync(HOOK_SCRIPT_SRC, HOOK_SCRIPT_DEST);
|
|
61
|
+
try { fs.chmodSync(HOOK_SCRIPT_DEST, 0o755); } catch {}
|
|
62
|
+
console.log(` ${green('✓')} Updated`);
|
|
63
|
+
hookInstalled = true;
|
|
64
|
+
} else {
|
|
65
|
+
console.log(` ${dim('Skipped')}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
if (await prompt(` Not found. Install? [Y/n] `)) {
|
|
70
|
+
fs.mkdirSync(HOOKS_DIR, { recursive: true });
|
|
71
|
+
fs.copyFileSync(HOOK_SCRIPT_SRC, HOOK_SCRIPT_DEST);
|
|
72
|
+
try { fs.chmodSync(HOOK_SCRIPT_DEST, 0o755); } catch {}
|
|
73
|
+
console.log(` ${green('✓')} Installed and set executable`);
|
|
74
|
+
hookInstalled = true;
|
|
75
|
+
} else {
|
|
76
|
+
console.log(` ${dim('Skipped')}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 3. Settings.json
|
|
81
|
+
console.log(`\n Settings: ${dim(SETTINGS_PATH)}`);
|
|
82
|
+
let settings;
|
|
83
|
+
try {
|
|
84
|
+
if (fs.existsSync(SETTINGS_PATH)) {
|
|
85
|
+
settings = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8'));
|
|
86
|
+
} else {
|
|
87
|
+
settings = {};
|
|
88
|
+
}
|
|
89
|
+
} catch (e) {
|
|
90
|
+
console.log(` ${red('✗')} Malformed JSON in settings.json — aborting settings update`);
|
|
91
|
+
printSummary(hookInstalled, false);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!settings.hooks) settings.hooks = {};
|
|
96
|
+
|
|
97
|
+
const needed = [];
|
|
98
|
+
for (const event of HOOK_EVENTS) {
|
|
99
|
+
if (!settings.hooks[event]) settings.hooks[event] = [];
|
|
100
|
+
const exists = settings.hooks[event].some(g =>
|
|
101
|
+
g.hooks?.some(h => h.command === HOOK_COMMAND)
|
|
102
|
+
);
|
|
103
|
+
if (!exists) needed.push(event);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let settingsUpdated = false;
|
|
107
|
+
if (needed.length === 0) {
|
|
108
|
+
console.log(` ${green('✓')} Already configured`);
|
|
109
|
+
settingsUpdated = true;
|
|
110
|
+
} else {
|
|
111
|
+
console.log(` Adding hooks for: ${needed.join(', ')}`);
|
|
112
|
+
if (await prompt(` Update settings? [Y/n] `)) {
|
|
113
|
+
for (const event of needed) {
|
|
114
|
+
settings.hooks[event].push({
|
|
115
|
+
matcher: '',
|
|
116
|
+
hooks: [{ type: 'command', command: HOOK_COMMAND, timeout: 5 }]
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
120
|
+
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n');
|
|
121
|
+
console.log(` ${green('✓')} ${needed.length} hook entries added`);
|
|
122
|
+
settingsUpdated = true;
|
|
123
|
+
} else {
|
|
124
|
+
console.log(` ${dim('Skipped')}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
printSummary(hookInstalled, settingsUpdated);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function printSummary(hookOk, settingsOk) {
|
|
132
|
+
console.log('');
|
|
133
|
+
if (hookOk && settingsOk) {
|
|
134
|
+
console.log(` ${green('Agent Log will appear in the Kanban footer when subagents are active.')}`);
|
|
135
|
+
} else {
|
|
136
|
+
console.log(` ${yellow('Partial install — re-run --install to complete setup.')}`);
|
|
137
|
+
}
|
|
138
|
+
console.log('');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function runUninstall() {
|
|
142
|
+
console.log(`\n ${bold('claude-code-kanban')} — Agent Log hook uninstaller\n`);
|
|
143
|
+
|
|
144
|
+
// 1. Remove hook entries from settings.json
|
|
145
|
+
if (fs.existsSync(SETTINGS_PATH)) {
|
|
146
|
+
try {
|
|
147
|
+
const settings = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8'));
|
|
148
|
+
if (settings.hooks) {
|
|
149
|
+
let removed = 0;
|
|
150
|
+
for (const event of HOOK_EVENTS) {
|
|
151
|
+
if (!Array.isArray(settings.hooks[event])) continue;
|
|
152
|
+
const before = settings.hooks[event].length;
|
|
153
|
+
settings.hooks[event] = settings.hooks[event].filter(g =>
|
|
154
|
+
!g.hooks?.some(h => h.command === HOOK_COMMAND)
|
|
155
|
+
);
|
|
156
|
+
removed += before - settings.hooks[event].length;
|
|
157
|
+
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
158
|
+
}
|
|
159
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
160
|
+
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n');
|
|
161
|
+
console.log(` Settings: ${green('✓')} Removed ${removed} hook entries`);
|
|
162
|
+
} else {
|
|
163
|
+
console.log(` Settings: ${dim('No hook entries found')}`);
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
console.log(` Settings: ${red('✗')} Could not parse settings.json`);
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
console.log(` Settings: ${dim('No settings.json found')}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 2. Remove hook script
|
|
173
|
+
if (fs.existsSync(HOOK_SCRIPT_DEST)) {
|
|
174
|
+
fs.unlinkSync(HOOK_SCRIPT_DEST);
|
|
175
|
+
console.log(` Hook script: ${green('✓')} Removed`);
|
|
176
|
+
} else {
|
|
177
|
+
console.log(` Hook script: ${dim('Not found')}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 3. Optionally remove agent-activity data
|
|
181
|
+
if (fs.existsSync(AGENT_ACTIVITY_DIR)) {
|
|
182
|
+
if (await prompt(`\n Remove agent activity data (${AGENT_ACTIVITY_DIR})? [y/N] `)) {
|
|
183
|
+
fs.rmSync(AGENT_ACTIVITY_DIR, { recursive: true, force: true });
|
|
184
|
+
console.log(` ${green('✓')} Agent activity data removed`);
|
|
185
|
+
} else {
|
|
186
|
+
console.log(` ${dim('Kept agent activity data')}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
console.log(`\n ${green('Uninstall complete.')}\n`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = { runInstall, runUninstall };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-kanban",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.0",
|
|
4
4
|
"description": "A web-based Kanban board for viewing Claude Code tasks with agent teams support",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
@@ -40,6 +40,8 @@
|
|
|
40
40
|
},
|
|
41
41
|
"files": [
|
|
42
42
|
"server.js",
|
|
43
|
+
"install.js",
|
|
44
|
+
"hooks/agent-spy.sh",
|
|
43
45
|
"public/**/*"
|
|
44
46
|
]
|
|
45
47
|
}
|
package/public/index.html
CHANGED
|
@@ -1389,6 +1389,72 @@
|
|
|
1389
1389
|
padding: 6px 10px;
|
|
1390
1390
|
}
|
|
1391
1391
|
|
|
1392
|
+
/* Agent footer panel */
|
|
1393
|
+
.agent-footer {
|
|
1394
|
+
display: none;
|
|
1395
|
+
flex-direction: column;
|
|
1396
|
+
border-top: 1px solid var(--border);
|
|
1397
|
+
background: var(--bg-surface);
|
|
1398
|
+
}
|
|
1399
|
+
.agent-footer.visible { display: flex; }
|
|
1400
|
+
.agent-footer-header {
|
|
1401
|
+
display: flex;
|
|
1402
|
+
align-items: center;
|
|
1403
|
+
justify-content: space-between;
|
|
1404
|
+
padding: 6px 24px;
|
|
1405
|
+
cursor: pointer;
|
|
1406
|
+
user-select: none;
|
|
1407
|
+
color: var(--text-tertiary);
|
|
1408
|
+
font-size: 12px;
|
|
1409
|
+
letter-spacing: 0.05em;
|
|
1410
|
+
text-transform: uppercase;
|
|
1411
|
+
}
|
|
1412
|
+
.agent-footer-header:hover { background: var(--bg-hover); }
|
|
1413
|
+
.agent-footer-toggle {
|
|
1414
|
+
background: none; border: none; color: var(--text-tertiary);
|
|
1415
|
+
cursor: pointer; font-size: 14px; padding: 2px 4px;
|
|
1416
|
+
}
|
|
1417
|
+
.agent-footer-content {
|
|
1418
|
+
display: flex;
|
|
1419
|
+
gap: 10px;
|
|
1420
|
+
padding: 0 24px 12px;
|
|
1421
|
+
overflow-x: auto;
|
|
1422
|
+
scrollbar-width: thin;
|
|
1423
|
+
}
|
|
1424
|
+
.agent-footer.collapsed .agent-footer-content { display: none; }
|
|
1425
|
+
.agent-card {
|
|
1426
|
+
display: flex;
|
|
1427
|
+
align-items: center;
|
|
1428
|
+
gap: 8px;
|
|
1429
|
+
padding: 8px 14px;
|
|
1430
|
+
background: var(--bg-elevated);
|
|
1431
|
+
border: 1px solid var(--border);
|
|
1432
|
+
border-radius: 8px;
|
|
1433
|
+
white-space: nowrap;
|
|
1434
|
+
min-width: 0;
|
|
1435
|
+
overflow: hidden;
|
|
1436
|
+
transition: opacity 0.3s;
|
|
1437
|
+
cursor: pointer;
|
|
1438
|
+
}
|
|
1439
|
+
.agent-card:hover { border-color: var(--accent); }
|
|
1440
|
+
.agent-card.fading { opacity: 0.4; }
|
|
1441
|
+
.agent-dot {
|
|
1442
|
+
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
|
|
1443
|
+
}
|
|
1444
|
+
.agent-dot.active { background: var(--success); box-shadow: 0 0 6px var(--success); }
|
|
1445
|
+
.agent-dot.idle { background: var(--warning); box-shadow: 0 0 6px var(--warning); }
|
|
1446
|
+
.agent-dot.stopped { background: var(--text-muted); }
|
|
1447
|
+
.agent-type {
|
|
1448
|
+
font-size: 13px; font-weight: 500; color: var(--text-primary);
|
|
1449
|
+
}
|
|
1450
|
+
.agent-status {
|
|
1451
|
+
font-size: 11px; color: var(--text-tertiary);
|
|
1452
|
+
}
|
|
1453
|
+
.agent-message {
|
|
1454
|
+
font-size: 11px; color: var(--text-muted);
|
|
1455
|
+
max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1392
1458
|
/* Light mode */
|
|
1393
1459
|
body.light {
|
|
1394
1460
|
--bg-deep: #e8e6e3;
|
|
@@ -2004,6 +2070,13 @@
|
|
|
2004
2070
|
<div id="completed-tasks" class="column-tasks" role="list"></div>
|
|
2005
2071
|
</div>
|
|
2006
2072
|
</div>
|
|
2073
|
+
<div id="agent-footer" class="agent-footer">
|
|
2074
|
+
<div class="agent-footer-header" onclick="toggleAgentFooter()">
|
|
2075
|
+
<span id="agent-footer-label">Agents</span>
|
|
2076
|
+
<button class="agent-footer-toggle" id="agent-footer-toggle" aria-label="Toggle agent panel">▾</button>
|
|
2077
|
+
</div>
|
|
2078
|
+
<div class="agent-footer-content" id="agent-footer-content"></div>
|
|
2079
|
+
</div>
|
|
2007
2080
|
</div>
|
|
2008
2081
|
</main>
|
|
2009
2082
|
|
|
@@ -2042,6 +2115,9 @@
|
|
|
2042
2115
|
let allTasksCache = []; // Cache all tasks for search
|
|
2043
2116
|
let bulkDeleteSessionId = null; // Track session for bulk delete
|
|
2044
2117
|
let ownerFilter = '';
|
|
2118
|
+
let currentAgents = [];
|
|
2119
|
+
let lastAgentsHash = '';
|
|
2120
|
+
let agentDurationInterval = null;
|
|
2045
2121
|
let selectedTaskId = null;
|
|
2046
2122
|
let selectedSessionId = null;
|
|
2047
2123
|
let focusZone = 'board'; // 'board' | 'sidebar'
|
|
@@ -2423,6 +2499,7 @@
|
|
|
2423
2499
|
ownerFilter = '';
|
|
2424
2500
|
updateUrl();
|
|
2425
2501
|
renderSession();
|
|
2502
|
+
fetchAgents(sessionId);
|
|
2426
2503
|
} catch (error) {
|
|
2427
2504
|
console.error('Failed to fetch tasks:', error);
|
|
2428
2505
|
currentTasks = [];
|
|
@@ -2433,11 +2510,182 @@
|
|
|
2433
2510
|
}
|
|
2434
2511
|
}
|
|
2435
2512
|
|
|
2513
|
+
const AGENT_COOLDOWN_MS = 3 * 60 * 1000;
|
|
2514
|
+
const AGENT_STALE_MS = 5 * 60 * 1000;
|
|
2515
|
+
const AGENT_LOG_MAX = 8;
|
|
2516
|
+
|
|
2517
|
+
async function fetchAgents(sessionId) {
|
|
2518
|
+
try {
|
|
2519
|
+
const res = await fetch(`/api/sessions/${sessionId}/agents`);
|
|
2520
|
+
if (!res.ok) { currentAgents = []; renderAgentFooter(); return; }
|
|
2521
|
+
const agents = await res.json();
|
|
2522
|
+
const hash = JSON.stringify(agents);
|
|
2523
|
+
if (hash === lastAgentsHash) return;
|
|
2524
|
+
lastAgentsHash = hash;
|
|
2525
|
+
currentAgents = agents;
|
|
2526
|
+
renderAgentFooter();
|
|
2527
|
+
} catch (e) {
|
|
2528
|
+
console.error('[fetchAgents]', e);
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
function formatDuration(ms) {
|
|
2533
|
+
const s = Math.floor(ms / 1000);
|
|
2534
|
+
if (s < 60) return `${s}s`;
|
|
2535
|
+
const m = Math.floor(s / 60);
|
|
2536
|
+
if (m < 60) return `${m}m ${s % 60}s`;
|
|
2537
|
+
return `${Math.floor(m / 60)}h ${m % 60}m`;
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
function renderAgentFooter() {
|
|
2541
|
+
const footer = document.getElementById('agent-footer');
|
|
2542
|
+
const content = document.getElementById('agent-footer-content');
|
|
2543
|
+
const label = document.getElementById('agent-footer-label');
|
|
2544
|
+
const now = Date.now();
|
|
2545
|
+
|
|
2546
|
+
// Mark stale "active" agents as stopped (SubagentStop unreliable for shutdown agents)
|
|
2547
|
+
const agents = currentAgents.map(a => {
|
|
2548
|
+
if (a.status === 'active' && a.startedAt && (now - new Date(a.startedAt).getTime()) > AGENT_STALE_MS) {
|
|
2549
|
+
return { ...a, status: 'stopped', stoppedAt: a.updatedAt };
|
|
2550
|
+
}
|
|
2551
|
+
return a;
|
|
2552
|
+
});
|
|
2553
|
+
// Filter shutdown ghosts: for same-type agents, keep if they overlapped (parallel)
|
|
2554
|
+
// or started >30s after previous stopped (legitimate re-spawn). Filter the rest.
|
|
2555
|
+
const byType = {};
|
|
2556
|
+
for (const a of agents) { (byType[a.type] = byType[a.type] || []).push(a); }
|
|
2557
|
+
const filtered = [];
|
|
2558
|
+
for (const group of Object.values(byType)) {
|
|
2559
|
+
group.sort((a, b) => new Date(a.startedAt || 0) - new Date(b.startedAt || 0));
|
|
2560
|
+
filtered.push(group[0]);
|
|
2561
|
+
for (let i = 1; i < group.length; i++) {
|
|
2562
|
+
const prev = group[i - 1];
|
|
2563
|
+
const prevStop = prev.stoppedAt ? new Date(prev.stoppedAt).getTime() : Infinity;
|
|
2564
|
+
const curStart = new Date(group[i].startedAt || 0).getTime();
|
|
2565
|
+
const overlapped = curStart < prevStop;
|
|
2566
|
+
const reSpawn = curStart - prevStop > 30000;
|
|
2567
|
+
if (overlapped || reSpawn) filtered.push(group[i]);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
// Sort by updatedAt desc, keep up to 7 most recent
|
|
2571
|
+
const visible = filtered
|
|
2572
|
+
.sort((a, b) => new Date(b.updatedAt || 0) - new Date(a.updatedAt || 0))
|
|
2573
|
+
.slice(0, AGENT_LOG_MAX);
|
|
2574
|
+
|
|
2575
|
+
if (visible.length === 0) {
|
|
2576
|
+
footer.classList.remove('visible');
|
|
2577
|
+
clearInterval(agentDurationInterval);
|
|
2578
|
+
agentDurationInterval = null;
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
footer.classList.add('visible');
|
|
2583
|
+
label.textContent = `Agents Log (${visible.length})`;
|
|
2584
|
+
|
|
2585
|
+
const collapsed = localStorage.getItem('agentFooterCollapsed') === 'true';
|
|
2586
|
+
footer.classList.toggle('collapsed', collapsed);
|
|
2587
|
+
document.getElementById('agent-footer-toggle').innerHTML = collapsed ? '▴' : '▾';
|
|
2588
|
+
|
|
2589
|
+
content.innerHTML = visible.map(a => {
|
|
2590
|
+
const elapsed = a.status === 'stopped' && a.stoppedAt
|
|
2591
|
+
? new Date(a.stoppedAt).getTime() - new Date(a.startedAt || a.stoppedAt).getTime()
|
|
2592
|
+
: now - new Date(a.startedAt || a.updatedAt).getTime();
|
|
2593
|
+
const statusText = a.status === 'stopped'
|
|
2594
|
+
? `stopped · ${formatDuration(elapsed)}`
|
|
2595
|
+
: a.status === 'idle'
|
|
2596
|
+
? `idle · ${formatDuration(elapsed)}`
|
|
2597
|
+
: `active · ${formatDuration(elapsed)}`;
|
|
2598
|
+
const msgTrimmed = a.lastMessage?.trim() || '';
|
|
2599
|
+
const msgTrunc = msgTrimmed.length > 60 ? msgTrimmed.substring(0, 60) + '…' : msgTrimmed;
|
|
2600
|
+
const msgHtml = msgTrunc
|
|
2601
|
+
? `<div class="agent-message" title="${escapeHtml(msgTrimmed)}">${escapeHtml(msgTrunc)}</div>` : '';
|
|
2602
|
+
return `<div class="agent-card" onclick="showAgentModal('${a.agentId}')">
|
|
2603
|
+
<span class="agent-dot ${a.status}"></span>
|
|
2604
|
+
<div>
|
|
2605
|
+
<div class="agent-type">${escapeHtml(a.type || 'unknown')}</div>
|
|
2606
|
+
<div class="agent-status">${statusText}</div>
|
|
2607
|
+
${msgHtml}
|
|
2608
|
+
</div>
|
|
2609
|
+
</div>`;
|
|
2610
|
+
}).join('');
|
|
2611
|
+
|
|
2612
|
+
clearInterval(agentDurationInterval);
|
|
2613
|
+
if (visible.some(a => a.status === 'active' || a.status === 'idle')) {
|
|
2614
|
+
agentDurationInterval = setInterval(() => renderAgentFooter(), 1000);
|
|
2615
|
+
} else {
|
|
2616
|
+
agentDurationInterval = setInterval(() => renderAgentFooter(), 10000);
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
function toggleAgentFooter() {
|
|
2621
|
+
const footer = document.getElementById('agent-footer');
|
|
2622
|
+
const collapsed = !footer.classList.contains('collapsed');
|
|
2623
|
+
footer.classList.toggle('collapsed', collapsed);
|
|
2624
|
+
localStorage.setItem('agentFooterCollapsed', collapsed);
|
|
2625
|
+
document.getElementById('agent-footer-toggle').innerHTML = collapsed ? '▴' : '▾';
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
function showAgentModal(agentId) {
|
|
2629
|
+
const agent = currentAgents.find(a => a.agentId === agentId);
|
|
2630
|
+
if (!agent) return;
|
|
2631
|
+
const modal = document.getElementById('agent-modal');
|
|
2632
|
+
const title = document.getElementById('agent-modal-title');
|
|
2633
|
+
const body = document.getElementById('agent-modal-body');
|
|
2634
|
+
const now = Date.now();
|
|
2635
|
+
const started = agent.startedAt ? new Date(agent.startedAt) : null;
|
|
2636
|
+
const stopped = agent.stoppedAt ? new Date(agent.stoppedAt) : null;
|
|
2637
|
+
const elapsed = stopped && started
|
|
2638
|
+
? stopped.getTime() - started.getTime()
|
|
2639
|
+
: started ? now - started.getTime() : 0;
|
|
2640
|
+
|
|
2641
|
+
const statusDot = `<span class="agent-dot ${agent.status}" style="display:inline-block;vertical-align:middle;margin-right:6px;"></span>`;
|
|
2642
|
+
title.innerHTML = `${statusDot} ${escapeHtml(agent.type || 'unknown')}`;
|
|
2643
|
+
|
|
2644
|
+
const rows = [
|
|
2645
|
+
['Status', agent.status],
|
|
2646
|
+
['Agent ID', `<code style="font-size:12px;color:var(--text-tertiary)">${escapeHtml(agent.agentId)}</code>`],
|
|
2647
|
+
['Duration', formatDuration(elapsed)],
|
|
2648
|
+
];
|
|
2649
|
+
if (started) rows.push(['Started', started.toLocaleTimeString()]);
|
|
2650
|
+
if (stopped) rows.push(['Stopped', stopped.toLocaleTimeString()]);
|
|
2651
|
+
|
|
2652
|
+
let html = `<table style="width:100%;border-collapse:collapse;">` +
|
|
2653
|
+
rows.map(([k, v]) =>
|
|
2654
|
+
`<tr><td style="padding:6px 12px 6px 0;color:var(--text-tertiary);white-space:nowrap;vertical-align:top;">${k}</td><td style="padding:6px 0;color:var(--text-primary);">${v}</td></tr>`
|
|
2655
|
+
).join('') + `</table>`;
|
|
2656
|
+
|
|
2657
|
+
if (agent.lastMessage) {
|
|
2658
|
+
const sanitized = typeof DOMPurify !== 'undefined' && typeof marked !== 'undefined'
|
|
2659
|
+
? DOMPurify.sanitize(marked.parse(agent.lastMessage.trim()))
|
|
2660
|
+
: `<pre style="white-space:pre-wrap;margin:0;">${escapeHtml(agent.lastMessage.trim())}</pre>`;
|
|
2661
|
+
html += `<div style="margin-top:16px;border-top:1px solid var(--border);padding-top:12px;">
|
|
2662
|
+
<div style="font-size:12px;color:var(--text-tertiary);margin-bottom:8px;text-transform:uppercase;letter-spacing:0.05em;">Last Message</div>
|
|
2663
|
+
<div class="detail-desc" style="font-size:13px;">${sanitized}</div>
|
|
2664
|
+
</div>`;
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
body.innerHTML = html;
|
|
2668
|
+
modal.classList.add('visible');
|
|
2669
|
+
const keyHandler = (e) => {
|
|
2670
|
+
if (e.key === 'Escape') {
|
|
2671
|
+
e.preventDefault();
|
|
2672
|
+
closeAgentModal();
|
|
2673
|
+
document.removeEventListener('keydown', keyHandler);
|
|
2674
|
+
}
|
|
2675
|
+
};
|
|
2676
|
+
document.addEventListener('keydown', keyHandler);
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
function closeAgentModal() {
|
|
2680
|
+
document.getElementById('agent-modal').classList.remove('visible');
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2436
2683
|
async function showAllTasks() {
|
|
2437
2684
|
try {
|
|
2438
2685
|
viewMode = 'all';
|
|
2439
2686
|
currentSessionId = null;
|
|
2440
2687
|
ownerFilter = '';
|
|
2688
|
+
currentAgents = []; lastAgentsHash = ''; renderAgentFooter();
|
|
2441
2689
|
const res = await fetch('/api/tasks/all');
|
|
2442
2690
|
let tasks = await res.json();
|
|
2443
2691
|
if (filterProject) {
|
|
@@ -3434,6 +3682,12 @@
|
|
|
3434
3682
|
refreshOpenPlan();
|
|
3435
3683
|
}
|
|
3436
3684
|
|
|
3685
|
+
if (data.type === 'agent-update') {
|
|
3686
|
+
if (currentSessionId && data.sessionId === currentSessionId) {
|
|
3687
|
+
fetchAgents(currentSessionId);
|
|
3688
|
+
}
|
|
3689
|
+
}
|
|
3690
|
+
|
|
3437
3691
|
if (data.type === 'team-update') {
|
|
3438
3692
|
console.log('[SSE] Team update:', data.teamName);
|
|
3439
3693
|
debouncedRefresh(data.teamName, false);
|
|
@@ -4168,5 +4422,22 @@
|
|
|
4168
4422
|
</div>
|
|
4169
4423
|
</div>
|
|
4170
4424
|
</div>
|
|
4425
|
+
|
|
4426
|
+
<div id="agent-modal" class="modal-overlay plan-modal-overlay" onclick="closeAgentModal()">
|
|
4427
|
+
<div class="modal plan-modal" onclick="event.stopPropagation()">
|
|
4428
|
+
<div class="modal-header">
|
|
4429
|
+
<h3 id="agent-modal-title" class="modal-title">Agent</h3>
|
|
4430
|
+
<button class="modal-close" aria-label="Close dialog" onclick="closeAgentModal()">
|
|
4431
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
4432
|
+
<path d="M18 6L6 18M6 6l12 12"/>
|
|
4433
|
+
</svg>
|
|
4434
|
+
</button>
|
|
4435
|
+
</div>
|
|
4436
|
+
<div id="agent-modal-body" class="modal-body" style="overflow-y: auto; flex: 1;"></div>
|
|
4437
|
+
<div class="modal-footer">
|
|
4438
|
+
<button class="btn btn-primary" onclick="closeAgentModal()">Close</button>
|
|
4439
|
+
</div>
|
|
4440
|
+
</div>
|
|
4441
|
+
</div>
|
|
4171
4442
|
</body>
|
|
4172
4443
|
</html>
|
package/server.js
CHANGED
|
@@ -8,6 +8,15 @@ const readline = require('readline');
|
|
|
8
8
|
const chokidar = require('chokidar');
|
|
9
9
|
const os = require('os');
|
|
10
10
|
|
|
11
|
+
const isSetupCommand = process.argv.includes('--install') || process.argv.includes('--uninstall');
|
|
12
|
+
|
|
13
|
+
if (isSetupCommand) {
|
|
14
|
+
const { runInstall, runUninstall } = require('./install');
|
|
15
|
+
(process.argv.includes('--install') ? runInstall() : runUninstall())
|
|
16
|
+
.then(() => process.exit(0))
|
|
17
|
+
.catch(e => { console.error(e.message); process.exit(1); });
|
|
18
|
+
}
|
|
19
|
+
|
|
11
20
|
const app = express();
|
|
12
21
|
const PORT = process.env.PORT || 3456;
|
|
13
22
|
|
|
@@ -32,6 +41,7 @@ const TASKS_DIR = path.join(CLAUDE_DIR, 'tasks');
|
|
|
32
41
|
const PROJECTS_DIR = path.join(CLAUDE_DIR, 'projects');
|
|
33
42
|
const TEAMS_DIR = path.join(CLAUDE_DIR, 'teams');
|
|
34
43
|
const PLANS_DIR = path.join(CLAUDE_DIR, 'plans');
|
|
44
|
+
const AGENT_ACTIVITY_DIR = path.join(CLAUDE_DIR, 'agent-activity');
|
|
35
45
|
|
|
36
46
|
function isTeamSession(sessionId) {
|
|
37
47
|
return existsSync(path.join(TEAMS_DIR, sessionId, 'config.json'));
|
|
@@ -219,13 +229,15 @@ function loadSessionMetadata() {
|
|
|
219
229
|
const project = parentMeta?.project || leadMember?.cwd || teamConfig.working_dir || null;
|
|
220
230
|
|
|
221
231
|
metadata[dir.name] = {
|
|
222
|
-
customTitle:
|
|
223
|
-
slug:
|
|
232
|
+
customTitle: teamConfig.description || dir.name,
|
|
233
|
+
slug: null,
|
|
224
234
|
project,
|
|
225
235
|
jsonlPath: parentMeta?.jsonlPath || null,
|
|
226
|
-
description:
|
|
236
|
+
description: teamConfig.description || parentMeta?.description || null,
|
|
227
237
|
gitBranch: parentMeta?.gitBranch || null,
|
|
228
|
-
created: parentMeta?.created || null
|
|
238
|
+
created: parentMeta?.created || null,
|
|
239
|
+
isTeamLeader: false,
|
|
240
|
+
teamLeaderId: teamConfig.leadSessionId || null
|
|
229
241
|
};
|
|
230
242
|
}
|
|
231
243
|
}
|
|
@@ -368,6 +380,21 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
368
380
|
}
|
|
369
381
|
}
|
|
370
382
|
|
|
383
|
+
// Hide leader UUID sessions that are represented by a team session
|
|
384
|
+
const teamLeaderIds = new Set();
|
|
385
|
+
for (const [sid, session] of sessionsMap) {
|
|
386
|
+
if (session.isTeam) {
|
|
387
|
+
const cfg = loadTeamConfig(sid);
|
|
388
|
+
if (cfg?.leadSessionId) teamLeaderIds.add(cfg.leadSessionId);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
for (const leaderId of teamLeaderIds) {
|
|
392
|
+
const session = sessionsMap.get(leaderId);
|
|
393
|
+
if (session && session.taskCount === 0) {
|
|
394
|
+
sessionsMap.delete(leaderId);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
371
398
|
// Convert map to array and sort by most recently modified
|
|
372
399
|
let sessions = Array.from(sessionsMap.values());
|
|
373
400
|
sessions.sort((a, b) => new Date(b.modifiedAt) - new Date(a.modifiedAt));
|
|
@@ -479,6 +506,30 @@ app.get('/api/teams/:name', (req, res) => {
|
|
|
479
506
|
res.json(config);
|
|
480
507
|
});
|
|
481
508
|
|
|
509
|
+
// API: Get agents for a session
|
|
510
|
+
app.get('/api/sessions/:sessionId/agents', (req, res) => {
|
|
511
|
+
let sessionId = req.params.sessionId;
|
|
512
|
+
// For team sessions, resolve to leader's session UUID
|
|
513
|
+
const teamConfig = loadTeamConfig(sessionId);
|
|
514
|
+
if (teamConfig && teamConfig.leadSessionId) {
|
|
515
|
+
sessionId = teamConfig.leadSessionId;
|
|
516
|
+
}
|
|
517
|
+
const agentDir = path.join(AGENT_ACTIVITY_DIR, sessionId);
|
|
518
|
+
if (!existsSync(agentDir)) return res.json([]);
|
|
519
|
+
try {
|
|
520
|
+
const files = readdirSync(agentDir).filter(f => f.endsWith('.json'));
|
|
521
|
+
const agents = [];
|
|
522
|
+
for (const file of files) {
|
|
523
|
+
try {
|
|
524
|
+
agents.push(JSON.parse(readFileSync(path.join(agentDir, file), 'utf8')));
|
|
525
|
+
} catch (e) { /* skip invalid */ }
|
|
526
|
+
}
|
|
527
|
+
res.json(agents);
|
|
528
|
+
} catch (e) {
|
|
529
|
+
res.json([]);
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
|
|
482
533
|
app.get('/api/version', (req, res) => {
|
|
483
534
|
const pkg = require('./package.json');
|
|
484
535
|
res.json({ version: pkg.version });
|
|
@@ -641,6 +692,13 @@ function broadcast(data) {
|
|
|
641
692
|
}
|
|
642
693
|
}
|
|
643
694
|
|
|
695
|
+
app.use('/api', (req, res) => {
|
|
696
|
+
res.status(404).json({ error: 'Not found' });
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
// File watchers and server startup (skip for --install/--uninstall)
|
|
700
|
+
if (!isSetupCommand) {
|
|
701
|
+
|
|
644
702
|
// Watch for file changes (chokidar handles non-existent paths)
|
|
645
703
|
const watcher = chokidar.watch(TASKS_DIR, {
|
|
646
704
|
persistent: true,
|
|
@@ -714,32 +772,76 @@ plansWatcher.on('all', (event, filePath) => {
|
|
|
714
772
|
}
|
|
715
773
|
});
|
|
716
774
|
|
|
717
|
-
|
|
718
|
-
|
|
775
|
+
// Watch agent-activity directory for subagent lifecycle events
|
|
776
|
+
const agentActivityWatcher = chokidar.watch(AGENT_ACTIVITY_DIR, {
|
|
777
|
+
persistent: true,
|
|
778
|
+
ignoreInitial: true,
|
|
779
|
+
depth: 2
|
|
719
780
|
});
|
|
720
781
|
|
|
721
|
-
|
|
722
|
-
const server = app.listen(PORT, () => {
|
|
723
|
-
const actualPort = server.address().port;
|
|
724
|
-
console.log(`Claude Task Viewer running at http://localhost:${actualPort}`);
|
|
782
|
+
const AGENT_FILE_CAP = 20;
|
|
725
783
|
|
|
726
|
-
|
|
727
|
-
|
|
784
|
+
agentActivityWatcher.on('all', (event, filePath) => {
|
|
785
|
+
if ((event === 'add' || event === 'change') && filePath.endsWith('.json')) {
|
|
786
|
+
const relativePath = path.relative(AGENT_ACTIVITY_DIR, filePath);
|
|
787
|
+
const sessionId = relativePath.split(path.sep)[0];
|
|
788
|
+
// Cleanup: if session dir exceeds cap, delete oldest files by mtime
|
|
789
|
+
if (event === 'add') {
|
|
790
|
+
try {
|
|
791
|
+
const sessionDir = path.join(AGENT_ACTIVITY_DIR, sessionId);
|
|
792
|
+
const files = readdirSync(sessionDir).filter(f => f.endsWith('.json'));
|
|
793
|
+
if (files.length > AGENT_FILE_CAP) {
|
|
794
|
+
const withStats = files.map(f => {
|
|
795
|
+
const fp = path.join(sessionDir, f);
|
|
796
|
+
return { file: fp, mtime: statSync(fp).mtimeMs };
|
|
797
|
+
}).sort((a, b) => a.mtime - b.mtime);
|
|
798
|
+
const toDelete = withStats.slice(0, files.length - AGENT_FILE_CAP);
|
|
799
|
+
for (const { file } of toDelete) {
|
|
800
|
+
fs.unlink(file).catch(() => {});
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
} catch (e) { /* ignore */ }
|
|
804
|
+
}
|
|
805
|
+
broadcast({ type: 'agent-update', sessionId });
|
|
806
|
+
// For team sessions, also broadcast with team name so frontend picks it up
|
|
807
|
+
if (existsSync(TEAMS_DIR)) {
|
|
808
|
+
try {
|
|
809
|
+
const teamDirs = readdirSync(TEAMS_DIR, { withFileTypes: true }).filter(d => d.isDirectory());
|
|
810
|
+
for (const td of teamDirs) {
|
|
811
|
+
const cfg = loadTeamConfig(td.name);
|
|
812
|
+
if (cfg && cfg.leadSessionId === sessionId) {
|
|
813
|
+
broadcast({ type: 'agent-update', sessionId: td.name });
|
|
814
|
+
break;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
} catch (e) { /* ignore */ }
|
|
818
|
+
}
|
|
728
819
|
}
|
|
729
820
|
});
|
|
730
821
|
|
|
731
|
-
server.
|
|
732
|
-
|
|
733
|
-
console.log(`
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
822
|
+
const server = app.listen(PORT, () => {
|
|
823
|
+
const actualPort = server.address().port;
|
|
824
|
+
console.log(`Claude Task Viewer running at http://localhost:${actualPort}`);
|
|
825
|
+
|
|
826
|
+
if (process.argv.includes('--open')) {
|
|
827
|
+
import('open').then(open => open.default(`http://localhost:${actualPort}`));
|
|
828
|
+
}
|
|
829
|
+
});
|
|
737
830
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
831
|
+
server.on('error', (err) => {
|
|
832
|
+
if (err.code === 'EADDRINUSE') {
|
|
833
|
+
console.log(`Port ${PORT} in use, trying random port...`);
|
|
834
|
+
const fallback = app.listen(0, () => {
|
|
835
|
+
const actualPort = fallback.address().port;
|
|
836
|
+
console.log(`Claude Task Viewer running at http://localhost:${actualPort}`);
|
|
837
|
+
|
|
838
|
+
if (process.argv.includes('--open')) {
|
|
839
|
+
import('open').then(open => open.default(`http://localhost:${actualPort}`));
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
} else {
|
|
843
|
+
throw err;
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
} // end if (!isSetupCommand)
|