claude-code-kanban 4.13.0 → 4.14.1
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/lib/parsers.js +100 -24
- package/package.json +1 -1
- package/public/app.js +222 -4
- package/public/index.html +24 -0
- package/public/style.css +244 -3
- package/server.js +412 -42
package/lib/parsers.js
CHANGED
|
@@ -1291,33 +1291,31 @@ function findTerminatedTeammates(jsonlPath) {
|
|
|
1291
1291
|
|
|
1292
1292
|
function extractPromptFromTranscript(jsonlPath) {
|
|
1293
1293
|
const { openSync, readSync, closeSync } = fs;
|
|
1294
|
-
|
|
1295
|
-
const
|
|
1294
|
+
// statSync (outside try) throws on a missing path — callers rely on that.
|
|
1295
|
+
const stat = statSync(jsonlPath);
|
|
1296
|
+
// Workflow-spawned subagent prompts embed full task context and routinely
|
|
1297
|
+
// exceed 64 KB, so read the whole first line up to a generous cap. One buffered
|
|
1298
|
+
// read (vs. per-chunk decode) also avoids corrupting multi-byte chars at chunk
|
|
1299
|
+
// boundaries for large prompts.
|
|
1300
|
+
const readSize = Math.min(2097152, stat.size);
|
|
1296
1301
|
const fd = openSync(jsonlPath, 'r');
|
|
1297
1302
|
try {
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
const
|
|
1309
|
-
|
|
1310
|
-
const content = obj.message?.content;
|
|
1311
|
-
if (typeof content === 'string') return content;
|
|
1312
|
-
if (Array.isArray(content)) {
|
|
1313
|
-
for (const b of content) {
|
|
1314
|
-
if (b.type === 'text' && b.text) return b.text;
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1303
|
+
const buf = Buffer.alloc(readSize);
|
|
1304
|
+
readSync(fd, buf, 0, readSize, 0);
|
|
1305
|
+
// Decode only the first line, not the whole (possibly large) read window.
|
|
1306
|
+
const nlByte = buf.indexOf(0x0a);
|
|
1307
|
+
const firstLine = (nlByte === -1 ? buf : buf.subarray(0, nlByte)).toString('utf8');
|
|
1308
|
+
const obj = JSON.parse(firstLine);
|
|
1309
|
+
if (obj.type === 'user') {
|
|
1310
|
+
const content = obj.message?.content;
|
|
1311
|
+
if (typeof content === 'string') return content;
|
|
1312
|
+
if (Array.isArray(content)) {
|
|
1313
|
+
for (const b of content) {
|
|
1314
|
+
if (b.type === 'text' && b.text) return b.text;
|
|
1317
1315
|
}
|
|
1318
|
-
}
|
|
1319
|
-
break;
|
|
1316
|
+
}
|
|
1320
1317
|
}
|
|
1318
|
+
} catch (_) {
|
|
1321
1319
|
} finally {
|
|
1322
1320
|
closeSync(fd);
|
|
1323
1321
|
}
|
|
@@ -1354,6 +1352,82 @@ function extractModelFromTranscript(jsonlPath) {
|
|
|
1354
1352
|
return null;
|
|
1355
1353
|
}
|
|
1356
1354
|
|
|
1355
|
+
// Full-file scan of a subagent transcript for roster stats: model, summed
|
|
1356
|
+
// output tokens, and first/last timestamps (for duration). COLD PATH ONLY —
|
|
1357
|
+
// used by the workflow run view on demand, never from the session-scan hot
|
|
1358
|
+
// path (a full read per agent would blow the /api/sessions budget).
|
|
1359
|
+
function extractTranscriptStats(jsonlPath) {
|
|
1360
|
+
let content;
|
|
1361
|
+
try { content = readFileSync(jsonlPath, 'utf8'); } catch (_) { return null; }
|
|
1362
|
+
let model = null;
|
|
1363
|
+
let outputTokens = 0;
|
|
1364
|
+
let firstTs = null;
|
|
1365
|
+
let lastTs = null;
|
|
1366
|
+
for (const line of content.split('\n')) {
|
|
1367
|
+
if (!line.trim()) continue;
|
|
1368
|
+
let obj;
|
|
1369
|
+
try { obj = JSON.parse(line); } catch (_) { continue; }
|
|
1370
|
+
if (obj.timestamp) {
|
|
1371
|
+
if (!firstTs) firstTs = obj.timestamp;
|
|
1372
|
+
lastTs = obj.timestamp;
|
|
1373
|
+
}
|
|
1374
|
+
const msg = obj.message;
|
|
1375
|
+
if (msg && typeof msg === 'object') {
|
|
1376
|
+
if (!model && msg.model) model = msg.model;
|
|
1377
|
+
const out = msg.usage?.output_tokens;
|
|
1378
|
+
if (typeof out === 'number') outputTokens += out;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
return { model, outputTokens, firstTs, lastTs };
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// Render a StructuredOutput tool input (arbitrary schema object) as markdown:
|
|
1385
|
+
// one section per field, prose kept verbatim, structured values as JSON blocks.
|
|
1386
|
+
function formatStructuredResult(input) {
|
|
1387
|
+
const parts = [];
|
|
1388
|
+
for (const [key, value] of Object.entries(input)) {
|
|
1389
|
+
if (value == null || value === '') continue;
|
|
1390
|
+
parts.push('### ' + key);
|
|
1391
|
+
parts.push(typeof value === 'string' ? value : '```json\n' + JSON.stringify(value, null, 2) + '\n```');
|
|
1392
|
+
}
|
|
1393
|
+
return parts.length ? parts.join('\n\n') : null;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// Workflow-spawned subagents given a schema end their run on a forced
|
|
1397
|
+
// StructuredOutput tool call and never emit a text response — so their
|
|
1398
|
+
// lastMessage is empty. Tail-read the transcript, take the LAST StructuredOutput
|
|
1399
|
+
// tool_use, and format its input as the agent's result. Returns null when absent.
|
|
1400
|
+
function extractStructuredResultFromTranscript(jsonlPath) {
|
|
1401
|
+
const { openSync, readSync, closeSync } = fs;
|
|
1402
|
+
const stat = statSync(jsonlPath);
|
|
1403
|
+
const readSize = Math.min(1048576, stat.size);
|
|
1404
|
+
const start = stat.size - readSize;
|
|
1405
|
+
const fd = openSync(jsonlPath, 'r');
|
|
1406
|
+
try {
|
|
1407
|
+
const buf = Buffer.alloc(readSize);
|
|
1408
|
+
readSync(fd, buf, 0, readSize, start);
|
|
1409
|
+
const text = buf.toString('utf8');
|
|
1410
|
+
// Drop the leading partial line unless we read from the very start.
|
|
1411
|
+
const clean = start > 0 ? text.slice(text.indexOf('\n') + 1) : text;
|
|
1412
|
+
if (!clean.includes('"StructuredOutput"')) return null;
|
|
1413
|
+
// Scan from the end for the last StructuredOutput tool call.
|
|
1414
|
+
const lines = clean.split('\n');
|
|
1415
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1416
|
+
if (!lines[i].includes('"StructuredOutput"')) continue;
|
|
1417
|
+
let obj;
|
|
1418
|
+
try { obj = JSON.parse(lines[i]); } catch (_) { continue; }
|
|
1419
|
+
const content = obj?.message?.content;
|
|
1420
|
+
if (!Array.isArray(content)) continue;
|
|
1421
|
+
const block = content.find(b => b?.type === 'tool_use' && b.name === 'StructuredOutput' && b.input && typeof b.input === 'object');
|
|
1422
|
+
if (block) return formatStructuredResult(block.input);
|
|
1423
|
+
}
|
|
1424
|
+
} catch (_) {
|
|
1425
|
+
} finally {
|
|
1426
|
+
closeSync(fd);
|
|
1427
|
+
}
|
|
1428
|
+
return null;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1357
1431
|
// Incremental loop-tool scanner. JSONL is append-only, so we keep per-path
|
|
1358
1432
|
// state and on each call read ONLY the bytes appended since scannedOffset.
|
|
1359
1433
|
// Avoids the only full-file read that ran inside the /api/sessions hot path.
|
|
@@ -1492,5 +1566,7 @@ module.exports = {
|
|
|
1492
1566
|
readCompactSummaries,
|
|
1493
1567
|
findTerminatedTeammates,
|
|
1494
1568
|
extractPromptFromTranscript,
|
|
1495
|
-
extractModelFromTranscript
|
|
1569
|
+
extractModelFromTranscript,
|
|
1570
|
+
extractStructuredResultFromTranscript,
|
|
1571
|
+
extractTranscriptStats
|
|
1496
1572
|
};
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -2634,7 +2634,7 @@ function showAgentModal(agentId) {
|
|
|
2634
2634
|
const modalNameLabel = agent.agentName ? ` · ${escapeHtml(agent.agentName)}` : '';
|
|
2635
2635
|
title.innerHTML = `${statusDot} ${escapeHtml(agent.type || 'unknown')}${modalNameLabel}`;
|
|
2636
2636
|
|
|
2637
|
-
const shortModel = agent.model
|
|
2637
|
+
const shortModel = shortModelName(agent.model);
|
|
2638
2638
|
const shortId = agent.agentId ? agent.agentId.slice(0, 8) : '';
|
|
2639
2639
|
const chip = (label, value, opts = {}) => {
|
|
2640
2640
|
const cls = opts.cls ? ` ${opts.cls}` : '';
|
|
@@ -2648,7 +2648,7 @@ function showAgentModal(agentId) {
|
|
|
2648
2648
|
// toolUseResult), not on the agent-activity record — so read them from there.
|
|
2649
2649
|
const agentMsg = currentMessages.find((m) => m.tool === 'Agent' && m.agentId === agentId);
|
|
2650
2650
|
const usageStats = agentMsg?.agentUsageStats || null;
|
|
2651
|
-
const fmtTok =
|
|
2651
|
+
const fmtTok = fmtTokens;
|
|
2652
2652
|
|
|
2653
2653
|
const chips = [];
|
|
2654
2654
|
if (agent.agentId) chips.push(chip('id', escapeHtml(shortId), { cls: 'agent-chip-mono', title: agent.agentId }));
|
|
@@ -2796,7 +2796,7 @@ function renderSessions() {
|
|
|
2796
2796
|
((!s.sharedTaskList && (s.pending > 0 || s.inProgress > 0)) ||
|
|
2797
2797
|
s.hasActiveAgents ||
|
|
2798
2798
|
s.hasWaitingForUser ||
|
|
2799
|
-
s.
|
|
2799
|
+
s.hasRecentActivity);
|
|
2800
2800
|
if (isActive) activeSessionIds.add(s.id);
|
|
2801
2801
|
return isActive;
|
|
2802
2802
|
});
|
|
@@ -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(),
|
|
@@ -5698,8 +5707,12 @@ function filterBySessions(value) {
|
|
|
5698
5707
|
sessionFilter = value;
|
|
5699
5708
|
if (value !== 'active') activityFilter.clear();
|
|
5700
5709
|
updateUrl();
|
|
5710
|
+
// Instant feedback from cached data, then refetch — the cached list was fetched
|
|
5711
|
+
// with the previous filter's server params (e.g. filter=active), so "All Sessions"
|
|
5712
|
+
// needs a server round-trip to actually include inactive sessions.
|
|
5701
5713
|
renderSessions();
|
|
5702
5714
|
renderActivityChip();
|
|
5715
|
+
fetchSessions(false);
|
|
5703
5716
|
}
|
|
5704
5717
|
|
|
5705
5718
|
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
@@ -6477,6 +6490,208 @@ function closeLoopModal() {
|
|
|
6477
6490
|
document.getElementById('loop-modal').classList.remove('visible');
|
|
6478
6491
|
}
|
|
6479
6492
|
|
|
6493
|
+
const WORKFLOW_GEAR_SVG =
|
|
6494
|
+
'<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>';
|
|
6495
|
+
|
|
6496
|
+
// Standard "open in editor" glyph — matches the message-detail / plan modals.
|
|
6497
|
+
const ICON_OPEN_EDITOR =
|
|
6498
|
+
'<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>';
|
|
6499
|
+
|
|
6500
|
+
function renderWorkflowBadge(session) {
|
|
6501
|
+
if (!session.hasWorkflow) return '';
|
|
6502
|
+
const n = session.workflowCount || 0;
|
|
6503
|
+
const tip = `${n} workflow${n === 1 ? '' : 's'}`;
|
|
6504
|
+
return `<span class="workflow-badge" onclick="event.stopPropagation(); showWorkflowModal('${session.id}')" title="${escapeHtml(tip)}">${WORKFLOW_GEAR_SVG}</span>`;
|
|
6505
|
+
}
|
|
6506
|
+
|
|
6507
|
+
let _workflowSessionId = null;
|
|
6508
|
+
let _workflowHeaderId = null;
|
|
6509
|
+
let _workflowList = [];
|
|
6510
|
+
|
|
6511
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
6512
|
+
function showWorkflowModal(sessionId) {
|
|
6513
|
+
_workflowSessionId = sessionId;
|
|
6514
|
+
setWorkflowHeader('Workflows', null);
|
|
6515
|
+
const body = document.getElementById('workflow-modal-body');
|
|
6516
|
+
body.innerHTML = '<div style="padding:16px;color:var(--text-secondary);">Loading…</div>';
|
|
6517
|
+
document.getElementById('workflow-modal').classList.add('visible');
|
|
6518
|
+
fetch(`/api/sessions/${sessionId}/workflows`)
|
|
6519
|
+
.then((r) => (r.ok ? r.json() : { workflows: [] }))
|
|
6520
|
+
.catch(() => ({ workflows: [] }))
|
|
6521
|
+
.then((data) => {
|
|
6522
|
+
_workflowList = data.workflows || [];
|
|
6523
|
+
if (!_workflowList.length) {
|
|
6524
|
+
setWorkflowHeader('Workflows', null);
|
|
6525
|
+
body.innerHTML =
|
|
6526
|
+
'<div style="padding:24px;text-align:center;color:var(--text-secondary);">No workflow scripts for this session.</div>';
|
|
6527
|
+
} else if (_workflowList.length === 1) {
|
|
6528
|
+
showWorkflowRun(_workflowList[0].id);
|
|
6529
|
+
} else {
|
|
6530
|
+
renderWorkflowPicker(_workflowList);
|
|
6531
|
+
}
|
|
6532
|
+
});
|
|
6533
|
+
}
|
|
6534
|
+
|
|
6535
|
+
function setWorkflowHeader(name, wfId) {
|
|
6536
|
+
const titleText = document.getElementById('workflow-modal-title-text');
|
|
6537
|
+
const headerBtn = document.getElementById('workflow-modal-open-btn');
|
|
6538
|
+
_workflowHeaderId = wfId || null;
|
|
6539
|
+
if (titleText) {
|
|
6540
|
+
titleText.textContent = name;
|
|
6541
|
+
titleText.title = wfId ? name : '';
|
|
6542
|
+
}
|
|
6543
|
+
if (headerBtn) headerBtn.style.display = wfId ? '' : 'none';
|
|
6544
|
+
}
|
|
6545
|
+
|
|
6546
|
+
// Multiple workflows: pick one to drill into (we show a single run at a time).
|
|
6547
|
+
function renderWorkflowPicker(workflows) {
|
|
6548
|
+
setWorkflowHeader('Workflows', null);
|
|
6549
|
+
const body = document.getElementById('workflow-modal-body');
|
|
6550
|
+
body.innerHTML = workflows
|
|
6551
|
+
.map((w) => {
|
|
6552
|
+
const when = w.modifiedAt ? formatDate(w.modifiedAt) : '';
|
|
6553
|
+
return `<div class="workflow-row" onclick="showWorkflowRun('${escapeHtml(w.id)}')">
|
|
6554
|
+
<div class="workflow-row-head">
|
|
6555
|
+
<span class="workflow-row-name">${escapeHtml(w.name || w.id)}</span>
|
|
6556
|
+
<span class="workflow-row-id"><code>${escapeHtml(w.id)}</code></span>
|
|
6557
|
+
<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>
|
|
6558
|
+
</div>
|
|
6559
|
+
${w.description ? `<div class="workflow-row-desc">${escapeHtml(w.description)}</div>` : ''}
|
|
6560
|
+
${when ? `<div class="workflow-row-meta">${escapeHtml(when)}</div>` : ''}
|
|
6561
|
+
</div>`;
|
|
6562
|
+
})
|
|
6563
|
+
.join('');
|
|
6564
|
+
}
|
|
6565
|
+
|
|
6566
|
+
function showWorkflowRun(wfId) {
|
|
6567
|
+
const name = _workflowList.find((w) => w.id === wfId)?.name || wfId;
|
|
6568
|
+
setWorkflowHeader(name, wfId);
|
|
6569
|
+
const body = document.getElementById('workflow-modal-body');
|
|
6570
|
+
body.innerHTML = '<div style="padding:16px;color:var(--text-secondary);">Loading…</div>';
|
|
6571
|
+
fetch(`/api/sessions/${_workflowSessionId}/workflows/${encodeURIComponent(wfId)}/run`)
|
|
6572
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
6573
|
+
.catch(() => null)
|
|
6574
|
+
.then((run) => {
|
|
6575
|
+
if (!run) {
|
|
6576
|
+
body.innerHTML = '<div style="padding:16px;color:var(--text-secondary);">Failed to load run state.</div>';
|
|
6577
|
+
return;
|
|
6578
|
+
}
|
|
6579
|
+
renderWorkflowRun(run);
|
|
6580
|
+
});
|
|
6581
|
+
}
|
|
6582
|
+
|
|
6583
|
+
function fmtTokens(t) {
|
|
6584
|
+
return t >= 1000 ? `${(t / 1000).toFixed(1).replace(/\.0$/, '')}k` : `${t}`;
|
|
6585
|
+
}
|
|
6586
|
+
|
|
6587
|
+
function shortModelName(model) {
|
|
6588
|
+
return model ? model.replace(/^claude-/, '').replace(/-\d{8}$/, '') : '';
|
|
6589
|
+
}
|
|
6590
|
+
|
|
6591
|
+
function renderWorkflowRun(run) {
|
|
6592
|
+
const body = document.getElementById('workflow-modal-body');
|
|
6593
|
+
const allDone = run.startedCount > 0 && run.doneCount >= run.startedCount;
|
|
6594
|
+
const wall =
|
|
6595
|
+
run.startedAt && run.stoppedAt ? formatDuration(new Date(run.stoppedAt) - new Date(run.startedAt)) : null;
|
|
6596
|
+
|
|
6597
|
+
const back =
|
|
6598
|
+
_workflowList.length > 1
|
|
6599
|
+
? `<div class="wf-back" onclick="renderWorkflowPicker(_workflowList)">‹ All workflows</div>`
|
|
6600
|
+
: '';
|
|
6601
|
+
|
|
6602
|
+
const stats = [`${run.doneCount}/${run.startedCount} agents`, wall ? escapeHtml(wall) : null]
|
|
6603
|
+
.filter(Boolean)
|
|
6604
|
+
.join(' · ');
|
|
6605
|
+
|
|
6606
|
+
const desc = run.description ? `<div class="wf-run-desc">${escapeHtml(run.description)}</div>` : '';
|
|
6607
|
+
|
|
6608
|
+
const phases = (run.phases || [])
|
|
6609
|
+
.map(
|
|
6610
|
+
(p) => `<div class="wf-phase">
|
|
6611
|
+
<span class="wf-phase-mark${allDone ? ' done' : ''}">${allDone ? '✓' : ''}</span>
|
|
6612
|
+
<span class="wf-phase-title">${escapeHtml(p.title)}</span>
|
|
6613
|
+
${p.detail ? `<span class="wf-phase-detail">${escapeHtml(p.detail)}</span>` : ''}
|
|
6614
|
+
</div>`,
|
|
6615
|
+
)
|
|
6616
|
+
.join('');
|
|
6617
|
+
const phasesBlock = phases ? `<div class="wf-section-title">Phases</div><div class="wf-phases">${phases}</div>` : '';
|
|
6618
|
+
|
|
6619
|
+
const agents = (run.agents || [])
|
|
6620
|
+
.map((a) => {
|
|
6621
|
+
const dur = a.durationMs != null ? formatDuration(a.durationMs) : '';
|
|
6622
|
+
const running = a.status !== 'done';
|
|
6623
|
+
return `<div class="wf-agent" onclick="showAgentModal('${escapeHtml(a.agentId)}')" title="${escapeHtml(a.agentId)}">
|
|
6624
|
+
<span class="wf-agent-type">${escapeHtml(a.type || 'agent')}</span>
|
|
6625
|
+
<span class="wf-agent-model">${escapeHtml(shortModelName(a.model))}</span>
|
|
6626
|
+
<span class="wf-agent-tok">${a.outputTokens ? `${fmtTokens(a.outputTokens)} tok` : ''}</span>
|
|
6627
|
+
<span class="wf-agent-dur">${escapeHtml(dur)}</span>
|
|
6628
|
+
<span class="wf-agent-status ${running ? 'running' : 'done'}">${running ? '●' : '✓'}</span>
|
|
6629
|
+
</div>`;
|
|
6630
|
+
})
|
|
6631
|
+
.join('');
|
|
6632
|
+
const agentsBlock = run.agents?.length
|
|
6633
|
+
? `<div class="wf-section-title">Agents · ${run.agents.length}</div><div class="wf-agents">${agents}</div>`
|
|
6634
|
+
: '<div class="wf-section-title">Agents</div><div class="wf-empty">No agent runs recorded yet.</div>';
|
|
6635
|
+
|
|
6636
|
+
body.innerHTML = `<div class="workflow-run">
|
|
6637
|
+
${back}
|
|
6638
|
+
${desc}
|
|
6639
|
+
<div class="wf-run-stats">${stats} · <code>${escapeHtml(run.id)}</code></div>
|
|
6640
|
+
${phasesBlock}
|
|
6641
|
+
${agentsBlock}
|
|
6642
|
+
<details class="wf-source">
|
|
6643
|
+
<summary>Source</summary>
|
|
6644
|
+
<div class="workflow-row-code"></div>
|
|
6645
|
+
</details>
|
|
6646
|
+
</div>`;
|
|
6647
|
+
|
|
6648
|
+
const details = body.querySelector('.wf-source');
|
|
6649
|
+
details.addEventListener(
|
|
6650
|
+
'toggle',
|
|
6651
|
+
() => {
|
|
6652
|
+
if (details.open) loadWorkflowCode(run.id, details.querySelector('.workflow-row-code'));
|
|
6653
|
+
},
|
|
6654
|
+
{ once: true },
|
|
6655
|
+
);
|
|
6656
|
+
}
|
|
6657
|
+
|
|
6658
|
+
function loadWorkflowCode(wfId, codeEl) {
|
|
6659
|
+
if (!codeEl) return;
|
|
6660
|
+
codeEl.innerHTML = '<div style="padding:8px 0;color:var(--text-secondary);">Loading…</div>';
|
|
6661
|
+
fetch(`/api/sessions/${_workflowSessionId}/workflows/${encodeURIComponent(wfId)}`)
|
|
6662
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
6663
|
+
.catch(() => null)
|
|
6664
|
+
.then((data) => {
|
|
6665
|
+
if (!data?.content) {
|
|
6666
|
+
codeEl.innerHTML = '<div style="padding:8px 0;color:var(--text-secondary);">Failed to load script.</div>';
|
|
6667
|
+
return;
|
|
6668
|
+
}
|
|
6669
|
+
let highlighted;
|
|
6670
|
+
if (typeof hljs !== 'undefined' && hljs.getLanguage('javascript')) {
|
|
6671
|
+
highlighted = hljs.highlight(data.content, { language: 'javascript' }).value;
|
|
6672
|
+
} else {
|
|
6673
|
+
highlighted = escapeHtml(data.content);
|
|
6674
|
+
}
|
|
6675
|
+
codeEl.innerHTML = `<pre><code class="hljs language-javascript">${highlighted}</code></pre>`;
|
|
6676
|
+
});
|
|
6677
|
+
}
|
|
6678
|
+
|
|
6679
|
+
function openWorkflowInEditor(wfId) {
|
|
6680
|
+
postAndToast(`/api/sessions/${_workflowSessionId}/workflows/${encodeURIComponent(wfId)}/open`, {}, 'in editor');
|
|
6681
|
+
}
|
|
6682
|
+
|
|
6683
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
6684
|
+
function openWorkflowHeaderScript() {
|
|
6685
|
+
if (_workflowHeaderId) openWorkflowInEditor(_workflowHeaderId);
|
|
6686
|
+
}
|
|
6687
|
+
|
|
6688
|
+
function closeWorkflowModal() {
|
|
6689
|
+
document.getElementById('workflow-modal').classList.remove('visible');
|
|
6690
|
+
_workflowSessionId = null;
|
|
6691
|
+
_workflowList = [];
|
|
6692
|
+
setWorkflowHeader('Workflows', null);
|
|
6693
|
+
}
|
|
6694
|
+
|
|
6480
6695
|
function openPlanForSession(sid) {
|
|
6481
6696
|
fetch(`/api/sessions/${sid}/plan`)
|
|
6482
6697
|
.then((r) => (r.ok ? r.json() : null))
|
|
@@ -6974,6 +7189,9 @@ window.addEventListener('popstate', () => {
|
|
|
6974
7189
|
ownerFilter = s.owner || '';
|
|
6975
7190
|
searchQuery = s.search || '';
|
|
6976
7191
|
loadPreferences();
|
|
7192
|
+
// fetchSessions derives query params from the globals set above — refetch so
|
|
7193
|
+
// back/forward across a filter change doesn't render a stale server-filtered list.
|
|
7194
|
+
fetchSessions(false);
|
|
6977
7195
|
if (s.projectView) {
|
|
6978
7196
|
try {
|
|
6979
7197
|
fetchProjectView(atob(s.projectView));
|
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">
|