claude-code-kanban 4.3.0 → 4.5.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/lib/parsers.js +103 -10
- package/package.json +1 -1
- package/plugin/plugins/claude-code-kanban/.claude-plugin/plugin.json +1 -1
- package/plugin/plugins/claude-code-kanban/scripts/agent-spy.sh +1 -1
- package/public/app.js +329 -47
- package/public/index.html +19 -0
- package/public/style.css +235 -4
- package/server.js +334 -87
package/public/app.js
CHANGED
|
@@ -161,12 +161,16 @@ async function fetchSessions(includeTasks = true) {
|
|
|
161
161
|
const allPinnedIds = new Set([...pinnedSessionIds, ...stickySessionIds]);
|
|
162
162
|
if (revealedPlanSessionId) allPinnedIds.add(revealedPlanSessionId);
|
|
163
163
|
if (revealedStorageSessionId) allPinnedIds.add(revealedStorageSessionId);
|
|
164
|
+
// When server filters by activity, the focused session may not be active —
|
|
165
|
+
// include it in pinned so the server still returns it.
|
|
166
|
+
if (sessionFilter === 'active' && currentSessionId) allPinnedIds.add(currentSessionId);
|
|
164
167
|
const pinnedParam = allPinnedIds.size > 0 ? `&pinned=${[...allPinnedIds].join(',')}` : '';
|
|
165
168
|
const projectParam =
|
|
166
169
|
filterProject && filterProject !== '__recent__' ? `&project=${encodeURIComponent(filterProject)}` : '';
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
+
const filterParam = sessionFilter === 'active' ? '&filter=active' : '';
|
|
171
|
+
const sessionsPromise = fetch(
|
|
172
|
+
`/api/sessions?limit=${sessionLimit}${pinnedParam}${projectParam}${filterParam}`,
|
|
173
|
+
).then((r) => r.json());
|
|
170
174
|
|
|
171
175
|
let newSessions, newTasks;
|
|
172
176
|
if (includeTasks) {
|
|
@@ -494,10 +498,21 @@ function renderActivityChip() {
|
|
|
494
498
|
.join('');
|
|
495
499
|
}
|
|
496
500
|
|
|
497
|
-
|
|
498
|
-
function setActivityFilter(kind) {
|
|
501
|
+
function toggleActivityKind(kind) {
|
|
499
502
|
if (activityFilter.has(kind)) activityFilter.delete(kind);
|
|
500
503
|
else activityFilter.add(kind);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
507
|
+
function setActivityFilter(kind) {
|
|
508
|
+
if (kind === 'active') {
|
|
509
|
+
// waiting is a sub-state of active — couple them so one click covers all running sessions
|
|
510
|
+
toggleActivityKind('active');
|
|
511
|
+
toggleActivityKind('waiting');
|
|
512
|
+
} else {
|
|
513
|
+
toggleActivityKind(kind);
|
|
514
|
+
}
|
|
515
|
+
localStorage.setItem('activityFilter', JSON.stringify([...activityFilter]));
|
|
501
516
|
// active/waiting only make sense with the active session filter on
|
|
502
517
|
const targetFilter = activityFilter.size > 0 ? 'active' : sessionFilter;
|
|
503
518
|
if (targetFilter !== sessionFilter) {
|
|
@@ -571,7 +586,6 @@ async function fetchTasks(sessionId) {
|
|
|
571
586
|
const WAITING_TTL_MS = 30 * 60 * 1000;
|
|
572
587
|
const AGENT_LOG_MAX = 8;
|
|
573
588
|
const LIVE_INDICATOR_MS = 10 * 1000;
|
|
574
|
-
const ACTIVE_PLAN_MS = 10 * 60 * 1000;
|
|
575
589
|
// #endregion
|
|
576
590
|
|
|
577
591
|
function resetAgentState() {
|
|
@@ -1133,42 +1147,80 @@ function toggleToolGroup(id) {
|
|
|
1133
1147
|
if (el) el.classList.toggle('show');
|
|
1134
1148
|
}
|
|
1135
1149
|
|
|
1136
|
-
const WAITING_PLAN_PREVIEW_CHARS = 120;
|
|
1137
|
-
const WAITING_PREVIEW_MAX_CHARS = 200;
|
|
1138
|
-
|
|
1139
1150
|
function getWaitingLabel(kind, tool) {
|
|
1140
1151
|
if (kind !== 'question') return `Awaiting permission: ${tool}`;
|
|
1141
1152
|
if (tool === 'ExitPlanMode') return 'Plan awaiting approval';
|
|
1142
1153
|
return 'Question pending';
|
|
1143
1154
|
}
|
|
1144
1155
|
|
|
1145
|
-
function
|
|
1146
|
-
if (
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1156
|
+
function getWaitingPill(kind, tool) {
|
|
1157
|
+
if (kind === 'question' && tool === 'ExitPlanMode') return 'Plan awaiting approval';
|
|
1158
|
+
if (kind === 'question') return 'Question pending';
|
|
1159
|
+
return 'Awaiting permission';
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function deriveWaitingDetail(tool, params) {
|
|
1163
|
+
if (!params) return '';
|
|
1164
|
+
const trunc = (s) => (s.length > 80 ? `${s.slice(0, 80)}...` : s);
|
|
1165
|
+
if (params.file_path) return params.file_path.replace(/^.*[/\\]/, '');
|
|
1166
|
+
if (params.command) return trunc(params.command);
|
|
1167
|
+
if (params.pattern) return trunc(params.pattern);
|
|
1168
|
+
if (params.query) return trunc(params.query);
|
|
1169
|
+
if (params.url) return trunc(params.url);
|
|
1170
|
+
if (params.skill) {
|
|
1171
|
+
const s = params.skill + (typeof params.args === 'string' ? ` ${params.args}` : '');
|
|
1172
|
+
return trunc(s);
|
|
1173
|
+
}
|
|
1174
|
+
if (tool === 'AskUserQuestion' && params.questions?.[0]?.question) return trunc(params.questions[0].question);
|
|
1175
|
+
if (tool === 'ExitPlanMode' && typeof params.plan === 'string') {
|
|
1176
|
+
const t = params.plan.match(/^#\s+(.+)/m);
|
|
1177
|
+
return trunc(t ? t[1] : params.plan);
|
|
1178
|
+
}
|
|
1179
|
+
if (params.description) return trunc(String(params.description));
|
|
1159
1180
|
return '';
|
|
1160
1181
|
}
|
|
1161
1182
|
|
|
1183
|
+
function renderWaitingBody(tool, params) {
|
|
1184
|
+
if (!params) return '';
|
|
1185
|
+
if (tool === 'AskUserQuestion' && Array.isArray(params.questions)) {
|
|
1186
|
+
const items = params.questions
|
|
1187
|
+
.map((q) => {
|
|
1188
|
+
const head = `<div style="font-weight:600">${escapeHtml(q.question || '')}</div>`;
|
|
1189
|
+
const opts = Array.isArray(q.options)
|
|
1190
|
+
? `<ul style="margin:2px 0 0 16px;padding:0">${q.options
|
|
1191
|
+
.map(
|
|
1192
|
+
(o) =>
|
|
1193
|
+
`<li><span style="font-weight:600">${escapeHtml(o.label || '')}</span>${o.description ? ` — <span style="color:var(--text-muted)">${escapeHtml(o.description)}</span>` : ''}</li>`,
|
|
1194
|
+
)
|
|
1195
|
+
.join('')}</ul>`
|
|
1196
|
+
: '';
|
|
1197
|
+
return `<div style="margin-top:6px">${head}${opts}</div>`;
|
|
1198
|
+
})
|
|
1199
|
+
.join('');
|
|
1200
|
+
return items;
|
|
1201
|
+
}
|
|
1202
|
+
return renderToolParamsHtml(params);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1162
1205
|
function renderWaitingEntry() {
|
|
1163
1206
|
if (!isWaitingFresh()) return '';
|
|
1164
1207
|
const tool = currentWaiting.toolName || 'unknown';
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1208
|
+
let params = null;
|
|
1209
|
+
if (currentWaiting.toolInput) {
|
|
1210
|
+
try {
|
|
1211
|
+
params = JSON.parse(currentWaiting.toolInput);
|
|
1212
|
+
} catch (_) {
|
|
1213
|
+
/* toolInput may be truncated/non-JSON */
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
const pillText = getWaitingPill(currentWaiting.kind, tool);
|
|
1217
|
+
const detail = deriveWaitingDetail(tool, params);
|
|
1218
|
+
const detailHtml = detail ? ` <span style="color:var(--text-secondary)">${escapeHtml(detail)}</span>` : '';
|
|
1219
|
+
const bodyHtml = renderWaitingBody(tool, params);
|
|
1220
|
+
const bodyWrap = bodyHtml ? `<div class="msg-waiting-body">${bodyHtml}</div>` : '';
|
|
1221
|
+
const pill = `<span class="msg-waiting-pill">${escapeHtml(pillText)}</span>`;
|
|
1170
1222
|
const discardBtn = `<button class="msg-waiting-discard" title="Discard permission prompt" onclick="event.stopPropagation();discardWaiting()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>`;
|
|
1171
|
-
return `<div class="msg-item msg-waiting" onclick="msgDetailFollowLatest=false;showWaitingDetail()">${
|
|
1223
|
+
return `<div class="msg-item msg-waiting" onclick="msgDetailFollowLatest=false;showWaitingDetail()">${getToolIcon(tool)}<div class="msg-body"><div class="msg-text">${pill} <span style="font-weight:600">${escapeHtml(tool)}</span>${detailHtml}</div>${bodyWrap}<div class="msg-time">waiting…</div></div>${discardBtn}</div>`;
|
|
1172
1224
|
}
|
|
1173
1225
|
|
|
1174
1226
|
// biome-ignore lint/correctness/noUnusedVariables: used in HTML onclick
|
|
@@ -1343,6 +1395,20 @@ function togglePin(msgIndex) {
|
|
|
1343
1395
|
currentPins.splice(idx, 1);
|
|
1344
1396
|
} else {
|
|
1345
1397
|
pinnedCollapsed = false;
|
|
1398
|
+
// Strip large server-truncated `*Full` payloads (Write contentFull,
|
|
1399
|
+
// MCP passthrough <k>Full) before stashing in localStorage — a few
|
|
1400
|
+
// pinned big writes can blow past the per-origin quota. On pin
|
|
1401
|
+
// expand, the modal falls back to the truncated string + the lazy
|
|
1402
|
+
// /api/sessions/:id/tool-result/:toolUseId endpoint.
|
|
1403
|
+
let paramsForPin = null;
|
|
1404
|
+
if (m.params) {
|
|
1405
|
+
paramsForPin = {};
|
|
1406
|
+
for (const [k, v] of Object.entries(m.params)) {
|
|
1407
|
+
if (k === 'contentFull') continue;
|
|
1408
|
+
if (k.endsWith('Full') && typeof v === 'string' && typeof m.params[k.slice(0, -4)] === 'string') continue;
|
|
1409
|
+
paramsForPin[k] = v;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1346
1412
|
currentPins.push({
|
|
1347
1413
|
id,
|
|
1348
1414
|
type: m.type,
|
|
@@ -1352,6 +1418,9 @@ function togglePin(msgIndex) {
|
|
|
1352
1418
|
toolUseId: m.toolUseId || null,
|
|
1353
1419
|
toolResult: m.toolResult || null,
|
|
1354
1420
|
toolResultTruncated: m.toolResultTruncated || false,
|
|
1421
|
+
toolResultFull: null,
|
|
1422
|
+
answerPayload: m.answerPayload || null,
|
|
1423
|
+
params: paramsForPin,
|
|
1355
1424
|
detail: m.detail || null,
|
|
1356
1425
|
fullDetail: m.fullDetail || null,
|
|
1357
1426
|
description: m.description || null,
|
|
@@ -1660,7 +1729,9 @@ function showMsgDetail(idx) {
|
|
|
1660
1729
|
} else {
|
|
1661
1730
|
mainHtml = TASK_TOOLS.has(m.tool) ? '' : '<em>No details</em>';
|
|
1662
1731
|
}
|
|
1663
|
-
|
|
1732
|
+
const answersHtml = m.answerPayload ? renderAnswerPayloadHtml(m.answerPayload) : '';
|
|
1733
|
+
body.innerHTML =
|
|
1734
|
+
mainHtml + toolParamsHtml + answersHtml + taskResultHtml + (hasAgentTabs ? '' : toolResultHtml) + agentExtraHtml;
|
|
1664
1735
|
} else if (m.type === 'teammate') {
|
|
1665
1736
|
document.getElementById('msg-detail-title').textContent = m.teammateId || 'Teammate';
|
|
1666
1737
|
document.getElementById('msg-detail-agent-btn').style.display = 'none';
|
|
@@ -1879,16 +1950,75 @@ function renderTaskResult(toolResult) {
|
|
|
1879
1950
|
return `${html}</div>`;
|
|
1880
1951
|
}
|
|
1881
1952
|
|
|
1953
|
+
function renderAnswerPayloadHtml(answerPayload) {
|
|
1954
|
+
if (!answerPayload?.answers || typeof answerPayload.answers !== 'object') return '';
|
|
1955
|
+
const qs = Array.isArray(answerPayload.questions) ? answerPayload.questions : [];
|
|
1956
|
+
const findOptionDesc = (qText, label) => {
|
|
1957
|
+
const q = qs.find((x) => x && x.question === qText);
|
|
1958
|
+
if (!q || !Array.isArray(q.options)) return null;
|
|
1959
|
+
const opt = q.options.find((o) => o && o.label === label);
|
|
1960
|
+
return opt?.description ? opt.description : null;
|
|
1961
|
+
};
|
|
1962
|
+
const rows = Object.entries(answerPayload.answers)
|
|
1963
|
+
.map(([q, a]) => {
|
|
1964
|
+
const ansList = Array.isArray(a) ? a : [a];
|
|
1965
|
+
const items = ansList
|
|
1966
|
+
.map((label) => {
|
|
1967
|
+
const desc = findOptionDesc(q, label);
|
|
1968
|
+
const descHtml = desc ? ` <span style="color:var(--text-muted)">— ${escapeHtml(desc)}</span>` : '';
|
|
1969
|
+
return `<li><span style="font-weight:600">${escapeHtml(String(label))}</span>${descHtml}</li>`;
|
|
1970
|
+
})
|
|
1971
|
+
.join('');
|
|
1972
|
+
return `<div style="margin-top:6px">
|
|
1973
|
+
<div style="font-size:0.75rem;color:var(--text-muted);margin-bottom:2px">${escapeHtml(q)}</div>
|
|
1974
|
+
<ul style="margin:2px 0 0 16px;padding:0">${items}</ul>
|
|
1975
|
+
</div>`;
|
|
1976
|
+
})
|
|
1977
|
+
.join('');
|
|
1978
|
+
return `<div style="margin-top:10px;padding-top:8px;border-top:1px solid var(--border)">
|
|
1979
|
+
<div style="font-size:0.8rem;color:var(--text-muted);margin-bottom:4px">Answers</div>
|
|
1980
|
+
${rows}
|
|
1981
|
+
</div>`;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1882
1984
|
function renderToolParamsHtml(params) {
|
|
1883
1985
|
if (!params) return '';
|
|
1884
|
-
const BLOCK_KEYS = new Set(['old_string', 'new_string', 'content', 'plan']);
|
|
1986
|
+
const BLOCK_KEYS = new Set(['old_string', 'new_string', 'content', 'contentFull', 'plan']);
|
|
1885
1987
|
const badges = [],
|
|
1886
|
-
blocks = []
|
|
1988
|
+
blocks = [],
|
|
1989
|
+
jsonBlocks = [];
|
|
1887
1990
|
for (const [k, v] of Object.entries(params)) {
|
|
1888
1991
|
if (BLOCK_KEYS.has(k)) continue;
|
|
1992
|
+
// Skip sibling `<k>Full` entries — they're used as expand targets, not
|
|
1993
|
+
// rendered as their own field. Only treat it as a sibling when the
|
|
1994
|
+
// trimmed key holds a server-truncated string (ends with the truncation
|
|
1995
|
+
// marker), otherwise a real param that happens to end in "Full" would
|
|
1996
|
+
// disappear.
|
|
1997
|
+
if (k.endsWith('Full')) {
|
|
1998
|
+
const baseKey = k.slice(0, -4);
|
|
1999
|
+
const base = params[baseKey];
|
|
2000
|
+
if (typeof base === 'string' && base.endsWith('... (truncated)') && typeof v === 'string') {
|
|
2001
|
+
continue;
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
if (v !== null && typeof v === 'object') {
|
|
2005
|
+
let pretty;
|
|
2006
|
+
try {
|
|
2007
|
+
pretty = JSON.stringify(v, null, 2);
|
|
2008
|
+
} catch (_) {
|
|
2009
|
+
pretty = String(v);
|
|
2010
|
+
}
|
|
2011
|
+
if (pretty.length > CONTENT_TRUNCATE_MAX) {
|
|
2012
|
+
pretty = `${pretty.slice(0, CONTENT_TRUNCATE_MAX)}\n... (truncated)`;
|
|
2013
|
+
}
|
|
2014
|
+
jsonBlocks.push({ k, pretty });
|
|
2015
|
+
continue;
|
|
2016
|
+
}
|
|
1889
2017
|
const display = typeof v === 'boolean' ? (v ? 'yes' : 'no') : String(v);
|
|
1890
2018
|
if (display.length > 60) {
|
|
1891
|
-
|
|
2019
|
+
const fullKey = `${k}Full`;
|
|
2020
|
+
const full = typeof params[fullKey] === 'string' ? params[fullKey] : null;
|
|
2021
|
+
blocks.push({ k, display, full });
|
|
1892
2022
|
} else {
|
|
1893
2023
|
badges.push(
|
|
1894
2024
|
`<span style="display:inline-flex;align-items:center;gap:3px;padding:1px 6px;border-radius:3px;background:var(--bg-secondary);font-size:0.75rem"><span style="color:var(--text-muted)">${escapeHtml(k)}:</span> ${escapeHtml(display)}</span>`,
|
|
@@ -1897,8 +2027,19 @@ function renderToolParamsHtml(params) {
|
|
|
1897
2027
|
}
|
|
1898
2028
|
let html = '';
|
|
1899
2029
|
if (badges.length) html += `<div style="margin-top:6px;display:flex;flex-wrap:wrap;gap:4px">${badges.join('')}</div>`;
|
|
1900
|
-
for (const { k, display } of blocks) {
|
|
1901
|
-
|
|
2030
|
+
for (const { k, display, full } of blocks) {
|
|
2031
|
+
let suffix = '';
|
|
2032
|
+
if (full && full.length > display.length) {
|
|
2033
|
+
const toggle = makeExpandToggle(escapeHtml(display), escapeHtml(full), { fontSize: '0.75rem' });
|
|
2034
|
+
suffix = ` ${toggle.btn}${toggle.full}`;
|
|
2035
|
+
}
|
|
2036
|
+
html += `<div style="margin-top:6px;font-size:0.75rem"><span style="color:var(--text-muted)">${escapeHtml(k)}:</span> <span style="word-break:break-all">${escapeHtml(display)}</span>${suffix}</div>`;
|
|
2037
|
+
}
|
|
2038
|
+
for (const { k, pretty } of jsonBlocks) {
|
|
2039
|
+
html += `<div style="margin-top:8px;padding-top:6px;border-top:1px solid var(--border)">
|
|
2040
|
+
<div style="font-size:0.75rem;color:var(--text-muted);margin-bottom:2px">${escapeHtml(k)}</div>
|
|
2041
|
+
<pre class="${TINTED_PRE_CLASS}" style="max-height:300px;overflow:auto;font-size:0.75rem">${escapeHtml(pretty)}</pre>
|
|
2042
|
+
</div>`;
|
|
1902
2043
|
}
|
|
1903
2044
|
if (params.old_string || params.new_string) {
|
|
1904
2045
|
html += `<div style="margin-top:8px;padding-top:6px;border-top:1px solid var(--border)">`;
|
|
@@ -1913,14 +2054,18 @@ function renderToolParamsHtml(params) {
|
|
|
1913
2054
|
html += `</div>`;
|
|
1914
2055
|
}
|
|
1915
2056
|
if (params.content) {
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
2057
|
+
// params.contentFull is set by the server when the truncated `content`
|
|
2058
|
+
// ends with `... (truncated)`. Fall back to params.content otherwise so
|
|
2059
|
+
// small writes render as before.
|
|
2060
|
+
const fullContent = params.contentFull || params.content;
|
|
2061
|
+
const isTruncated = !!params.contentFull || params.content.length > CONTENT_TRUNCATE_MAX;
|
|
2062
|
+
const truncContent = isTruncated
|
|
2063
|
+
? `${params.content.slice(0, CONTENT_TRUNCATE_MAX)}${params.content.length > CONTENT_TRUNCATE_MAX ? '\n... (truncated)' : ''}`
|
|
1919
2064
|
: params.content;
|
|
1920
2065
|
let writeMoreBtn = '',
|
|
1921
2066
|
fullBlock = '';
|
|
1922
|
-
if (
|
|
1923
|
-
const toggle = makeExpandToggle(escapeHtml(truncContent), escapeHtml(
|
|
2067
|
+
if (isTruncated) {
|
|
2068
|
+
const toggle = makeExpandToggle(escapeHtml(truncContent), escapeHtml(fullContent), {
|
|
1924
2069
|
fontSize: '0.75rem',
|
|
1925
2070
|
maxHeight: '500px',
|
|
1926
2071
|
tinted: true,
|
|
@@ -2098,6 +2243,16 @@ async function postAndToast(url, body, label) {
|
|
|
2098
2243
|
async function openMsgInEditor() {
|
|
2099
2244
|
const m = getDetailMsg();
|
|
2100
2245
|
if (!m) return;
|
|
2246
|
+
// Write/Edit tool calls record the source path — open that directly instead
|
|
2247
|
+
// of dumping the rendered modal body into a temp buffer.
|
|
2248
|
+
const filePath =
|
|
2249
|
+
m.type === 'tool_use' && (m.tool === 'Write' || m.tool === 'Edit')
|
|
2250
|
+
? m.params?.file_path || m.fullDetail || null
|
|
2251
|
+
: null;
|
|
2252
|
+
if (filePath) {
|
|
2253
|
+
postAndToast('/api/open-in-editor', { file: filePath }, 'in editor');
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2101
2256
|
const title = m.type === 'tool_use' ? m.tool : m.compactSummary ? 'compact-summary' : m.type;
|
|
2102
2257
|
postAndToast('/api/open-in-editor', { content: getMessageDisplayContent(m), title }, 'in editor');
|
|
2103
2258
|
}
|
|
@@ -2452,7 +2607,6 @@ function renderSessions() {
|
|
|
2452
2607
|
// project filter → search filter → ensure pinned/sticky sessions are always included
|
|
2453
2608
|
let filteredSessions = sessions;
|
|
2454
2609
|
if (sessionFilter === 'active') {
|
|
2455
|
-
const now = Date.now();
|
|
2456
2610
|
const activeSessionIds = new Set();
|
|
2457
2611
|
filteredSessions = filteredSessions.filter((s) => {
|
|
2458
2612
|
if (dismissedSessionIds.has(s.id)) return false;
|
|
@@ -2461,8 +2615,7 @@ function renderSessions() {
|
|
|
2461
2615
|
((!s.sharedTaskList && (s.pending > 0 || s.inProgress > 0)) ||
|
|
2462
2616
|
s.hasActiveAgents ||
|
|
2463
2617
|
s.hasWaitingForUser ||
|
|
2464
|
-
s.hasRecentLog
|
|
2465
|
-
(s.hasPlan && !s.planImplementationSessionId && now - new Date(s.modifiedAt).getTime() <= ACTIVE_PLAN_MS));
|
|
2618
|
+
s.hasRecentLog);
|
|
2466
2619
|
if (isActive) activeSessionIds.add(s.id);
|
|
2467
2620
|
return isActive;
|
|
2468
2621
|
});
|
|
@@ -2602,12 +2755,13 @@ function renderSessions() {
|
|
|
2602
2755
|
${secondaryName ? `<div class="session-secondary">${escapeHtml(secondaryName)}</div>` : ''}
|
|
2603
2756
|
${gitBranch ? `<div class="session-branch">${gitBranch}</div>` : ''}
|
|
2604
2757
|
${session.planTitle ? `<div class="session-plan">${escapeHtml(session.planTitle)}</div>` : ''}
|
|
2758
|
+
${renderGoalSubtitle(session)}
|
|
2605
2759
|
<div class="session-progress">
|
|
2606
2760
|
<span class="session-indicators">
|
|
2607
2761
|
${isTeam ? `<span class="team-badge" title="${memberCount} team members"><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="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>${memberCount}</span>` : ''}
|
|
2608
2762
|
${session.sharedTaskList ? `<span class="shared-tasklist-badge" title="Shared task list: ${escapeHtml(session.sharedTaskList)}">${linkSvg(12)}</span>` : ''}
|
|
2609
2763
|
${isTeam || session.project || showCtx ? `<span class="team-info-btn" onclick="event.stopPropagation(); showSessionInfoModal('${session.id}')" title="View session info">ℹ</span>` : ''}
|
|
2610
|
-
${session.hasPlan ? `<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>` : ''}
|
|
2764
|
+
${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>` : ''}
|
|
2611
2765
|
${renderLoopBadge(session)}
|
|
2612
2766
|
${linkedDocsCount > 0 ? `<span class="linked-docs-badge" onclick="event.stopPropagation(); showSessionInfoModal('${session.id}')" title="${linkedDocsCount} linked document${linkedDocsCount > 1 ? 's' : ''}">${linkSvg(10)}${linkedDocsCount}</span>` : ''}
|
|
2613
2767
|
${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>` : ''}
|
|
@@ -2704,6 +2858,7 @@ function renderSessions() {
|
|
|
2704
2858
|
const folderName = projectPath.split(/[/\\]/).pop();
|
|
2705
2859
|
const isCollapsed = collapsedProjectGroups.has(projectPath);
|
|
2706
2860
|
const escapedPath = escapeHtml(projectPath);
|
|
2861
|
+
const activeCount = projectSessions.reduce((n, s) => n + (isSessionActive(s) ? 1 : 0), 0);
|
|
2707
2862
|
const breadcrumbParts = projectPath
|
|
2708
2863
|
.replace(/^\/home\/[^/]+/, '~')
|
|
2709
2864
|
.split(/[/\\]/)
|
|
@@ -2716,7 +2871,7 @@ function renderSessions() {
|
|
|
2716
2871
|
<div class="project-group-header${isCollapsed ? ' collapsed' : ''}" data-group-path="${escapedPath}">
|
|
2717
2872
|
<svg class="group-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
|
2718
2873
|
<span class="group-name">${escapeHtml(folderName)}</span>
|
|
2719
|
-
<span class="group-count">${projectSessions.length}</span>
|
|
2874
|
+
<span class="group-count" title="${activeCount} active / ${projectSessions.length} total">${activeCount > 0 ? `<span class="group-count-active">${activeCount}</span><span class="group-count-sep">/</span>` : ''}${projectSessions.length}</span>
|
|
2720
2875
|
<span class="project-view-btn" data-project-path="${escapedPath}" title="Open project view — combined tasks from all sessions">
|
|
2721
2876
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
|
|
2722
2877
|
</span>
|
|
@@ -2730,11 +2885,12 @@ function renderSessions() {
|
|
|
2730
2885
|
|
|
2731
2886
|
if (ungrouped.length > 0 && sortedGroups.length > 0) {
|
|
2732
2887
|
const isCollapsed = collapsedProjectGroups.has('__ungrouped__');
|
|
2888
|
+
const ungroupedActive = ungrouped.reduce((n, s) => n + (isSessionActive(s) ? 1 : 0), 0);
|
|
2733
2889
|
html += `
|
|
2734
2890
|
<div class="project-group-header${isCollapsed ? ' collapsed' : ''}" data-group-path="__ungrouped__">
|
|
2735
2891
|
<svg class="group-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
|
2736
2892
|
<span class="group-name">Ungrouped</span>
|
|
2737
|
-
<span class="group-count">${ungrouped.length}</span>
|
|
2893
|
+
<span class="group-count" title="${ungroupedActive} active / ${ungrouped.length} total">${ungroupedActive > 0 ? `<span class="group-count-active">${ungroupedActive}</span><span class="group-count-sep">/</span>` : ''}${ungrouped.length}</span>
|
|
2738
2894
|
</div>
|
|
2739
2895
|
<div class="project-group-sessions${isCollapsed ? ' collapsed' : ''}">
|
|
2740
2896
|
${renderGroupSessions(ungrouped, '__pinned___ungrouped__')}
|
|
@@ -4256,13 +4412,14 @@ function matchKey(e, ...keys) {
|
|
|
4256
4412
|
return keys.some((k) => e.key === k || e.code === k);
|
|
4257
4413
|
}
|
|
4258
4414
|
|
|
4259
|
-
const MODAL_ESC_PRIORITY = ['preview-modal', 'msg-detail-modal', 'plan-modal', 'loop-modal'];
|
|
4415
|
+
const MODAL_ESC_PRIORITY = ['preview-modal', 'msg-detail-modal', 'tool-stats-modal', 'plan-modal', 'loop-modal'];
|
|
4260
4416
|
const MODAL_CLOSERS = {
|
|
4261
4417
|
'preview-modal': () => closePreviewModal(),
|
|
4262
4418
|
'msg-detail-modal': () => {
|
|
4263
4419
|
closeMsgDetailModal();
|
|
4264
4420
|
msgDetailFollowLatest = false;
|
|
4265
4421
|
},
|
|
4422
|
+
'tool-stats-modal': () => closeToolStatsModal(),
|
|
4266
4423
|
'plan-modal': () => closePlanModal(),
|
|
4267
4424
|
'loop-modal': () => closeLoopModal(),
|
|
4268
4425
|
'team-modal': () => closeTeamModal(),
|
|
@@ -5791,6 +5948,13 @@ function showInfoModal(session, teamConfig, tasks, planContent, parentInfo) {
|
|
|
5791
5948
|
});
|
|
5792
5949
|
html += `</div>`;
|
|
5793
5950
|
|
|
5951
|
+
if (session.goal?.condition) {
|
|
5952
|
+
html += `<div class="info-goal-card">
|
|
5953
|
+
<div class="info-goal-head"><span class="info-goal-icon">◎</span>Goal</div>
|
|
5954
|
+
<div class="info-goal-text">${escapeHtml(session.goal.condition)}</div>
|
|
5955
|
+
</div>`;
|
|
5956
|
+
}
|
|
5957
|
+
|
|
5794
5958
|
if (session.contextStatus) {
|
|
5795
5959
|
html += `<hr style="border: none; border-top: 1px solid var(--border); margin: 12px 0;">`;
|
|
5796
5960
|
html += renderContextDetail(session.contextStatus);
|
|
@@ -5885,6 +6049,7 @@ function showInfoModal(session, teamConfig, tasks, planContent, parentInfo) {
|
|
|
5885
6049
|
const keyHandler = (e) => {
|
|
5886
6050
|
if (e.key === 'Escape') {
|
|
5887
6051
|
if (document.getElementById('plan-modal').classList.contains('visible')) return;
|
|
6052
|
+
if (document.getElementById('tool-stats-modal').classList.contains('visible')) return;
|
|
5888
6053
|
e.preventDefault();
|
|
5889
6054
|
closeTeamModal();
|
|
5890
6055
|
document.removeEventListener('keydown', keyHandler);
|
|
@@ -6031,6 +6196,16 @@ function renderLoopModalBody(data) {
|
|
|
6031
6196
|
body.innerHTML = section('Wakeups', wakeups, 'wakeup') + section('Cron jobs', crons, 'cron');
|
|
6032
6197
|
}
|
|
6033
6198
|
|
|
6199
|
+
function renderGoalSubtitle(session) {
|
|
6200
|
+
const g = session.goal;
|
|
6201
|
+
if (!g?.condition) return '';
|
|
6202
|
+
const short = g.condition.length > 70 ? `${g.condition.slice(0, 70)}…` : g.condition;
|
|
6203
|
+
// Only active (unmet) goals reach here — a met goal auto-clears. Clicking
|
|
6204
|
+
// opens the info modal (full text); stopPropagation so it doesn't also
|
|
6205
|
+
// trigger the card's fetchTasks.
|
|
6206
|
+
return `<div class="session-goal" onclick="event.stopPropagation(); showSessionInfoModal('${session.id}')" title="${escapeHtml(g.condition)}"><span class="session-goal-icon">◎</span>${escapeHtml(short)}</div>`;
|
|
6207
|
+
}
|
|
6208
|
+
|
|
6034
6209
|
function renderLoopBadge(session) {
|
|
6035
6210
|
const li = session.loopInfo;
|
|
6036
6211
|
const total = (li?.wakeupCount || 0) + (li?.cronCount || 0);
|
|
@@ -6140,6 +6315,108 @@ function openMemoryForInfoModal() {
|
|
|
6140
6315
|
|
|
6141
6316
|
//#endregion
|
|
6142
6317
|
|
|
6318
|
+
//#region TOOL_STATS_MODAL
|
|
6319
|
+
let _toolStatsSortCol = 'count';
|
|
6320
|
+
let _toolStatsSortDir = 'desc';
|
|
6321
|
+
let _toolStatsData = null;
|
|
6322
|
+
|
|
6323
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
6324
|
+
function showToolStatsModal(sessionId) {
|
|
6325
|
+
if (!sessionId) return;
|
|
6326
|
+
const body = document.getElementById('tool-stats-modal-body');
|
|
6327
|
+
body.innerHTML = '<div style="padding:16px;color:var(--text-secondary);">Loading…</div>';
|
|
6328
|
+
document.getElementById('tool-stats-modal').classList.add('visible');
|
|
6329
|
+
|
|
6330
|
+
fetch(`/api/sessions/${sessionId}/tool-stats`)
|
|
6331
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
6332
|
+
.catch(() => null)
|
|
6333
|
+
.then((data) => {
|
|
6334
|
+
if (!data) {
|
|
6335
|
+
body.innerHTML = '<div style="padding:16px;color:var(--text-secondary);">Failed to load tool statistics.</div>';
|
|
6336
|
+
return;
|
|
6337
|
+
}
|
|
6338
|
+
_toolStatsSortCol = 'count';
|
|
6339
|
+
_toolStatsSortDir = 'desc';
|
|
6340
|
+
_toolStatsData = data;
|
|
6341
|
+
body.innerHTML = renderToolStatsBody(data);
|
|
6342
|
+
});
|
|
6343
|
+
}
|
|
6344
|
+
|
|
6345
|
+
function renderToolStatsBody(data) {
|
|
6346
|
+
const { totalCalls, uniqueTools, totalFailed, totalRejected, tools } = data;
|
|
6347
|
+
|
|
6348
|
+
const summary = `
|
|
6349
|
+
<div class="tool-stats-summary">
|
|
6350
|
+
<div class="tool-stats-chip"><span class="tool-stats-chip-val">${totalCalls}</span><span class="tool-stats-chip-lbl">Total calls</span></div>
|
|
6351
|
+
<div class="tool-stats-chip"><span class="tool-stats-chip-val">${uniqueTools}</span><span class="tool-stats-chip-lbl">Unique tools</span></div>
|
|
6352
|
+
<div class="tool-stats-chip"><span class="tool-stats-chip-val${totalFailed > 0 ? ' failed' : ''}">${totalFailed}</span><span class="tool-stats-chip-lbl">Failed</span></div>
|
|
6353
|
+
<div class="tool-stats-chip"><span class="tool-stats-chip-val${totalRejected > 0 ? ' rejected' : ''}">${totalRejected}</span><span class="tool-stats-chip-lbl">Rejected</span></div>
|
|
6354
|
+
</div>`;
|
|
6355
|
+
|
|
6356
|
+
if (!tools?.length) {
|
|
6357
|
+
return (
|
|
6358
|
+
summary +
|
|
6359
|
+
'<div style="padding:24px;text-align:center;color:var(--text-tertiary);">No tool calls recorded in this session.</div>'
|
|
6360
|
+
);
|
|
6361
|
+
}
|
|
6362
|
+
|
|
6363
|
+
const sorted = [...tools].sort((a, b) => {
|
|
6364
|
+
if (_toolStatsSortCol === 'name')
|
|
6365
|
+
return _toolStatsSortDir === 'asc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
|
|
6366
|
+
return _toolStatsSortDir === 'asc'
|
|
6367
|
+
? a[_toolStatsSortCol] - b[_toolStatsSortCol]
|
|
6368
|
+
: b[_toolStatsSortCol] - a[_toolStatsSortCol];
|
|
6369
|
+
});
|
|
6370
|
+
const arrow = (col) => (col === _toolStatsSortCol ? (_toolStatsSortDir === 'asc' ? ' ▲' : ' ▼') : '');
|
|
6371
|
+
|
|
6372
|
+
const table = `<table class="tool-stats-table">
|
|
6373
|
+
<thead><tr>
|
|
6374
|
+
<th onclick="toolStatsSortBy('name')">Tool${arrow('name')}</th>
|
|
6375
|
+
<th onclick="toolStatsSortBy('count')">Calls${arrow('count')}</th>
|
|
6376
|
+
<th onclick="toolStatsSortBy('success')">✓ Success${arrow('success')}</th>
|
|
6377
|
+
<th onclick="toolStatsSortBy('failed')">✗ Failed${arrow('failed')}</th>
|
|
6378
|
+
<th onclick="toolStatsSortBy('rejected')">⊘ Rejected${arrow('rejected')}</th>
|
|
6379
|
+
<th onclick="toolStatsSortBy('impact')" title="Share of total tool output by character count">Impact${arrow('impact')}</th>
|
|
6380
|
+
</tr></thead>
|
|
6381
|
+
<tbody>${sorted
|
|
6382
|
+
.map(
|
|
6383
|
+
(t) => `<tr>
|
|
6384
|
+
<td class="tool-name">${escapeHtml(t.name)}</td>
|
|
6385
|
+
<td>${t.count}</td>
|
|
6386
|
+
<td>${t.success > 0 ? `<span class="badge-success">${t.success}</span>` : '—'}</td>
|
|
6387
|
+
<td>${t.failed > 0 ? `<span class="badge-failed">${t.failed}</span>` : '—'}</td>
|
|
6388
|
+
<td>${t.rejected > 0 ? `<span class="badge-rejected">${t.rejected}</span>` : '—'}</td>
|
|
6389
|
+
<td class="impact-cell">${
|
|
6390
|
+
t.impact != null
|
|
6391
|
+
? `<div class="impact-cell-inner"><div class="impact-bar-wrap"><div class="impact-bar-fill" style="width:${t.impact}%"></div></div><span class="impact-pct">${t.impact < 1 ? '<1' : t.impact}%</span></div>`
|
|
6392
|
+
: '—'
|
|
6393
|
+
}</td>
|
|
6394
|
+
</tr>`,
|
|
6395
|
+
)
|
|
6396
|
+
.join('')}</tbody>
|
|
6397
|
+
</table>`;
|
|
6398
|
+
|
|
6399
|
+
return summary + table;
|
|
6400
|
+
}
|
|
6401
|
+
|
|
6402
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
6403
|
+
function toolStatsSortBy(col) {
|
|
6404
|
+
if (_toolStatsSortCol === col) {
|
|
6405
|
+
_toolStatsSortDir = _toolStatsSortDir === 'asc' ? 'desc' : 'asc';
|
|
6406
|
+
} else {
|
|
6407
|
+
_toolStatsSortCol = col;
|
|
6408
|
+
_toolStatsSortDir = col === 'name' ? 'asc' : 'desc';
|
|
6409
|
+
}
|
|
6410
|
+
if (!_toolStatsData) return;
|
|
6411
|
+
const body = document.getElementById('tool-stats-modal-body');
|
|
6412
|
+
body.innerHTML = renderToolStatsBody(_toolStatsData);
|
|
6413
|
+
}
|
|
6414
|
+
|
|
6415
|
+
function closeToolStatsModal() {
|
|
6416
|
+
document.getElementById('tool-stats-modal').classList.remove('visible');
|
|
6417
|
+
}
|
|
6418
|
+
//#endregion
|
|
6419
|
+
|
|
6143
6420
|
//#region OWNER_FILTER
|
|
6144
6421
|
function updateOwnerFilter() {
|
|
6145
6422
|
const bar = document.getElementById('owner-filter-bar');
|
|
@@ -6239,6 +6516,11 @@ try {
|
|
|
6239
6516
|
// biome-ignore lint/suspicious/useIterableCallbackReturn: forEach side-effect
|
|
6240
6517
|
cg.forEach((p) => collapsedProjectGroups.add(p));
|
|
6241
6518
|
} catch (_) {}
|
|
6519
|
+
try {
|
|
6520
|
+
const af = JSON.parse(localStorage.getItem('activityFilter') || '[]');
|
|
6521
|
+
// biome-ignore lint/suspicious/useIterableCallbackReturn: forEach side-effect
|
|
6522
|
+
af.forEach((k) => activityFilter.add(k));
|
|
6523
|
+
} catch (_) {}
|
|
6242
6524
|
initSidebarResize();
|
|
6243
6525
|
loadPanelWidths();
|
|
6244
6526
|
initPanelResize('detail-panel', 'detail-panel-resize', '--detail-panel-width', 'detail-panel-width');
|
package/public/index.html
CHANGED
|
@@ -543,11 +543,30 @@
|
|
|
543
543
|
<div id="team-modal-body" class="modal-body"></div>
|
|
544
544
|
<div class="modal-footer">
|
|
545
545
|
<button id="session-info-dismiss-btn" class="btn btn-secondary" onclick="toggleDismissSession(_infoModalSessionId); closeTeamModal()">Dismiss</button>
|
|
546
|
+
<button class="btn btn-secondary" onclick="showToolStatsModal(_infoModalSessionId)" title="Tool invocation statistics">Tool Stats</button>
|
|
546
547
|
<button class="btn btn-primary" onclick="closeTeamModal()">Close</button>
|
|
547
548
|
</div>
|
|
548
549
|
</div>
|
|
549
550
|
</div>
|
|
550
551
|
|
|
552
|
+
<!-- Tool Stats Modal (stacked on top of info modal) -->
|
|
553
|
+
<div id="tool-stats-modal" class="modal-overlay plan-modal-overlay" onclick="closeToolStatsModal()">
|
|
554
|
+
<div class="modal tool-stats-modal" onclick="event.stopPropagation()">
|
|
555
|
+
<div class="modal-header">
|
|
556
|
+
<h3 id="tool-stats-modal-title" class="modal-title">Tool Statistics</h3>
|
|
557
|
+
<button class="modal-close" aria-label="Close dialog" onclick="closeToolStatsModal()">
|
|
558
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
559
|
+
<path d="M18 6L6 18M6 6l12 12"/>
|
|
560
|
+
</svg>
|
|
561
|
+
</button>
|
|
562
|
+
</div>
|
|
563
|
+
<div id="tool-stats-modal-body" class="modal-body" style="overflow-y:auto;flex:1 1 auto;min-height:0;"></div>
|
|
564
|
+
<div class="modal-footer">
|
|
565
|
+
<button class="btn btn-primary" onclick="closeToolStatsModal()">Close</button>
|
|
566
|
+
</div>
|
|
567
|
+
</div>
|
|
568
|
+
</div>
|
|
569
|
+
|
|
551
570
|
<!-- Plan Modal (stacked on top of info modal) -->
|
|
552
571
|
<div id="plan-modal" class="modal-overlay plan-modal-overlay" onclick="closePlanModal()">
|
|
553
572
|
<div class="modal plan-modal" onclick="event.stopPropagation()">
|