claude-code-kanban 4.4.0 → 4.6.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 +171 -15
- 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 +278 -56
- package/public/style.css +212 -81
- package/server.js +164 -59
package/public/app.js
CHANGED
|
@@ -27,6 +27,8 @@ let agentDurationInterval = null;
|
|
|
27
27
|
let agentPollInterval = null;
|
|
28
28
|
let selectedTaskId = null;
|
|
29
29
|
let selectedSessionId = null;
|
|
30
|
+
// Task stays selected (keyboard nav) but its white highlight is dimmed once the detail panel closes.
|
|
31
|
+
let taskHighlightDimmed = false;
|
|
30
32
|
let focusZone = 'board'; // 'board' | 'sidebar'
|
|
31
33
|
let appConfig = { marketplaceUrl: null, costUrl: null, memoryUrl: null };
|
|
32
34
|
let selectedSessionIdx = -1;
|
|
@@ -161,12 +163,16 @@ async function fetchSessions(includeTasks = true) {
|
|
|
161
163
|
const allPinnedIds = new Set([...pinnedSessionIds, ...stickySessionIds]);
|
|
162
164
|
if (revealedPlanSessionId) allPinnedIds.add(revealedPlanSessionId);
|
|
163
165
|
if (revealedStorageSessionId) allPinnedIds.add(revealedStorageSessionId);
|
|
166
|
+
// When server filters by activity, the focused session may not be active —
|
|
167
|
+
// include it in pinned so the server still returns it.
|
|
168
|
+
if (sessionFilter === 'active' && currentSessionId) allPinnedIds.add(currentSessionId);
|
|
164
169
|
const pinnedParam = allPinnedIds.size > 0 ? `&pinned=${[...allPinnedIds].join(',')}` : '';
|
|
165
170
|
const projectParam =
|
|
166
171
|
filterProject && filterProject !== '__recent__' ? `&project=${encodeURIComponent(filterProject)}` : '';
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
172
|
+
const filterParam = sessionFilter === 'active' ? '&filter=active' : '';
|
|
173
|
+
const sessionsPromise = fetch(
|
|
174
|
+
`/api/sessions?limit=${sessionLimit}${pinnedParam}${projectParam}${filterParam}`,
|
|
175
|
+
).then((r) => r.json());
|
|
170
176
|
|
|
171
177
|
let newSessions, newTasks;
|
|
172
178
|
if (includeTasks) {
|
|
@@ -582,7 +588,6 @@ async function fetchTasks(sessionId) {
|
|
|
582
588
|
const WAITING_TTL_MS = 30 * 60 * 1000;
|
|
583
589
|
const AGENT_LOG_MAX = 8;
|
|
584
590
|
const LIVE_INDICATOR_MS = 10 * 1000;
|
|
585
|
-
const ACTIVE_PLAN_MS = 10 * 60 * 1000;
|
|
586
591
|
// #endregion
|
|
587
592
|
|
|
588
593
|
function resetAgentState() {
|
|
@@ -1026,7 +1031,7 @@ function renderToolItem(m, i, compact) {
|
|
|
1026
1031
|
? `onclick="showAgentModal('${escapeHtml(m.agentId)}')" ${combinedStyle}`
|
|
1027
1032
|
: `onclick="msgDetailFollowLatest=false;showMsgDetail(${i})" ${combinedStyle}`;
|
|
1028
1033
|
const pinBtn = renderMsgPinBtn(m, i);
|
|
1029
|
-
return `<div class="msg-item msg-tool${compactClass}" ${itemClickAttr}>
|
|
1034
|
+
return `<div class="msg-item msg-tool${compactClass}" data-msg-idx="${i}" ${itemClickAttr}>
|
|
1030
1035
|
${getToolIcon(m.tool)}
|
|
1031
1036
|
<div class="msg-body"><div class="msg-text">${escapeHtml(m.tool)}${toolDetail}${agentLink}</div><div class="msg-time">${formatDate(m.timestamp)}</div></div>${agentLogBtn}${pinBtn}
|
|
1032
1037
|
</div>`;
|
|
@@ -1068,7 +1073,7 @@ function renderMessageList(messages) {
|
|
|
1068
1073
|
continue;
|
|
1069
1074
|
}
|
|
1070
1075
|
|
|
1071
|
-
const clickable = `onclick="msgDetailFollowLatest=false;showMsgDetail(${i})" style="cursor:pointer"`;
|
|
1076
|
+
const clickable = `data-msg-idx="${i}" onclick="msgDetailFollowLatest=false;showMsgDetail(${i})" style="cursor:pointer"`;
|
|
1072
1077
|
const pinBtn = renderMsgPinBtn(m, i);
|
|
1073
1078
|
if (m.type === 'user') {
|
|
1074
1079
|
if (m.systemLabel) {
|
|
@@ -1094,9 +1099,10 @@ function renderMessageList(messages) {
|
|
|
1094
1099
|
if (displayText) textHtml = isCmd ? `<code>${escapeHtml(displayText)}</code>` : displayText;
|
|
1095
1100
|
else if (chips.length) textHtml = '<em class="msg-text-muted">(attachment)</em>';
|
|
1096
1101
|
else textHtml = '';
|
|
1097
|
-
|
|
1102
|
+
const queuedTag = m.queued ? '<span class="msg-queued-tag">queued</span>' : '';
|
|
1103
|
+
parts.push(`<div class="msg-item msg-user${isCmd ? ' msg-cmd' : ''}${m.queued ? ' msg-queued' : ''}" ${clickable}>
|
|
1098
1104
|
${MSG_ICON_USER}
|
|
1099
|
-
<div class="msg-body"><div class="msg-text">${textHtml}${cmdArgsHtml}</div>${chipsHtml}<div class="msg-time">${formatDate(m.timestamp)}</div></div>${pinBtn}
|
|
1105
|
+
<div class="msg-body"><div class="msg-text">${textHtml}${cmdArgsHtml}</div>${chipsHtml}<div class="msg-time">${formatDate(m.timestamp)}${queuedTag}</div></div>${pinBtn}
|
|
1100
1106
|
</div>`);
|
|
1101
1107
|
}
|
|
1102
1108
|
} else if (m.type === 'assistant') {
|
|
@@ -1144,42 +1150,80 @@ function toggleToolGroup(id) {
|
|
|
1144
1150
|
if (el) el.classList.toggle('show');
|
|
1145
1151
|
}
|
|
1146
1152
|
|
|
1147
|
-
const WAITING_PLAN_PREVIEW_CHARS = 120;
|
|
1148
|
-
const WAITING_PREVIEW_MAX_CHARS = 200;
|
|
1149
|
-
|
|
1150
1153
|
function getWaitingLabel(kind, tool) {
|
|
1151
1154
|
if (kind !== 'question') return `Awaiting permission: ${tool}`;
|
|
1152
1155
|
if (tool === 'ExitPlanMode') return 'Plan awaiting approval';
|
|
1153
1156
|
return 'Question pending';
|
|
1154
1157
|
}
|
|
1155
1158
|
|
|
1156
|
-
function
|
|
1157
|
-
if (
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1159
|
+
function getWaitingPill(kind, tool) {
|
|
1160
|
+
if (kind === 'question' && tool === 'ExitPlanMode') return 'Plan awaiting approval';
|
|
1161
|
+
if (kind === 'question') return 'Question pending';
|
|
1162
|
+
return 'Awaiting permission';
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function deriveWaitingDetail(tool, params) {
|
|
1166
|
+
if (!params) return '';
|
|
1167
|
+
const trunc = (s) => (s.length > 80 ? `${s.slice(0, 80)}...` : s);
|
|
1168
|
+
if (params.file_path) return params.file_path.replace(/^.*[/\\]/, '');
|
|
1169
|
+
if (params.command) return trunc(params.command);
|
|
1170
|
+
if (params.pattern) return trunc(params.pattern);
|
|
1171
|
+
if (params.query) return trunc(params.query);
|
|
1172
|
+
if (params.url) return trunc(params.url);
|
|
1173
|
+
if (params.skill) {
|
|
1174
|
+
const s = params.skill + (typeof params.args === 'string' ? ` ${params.args}` : '');
|
|
1175
|
+
return trunc(s);
|
|
1176
|
+
}
|
|
1177
|
+
if (tool === 'AskUserQuestion' && params.questions?.[0]?.question) return trunc(params.questions[0].question);
|
|
1178
|
+
if (tool === 'ExitPlanMode' && typeof params.plan === 'string') {
|
|
1179
|
+
const t = params.plan.match(/^#\s+(.+)/m);
|
|
1180
|
+
return trunc(t ? t[1] : params.plan);
|
|
1181
|
+
}
|
|
1182
|
+
if (params.description) return trunc(String(params.description));
|
|
1170
1183
|
return '';
|
|
1171
1184
|
}
|
|
1172
1185
|
|
|
1186
|
+
function renderWaitingBody(tool, params) {
|
|
1187
|
+
if (!params) return '';
|
|
1188
|
+
if (tool === 'AskUserQuestion' && Array.isArray(params.questions)) {
|
|
1189
|
+
const items = params.questions
|
|
1190
|
+
.map((q) => {
|
|
1191
|
+
const head = `<div style="font-weight:600">${escapeHtml(q.question || '')}</div>`;
|
|
1192
|
+
const opts = Array.isArray(q.options)
|
|
1193
|
+
? `<ul style="margin:2px 0 0 16px;padding:0">${q.options
|
|
1194
|
+
.map(
|
|
1195
|
+
(o) =>
|
|
1196
|
+
`<li><span style="font-weight:600">${escapeHtml(o.label || '')}</span>${o.description ? ` — <span style="color:var(--text-muted)">${escapeHtml(o.description)}</span>` : ''}</li>`,
|
|
1197
|
+
)
|
|
1198
|
+
.join('')}</ul>`
|
|
1199
|
+
: '';
|
|
1200
|
+
return `<div style="margin-top:6px">${head}${opts}</div>`;
|
|
1201
|
+
})
|
|
1202
|
+
.join('');
|
|
1203
|
+
return items;
|
|
1204
|
+
}
|
|
1205
|
+
return renderToolParamsHtml(params);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1173
1208
|
function renderWaitingEntry() {
|
|
1174
1209
|
if (!isWaitingFresh()) return '';
|
|
1175
1210
|
const tool = currentWaiting.toolName || 'unknown';
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1211
|
+
let params = null;
|
|
1212
|
+
if (currentWaiting.toolInput) {
|
|
1213
|
+
try {
|
|
1214
|
+
params = JSON.parse(currentWaiting.toolInput);
|
|
1215
|
+
} catch (_) {
|
|
1216
|
+
/* toolInput may be truncated/non-JSON */
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
const pillText = getWaitingPill(currentWaiting.kind, tool);
|
|
1220
|
+
const detail = deriveWaitingDetail(tool, params);
|
|
1221
|
+
const detailHtml = detail ? ` <span style="color:var(--text-secondary)">${escapeHtml(detail)}</span>` : '';
|
|
1222
|
+
const bodyHtml = renderWaitingBody(tool, params);
|
|
1223
|
+
const bodyWrap = bodyHtml ? `<div class="msg-waiting-body">${bodyHtml}</div>` : '';
|
|
1224
|
+
const pill = `<span class="msg-waiting-pill">${escapeHtml(pillText)}</span>`;
|
|
1181
1225
|
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>`;
|
|
1182
|
-
return `<div class="msg-item msg-waiting" onclick="msgDetailFollowLatest=false;showWaitingDetail()">${
|
|
1226
|
+
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>`;
|
|
1183
1227
|
}
|
|
1184
1228
|
|
|
1185
1229
|
// biome-ignore lint/correctness/noUnusedVariables: used in HTML onclick
|
|
@@ -1214,6 +1258,7 @@ function renderMessages(messages) {
|
|
|
1214
1258
|
? `<div class="msg-limit-banner">Showing last ${MSG_MAX_LOADED} messages</div>`
|
|
1215
1259
|
: '';
|
|
1216
1260
|
container.innerHTML = limitBanner + msgsHtml + renderWaitingEntry();
|
|
1261
|
+
highlightSelectedMsg();
|
|
1217
1262
|
if (!msgUserScrolledUp) container.scrollTop = container.scrollHeight;
|
|
1218
1263
|
// Auto-load more if content doesn't overflow yet
|
|
1219
1264
|
if (
|
|
@@ -1228,6 +1273,8 @@ function renderMessages(messages) {
|
|
|
1228
1273
|
|
|
1229
1274
|
let currentMsgDetailIdx = null;
|
|
1230
1275
|
let msgDetailFollowLatest = false;
|
|
1276
|
+
// Message stays tracked but its highlight is dimmed once the detail modal closes.
|
|
1277
|
+
let msgHighlightDimmed = false;
|
|
1231
1278
|
const MSG_DETAIL_WAITING_IDX = -2;
|
|
1232
1279
|
let currentPins = [];
|
|
1233
1280
|
let pinnedCollapsed = false;
|
|
@@ -1246,6 +1293,8 @@ const MSG_ICON_TEAMMATE =
|
|
|
1246
1293
|
'<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>';
|
|
1247
1294
|
const MSG_ICON_IDLE =
|
|
1248
1295
|
'<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6"/></svg>';
|
|
1296
|
+
const ICON_AGENT =
|
|
1297
|
+
'<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><path d="M9 2v2M15 2v2M9 20v2M15 20v2M20 9h2M20 14h2M2 9h2M2 14h2"/></svg>';
|
|
1249
1298
|
const ICON_TASK =
|
|
1250
1299
|
'<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>';
|
|
1251
1300
|
const ICON_WEB =
|
|
@@ -1268,7 +1317,7 @@ const TOOL_ICONS = {
|
|
|
1268
1317
|
Edit: '<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>',
|
|
1269
1318
|
Glob: '<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/><circle cx="14" cy="14" r="3"/><line x1="16.5" y1="16.5" x2="19" y2="19"/></svg>',
|
|
1270
1319
|
Grep: '<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>',
|
|
1271
|
-
Agent:
|
|
1320
|
+
Agent: ICON_AGENT,
|
|
1272
1321
|
SendMessage:
|
|
1273
1322
|
'<svg class="msg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>',
|
|
1274
1323
|
TaskCreate: ICON_TASK,
|
|
@@ -1354,6 +1403,20 @@ function togglePin(msgIndex) {
|
|
|
1354
1403
|
currentPins.splice(idx, 1);
|
|
1355
1404
|
} else {
|
|
1356
1405
|
pinnedCollapsed = false;
|
|
1406
|
+
// Strip large server-truncated `*Full` payloads (Write contentFull,
|
|
1407
|
+
// MCP passthrough <k>Full) before stashing in localStorage — a few
|
|
1408
|
+
// pinned big writes can blow past the per-origin quota. On pin
|
|
1409
|
+
// expand, the modal falls back to the truncated string + the lazy
|
|
1410
|
+
// /api/sessions/:id/tool-result/:toolUseId endpoint.
|
|
1411
|
+
let paramsForPin = null;
|
|
1412
|
+
if (m.params) {
|
|
1413
|
+
paramsForPin = {};
|
|
1414
|
+
for (const [k, v] of Object.entries(m.params)) {
|
|
1415
|
+
if (k === 'contentFull') continue;
|
|
1416
|
+
if (k.endsWith('Full') && typeof v === 'string' && typeof m.params[k.slice(0, -4)] === 'string') continue;
|
|
1417
|
+
paramsForPin[k] = v;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1357
1420
|
currentPins.push({
|
|
1358
1421
|
id,
|
|
1359
1422
|
type: m.type,
|
|
@@ -1363,6 +1426,9 @@ function togglePin(msgIndex) {
|
|
|
1363
1426
|
toolUseId: m.toolUseId || null,
|
|
1364
1427
|
toolResult: m.toolResult || null,
|
|
1365
1428
|
toolResultTruncated: m.toolResultTruncated || false,
|
|
1429
|
+
toolResultFull: null,
|
|
1430
|
+
answerPayload: m.answerPayload || null,
|
|
1431
|
+
params: paramsForPin,
|
|
1366
1432
|
detail: m.detail || null,
|
|
1367
1433
|
fullDetail: m.fullDetail || null,
|
|
1368
1434
|
description: m.description || null,
|
|
@@ -1588,10 +1654,20 @@ const linkSvg = (size) =>
|
|
|
1588
1654
|
//#region MODALS
|
|
1589
1655
|
function renderUserAttachments(m) {
|
|
1590
1656
|
const parts = [];
|
|
1591
|
-
if (m.images?.length &&
|
|
1657
|
+
if (m.images?.length && currentSessionId) {
|
|
1658
|
+
const sid = encodeURIComponent(currentSessionId);
|
|
1592
1659
|
const imgs = m.images
|
|
1593
1660
|
.map((img) => {
|
|
1594
|
-
|
|
1661
|
+
// Cache-kind images live on disk (image-cache/<sessionId>/<n>.png); base64
|
|
1662
|
+
// images are read from the JSONL block and need the message uuid.
|
|
1663
|
+
let url;
|
|
1664
|
+
if (img.kind === 'cache') {
|
|
1665
|
+
url = `/api/sessions/${sid}/cached-image/${img.n}`;
|
|
1666
|
+
} else if (m.uuid) {
|
|
1667
|
+
url = `/api/sessions/${sid}/user-image/${encodeURIComponent(m.uuid)}/${img.blockIndex}`;
|
|
1668
|
+
} else {
|
|
1669
|
+
return '';
|
|
1670
|
+
}
|
|
1595
1671
|
return `<img src="${url}" loading="lazy" alt="user image" class="user-attach-image" />`;
|
|
1596
1672
|
})
|
|
1597
1673
|
.join('');
|
|
@@ -1620,10 +1696,22 @@ function renderUserAttachments(m) {
|
|
|
1620
1696
|
return parts.join('');
|
|
1621
1697
|
}
|
|
1622
1698
|
|
|
1699
|
+
// Highlight the message whose detail is open, mirroring task-card selection.
|
|
1700
|
+
function highlightSelectedMsg() {
|
|
1701
|
+
const container = document.getElementById('message-panel-content');
|
|
1702
|
+
if (!container) return;
|
|
1703
|
+
for (const el of container.querySelectorAll('.msg-item.selected')) el.classList.remove('selected');
|
|
1704
|
+
if (msgHighlightDimmed || currentMsgDetailIdx == null || currentMsgDetailIdx < 0) return;
|
|
1705
|
+
const el = container.querySelector(`.msg-item[data-msg-idx="${currentMsgDetailIdx}"]`);
|
|
1706
|
+
if (el) el.classList.add('selected');
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1623
1709
|
function showMsgDetail(idx) {
|
|
1624
1710
|
currentMsgDetailIdx = idx;
|
|
1711
|
+
msgHighlightDimmed = false;
|
|
1625
1712
|
const m = currentMessages[idx];
|
|
1626
1713
|
if (!m) return;
|
|
1714
|
+
highlightSelectedMsg();
|
|
1627
1715
|
const body = document.getElementById('msg-detail-body');
|
|
1628
1716
|
if (m.type === 'tool_use') {
|
|
1629
1717
|
document.getElementById('msg-detail-title').textContent = m.tool;
|
|
@@ -1671,7 +1759,9 @@ function showMsgDetail(idx) {
|
|
|
1671
1759
|
} else {
|
|
1672
1760
|
mainHtml = TASK_TOOLS.has(m.tool) ? '' : '<em>No details</em>';
|
|
1673
1761
|
}
|
|
1674
|
-
|
|
1762
|
+
const answersHtml = m.answerPayload ? renderAnswerPayloadHtml(m.answerPayload) : '';
|
|
1763
|
+
body.innerHTML =
|
|
1764
|
+
mainHtml + toolParamsHtml + answersHtml + taskResultHtml + (hasAgentTabs ? '' : toolResultHtml) + agentExtraHtml;
|
|
1675
1765
|
} else if (m.type === 'teammate') {
|
|
1676
1766
|
document.getElementById('msg-detail-title').textContent = m.teammateId || 'Teammate';
|
|
1677
1767
|
document.getElementById('msg-detail-agent-btn').style.display = 'none';
|
|
@@ -1725,6 +1815,12 @@ function showMsgDetail(idx) {
|
|
|
1725
1815
|
function closeMsgDetailModal() {
|
|
1726
1816
|
resetModalFullscreen('msg-detail-modal');
|
|
1727
1817
|
msgDetailFollowLatest = false;
|
|
1818
|
+
// Drop the message highlight on close, mirroring task-card behavior.
|
|
1819
|
+
msgHighlightDimmed = true;
|
|
1820
|
+
const container = document.getElementById('message-panel-content');
|
|
1821
|
+
if (container) {
|
|
1822
|
+
for (const el of container.querySelectorAll('.msg-item.selected')) el.classList.remove('selected');
|
|
1823
|
+
}
|
|
1728
1824
|
}
|
|
1729
1825
|
|
|
1730
1826
|
function _setModalWidth(modal, slot, on, maxWidth, width) {
|
|
@@ -1890,16 +1986,75 @@ function renderTaskResult(toolResult) {
|
|
|
1890
1986
|
return `${html}</div>`;
|
|
1891
1987
|
}
|
|
1892
1988
|
|
|
1989
|
+
function renderAnswerPayloadHtml(answerPayload) {
|
|
1990
|
+
if (!answerPayload?.answers || typeof answerPayload.answers !== 'object') return '';
|
|
1991
|
+
const qs = Array.isArray(answerPayload.questions) ? answerPayload.questions : [];
|
|
1992
|
+
const findOptionDesc = (qText, label) => {
|
|
1993
|
+
const q = qs.find((x) => x && x.question === qText);
|
|
1994
|
+
if (!q || !Array.isArray(q.options)) return null;
|
|
1995
|
+
const opt = q.options.find((o) => o && o.label === label);
|
|
1996
|
+
return opt?.description ? opt.description : null;
|
|
1997
|
+
};
|
|
1998
|
+
const rows = Object.entries(answerPayload.answers)
|
|
1999
|
+
.map(([q, a]) => {
|
|
2000
|
+
const ansList = Array.isArray(a) ? a : [a];
|
|
2001
|
+
const items = ansList
|
|
2002
|
+
.map((label) => {
|
|
2003
|
+
const desc = findOptionDesc(q, label);
|
|
2004
|
+
const descHtml = desc ? ` <span style="color:var(--text-muted)">— ${escapeHtml(desc)}</span>` : '';
|
|
2005
|
+
return `<li><span style="font-weight:600">${escapeHtml(String(label))}</span>${descHtml}</li>`;
|
|
2006
|
+
})
|
|
2007
|
+
.join('');
|
|
2008
|
+
return `<div style="margin-top:6px">
|
|
2009
|
+
<div style="font-size:0.75rem;color:var(--text-muted);margin-bottom:2px">${escapeHtml(q)}</div>
|
|
2010
|
+
<ul style="margin:2px 0 0 16px;padding:0">${items}</ul>
|
|
2011
|
+
</div>`;
|
|
2012
|
+
})
|
|
2013
|
+
.join('');
|
|
2014
|
+
return `<div style="margin-top:10px;padding-top:8px;border-top:1px solid var(--border)">
|
|
2015
|
+
<div style="font-size:0.8rem;color:var(--text-muted);margin-bottom:4px">Answers</div>
|
|
2016
|
+
${rows}
|
|
2017
|
+
</div>`;
|
|
2018
|
+
}
|
|
2019
|
+
|
|
1893
2020
|
function renderToolParamsHtml(params) {
|
|
1894
2021
|
if (!params) return '';
|
|
1895
|
-
const BLOCK_KEYS = new Set(['old_string', 'new_string', 'content', 'plan']);
|
|
2022
|
+
const BLOCK_KEYS = new Set(['old_string', 'new_string', 'content', 'contentFull', 'plan']);
|
|
1896
2023
|
const badges = [],
|
|
1897
|
-
blocks = []
|
|
2024
|
+
blocks = [],
|
|
2025
|
+
jsonBlocks = [];
|
|
1898
2026
|
for (const [k, v] of Object.entries(params)) {
|
|
1899
2027
|
if (BLOCK_KEYS.has(k)) continue;
|
|
2028
|
+
// Skip sibling `<k>Full` entries — they're used as expand targets, not
|
|
2029
|
+
// rendered as their own field. Only treat it as a sibling when the
|
|
2030
|
+
// trimmed key holds a server-truncated string (ends with the truncation
|
|
2031
|
+
// marker), otherwise a real param that happens to end in "Full" would
|
|
2032
|
+
// disappear.
|
|
2033
|
+
if (k.endsWith('Full')) {
|
|
2034
|
+
const baseKey = k.slice(0, -4);
|
|
2035
|
+
const base = params[baseKey];
|
|
2036
|
+
if (typeof base === 'string' && base.endsWith('... (truncated)') && typeof v === 'string') {
|
|
2037
|
+
continue;
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
if (v !== null && typeof v === 'object') {
|
|
2041
|
+
let pretty;
|
|
2042
|
+
try {
|
|
2043
|
+
pretty = JSON.stringify(v, null, 2);
|
|
2044
|
+
} catch (_) {
|
|
2045
|
+
pretty = String(v);
|
|
2046
|
+
}
|
|
2047
|
+
if (pretty.length > CONTENT_TRUNCATE_MAX) {
|
|
2048
|
+
pretty = `${pretty.slice(0, CONTENT_TRUNCATE_MAX)}\n... (truncated)`;
|
|
2049
|
+
}
|
|
2050
|
+
jsonBlocks.push({ k, pretty });
|
|
2051
|
+
continue;
|
|
2052
|
+
}
|
|
1900
2053
|
const display = typeof v === 'boolean' ? (v ? 'yes' : 'no') : String(v);
|
|
1901
2054
|
if (display.length > 60) {
|
|
1902
|
-
|
|
2055
|
+
const fullKey = `${k}Full`;
|
|
2056
|
+
const full = typeof params[fullKey] === 'string' ? params[fullKey] : null;
|
|
2057
|
+
blocks.push({ k, display, full });
|
|
1903
2058
|
} else {
|
|
1904
2059
|
badges.push(
|
|
1905
2060
|
`<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>`,
|
|
@@ -1908,8 +2063,19 @@ function renderToolParamsHtml(params) {
|
|
|
1908
2063
|
}
|
|
1909
2064
|
let html = '';
|
|
1910
2065
|
if (badges.length) html += `<div style="margin-top:6px;display:flex;flex-wrap:wrap;gap:4px">${badges.join('')}</div>`;
|
|
1911
|
-
for (const { k, display } of blocks) {
|
|
1912
|
-
|
|
2066
|
+
for (const { k, display, full } of blocks) {
|
|
2067
|
+
let suffix = '';
|
|
2068
|
+
if (full && full.length > display.length) {
|
|
2069
|
+
const toggle = makeExpandToggle(escapeHtml(display), escapeHtml(full), { fontSize: '0.75rem' });
|
|
2070
|
+
suffix = ` ${toggle.btn}${toggle.full}`;
|
|
2071
|
+
}
|
|
2072
|
+
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>`;
|
|
2073
|
+
}
|
|
2074
|
+
for (const { k, pretty } of jsonBlocks) {
|
|
2075
|
+
html += `<div style="margin-top:8px;padding-top:6px;border-top:1px solid var(--border)">
|
|
2076
|
+
<div style="font-size:0.75rem;color:var(--text-muted);margin-bottom:2px">${escapeHtml(k)}</div>
|
|
2077
|
+
<pre class="${TINTED_PRE_CLASS}" style="max-height:300px;overflow:auto;font-size:0.75rem">${escapeHtml(pretty)}</pre>
|
|
2078
|
+
</div>`;
|
|
1913
2079
|
}
|
|
1914
2080
|
if (params.old_string || params.new_string) {
|
|
1915
2081
|
html += `<div style="margin-top:8px;padding-top:6px;border-top:1px solid var(--border)">`;
|
|
@@ -1924,14 +2090,18 @@ function renderToolParamsHtml(params) {
|
|
|
1924
2090
|
html += `</div>`;
|
|
1925
2091
|
}
|
|
1926
2092
|
if (params.content) {
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
2093
|
+
// params.contentFull is set by the server when the truncated `content`
|
|
2094
|
+
// ends with `... (truncated)`. Fall back to params.content otherwise so
|
|
2095
|
+
// small writes render as before.
|
|
2096
|
+
const fullContent = params.contentFull || params.content;
|
|
2097
|
+
const isTruncated = !!params.contentFull || params.content.length > CONTENT_TRUNCATE_MAX;
|
|
2098
|
+
const truncContent = isTruncated
|
|
2099
|
+
? `${params.content.slice(0, CONTENT_TRUNCATE_MAX)}${params.content.length > CONTENT_TRUNCATE_MAX ? '\n... (truncated)' : ''}`
|
|
1930
2100
|
: params.content;
|
|
1931
2101
|
let writeMoreBtn = '',
|
|
1932
2102
|
fullBlock = '';
|
|
1933
|
-
if (
|
|
1934
|
-
const toggle = makeExpandToggle(escapeHtml(truncContent), escapeHtml(
|
|
2103
|
+
if (isTruncated) {
|
|
2104
|
+
const toggle = makeExpandToggle(escapeHtml(truncContent), escapeHtml(fullContent), {
|
|
1935
2105
|
fontSize: '0.75rem',
|
|
1936
2106
|
maxHeight: '500px',
|
|
1937
2107
|
tinted: true,
|
|
@@ -2109,6 +2279,16 @@ async function postAndToast(url, body, label) {
|
|
|
2109
2279
|
async function openMsgInEditor() {
|
|
2110
2280
|
const m = getDetailMsg();
|
|
2111
2281
|
if (!m) return;
|
|
2282
|
+
// Write/Edit tool calls record the source path — open that directly instead
|
|
2283
|
+
// of dumping the rendered modal body into a temp buffer.
|
|
2284
|
+
const filePath =
|
|
2285
|
+
m.type === 'tool_use' && (m.tool === 'Write' || m.tool === 'Edit')
|
|
2286
|
+
? m.params?.file_path || m.fullDetail || null
|
|
2287
|
+
: null;
|
|
2288
|
+
if (filePath) {
|
|
2289
|
+
postAndToast('/api/open-in-editor', { file: filePath }, 'in editor');
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
2112
2292
|
const title = m.type === 'tool_use' ? m.tool : m.compactSummary ? 'compact-summary' : m.type;
|
|
2113
2293
|
postAndToast('/api/open-in-editor', { content: getMessageDisplayContent(m), title }, 'in editor');
|
|
2114
2294
|
}
|
|
@@ -2221,7 +2401,8 @@ function renderAgentFooter() {
|
|
|
2221
2401
|
: '';
|
|
2222
2402
|
const agentColor = resolveNamedColor(a.color);
|
|
2223
2403
|
const colorStyle = agentColor ? ` style="border-left:3px solid ${agentColor.color}"` : '';
|
|
2224
|
-
|
|
2404
|
+
const selectedClass = a.agentId === currentAgentModalId ? ' selected' : '';
|
|
2405
|
+
return `<div class="agent-card${selectedClass}" data-agent-id="${escapeHtml(a.agentId)}"${colorStyle} onclick="showAgentModal('${a.agentId}')">
|
|
2225
2406
|
<div class="agent-type-row">${typeNs ? `<span class="agent-type-ns">${escapeHtml(typeNs)}</span>` : ''}<span class="agent-type-name">${escapeHtml(typeName)}</span>${nameBadgeHtml}</div>
|
|
2226
2407
|
<div class="agent-status-row"><span class="agent-dot ${a.status}"></span><span class="agent-status">${statusText}</span></div>
|
|
2227
2408
|
${msgHtml}
|
|
@@ -2313,6 +2494,7 @@ function showAgentModal(agentId) {
|
|
|
2313
2494
|
const agent = findAgentById(agentId);
|
|
2314
2495
|
if (!agent) return;
|
|
2315
2496
|
currentAgentModalId = agentId;
|
|
2497
|
+
highlightSelectedAgent();
|
|
2316
2498
|
const modal = document.getElementById('agent-modal');
|
|
2317
2499
|
const title = document.getElementById('agent-modal-title');
|
|
2318
2500
|
const body = document.getElementById('agent-modal-body');
|
|
@@ -2382,6 +2564,16 @@ function showAgentModal(agentId) {
|
|
|
2382
2564
|
function closeAgentModal() {
|
|
2383
2565
|
resetModalFullscreen('agent-modal');
|
|
2384
2566
|
currentAgentModalId = null;
|
|
2567
|
+
highlightSelectedAgent();
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
function highlightSelectedAgent() {
|
|
2571
|
+
const content = document.getElementById('agent-footer-content');
|
|
2572
|
+
if (!content) return;
|
|
2573
|
+
for (const el of content.querySelectorAll('.agent-card.selected')) el.classList.remove('selected');
|
|
2574
|
+
if (currentAgentModalId == null) return;
|
|
2575
|
+
const el = content.querySelector(`.agent-card[data-agent-id="${currentAgentModalId}"]`);
|
|
2576
|
+
if (el) el.classList.add('selected');
|
|
2385
2577
|
}
|
|
2386
2578
|
|
|
2387
2579
|
//#endregion
|
|
@@ -2463,7 +2655,6 @@ function renderSessions() {
|
|
|
2463
2655
|
// project filter → search filter → ensure pinned/sticky sessions are always included
|
|
2464
2656
|
let filteredSessions = sessions;
|
|
2465
2657
|
if (sessionFilter === 'active') {
|
|
2466
|
-
const now = Date.now();
|
|
2467
2658
|
const activeSessionIds = new Set();
|
|
2468
2659
|
filteredSessions = filteredSessions.filter((s) => {
|
|
2469
2660
|
if (dismissedSessionIds.has(s.id)) return false;
|
|
@@ -2472,8 +2663,7 @@ function renderSessions() {
|
|
|
2472
2663
|
((!s.sharedTaskList && (s.pending > 0 || s.inProgress > 0)) ||
|
|
2473
2664
|
s.hasActiveAgents ||
|
|
2474
2665
|
s.hasWaitingForUser ||
|
|
2475
|
-
s.hasRecentLog
|
|
2476
|
-
(s.hasPlan && !s.planImplementationSessionId && now - new Date(s.modifiedAt).getTime() <= ACTIVE_PLAN_MS));
|
|
2666
|
+
s.hasRecentLog);
|
|
2477
2667
|
if (isActive) activeSessionIds.add(s.id);
|
|
2478
2668
|
return isActive;
|
|
2479
2669
|
});
|
|
@@ -2613,11 +2803,12 @@ function renderSessions() {
|
|
|
2613
2803
|
${secondaryName ? `<div class="session-secondary">${escapeHtml(secondaryName)}</div>` : ''}
|
|
2614
2804
|
${gitBranch ? `<div class="session-branch">${gitBranch}</div>` : ''}
|
|
2615
2805
|
${session.planTitle ? `<div class="session-plan">${escapeHtml(session.planTitle)}</div>` : ''}
|
|
2806
|
+
${renderGoalSubtitle(session)}
|
|
2616
2807
|
<div class="session-progress">
|
|
2617
2808
|
<span class="session-indicators">
|
|
2618
2809
|
${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>` : ''}
|
|
2619
2810
|
${session.sharedTaskList ? `<span class="shared-tasklist-badge" title="Shared task list: ${escapeHtml(session.sharedTaskList)}">${linkSvg(12)}</span>` : ''}
|
|
2620
|
-
${isTeam || session.project || showCtx ? `<span class="team-info-btn" onclick="event.stopPropagation(); showSessionInfoModal('${session.id}')" title="View session info"
|
|
2811
|
+
${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>` : ''}
|
|
2621
2812
|
${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>` : ''}
|
|
2622
2813
|
${renderLoopBadge(session)}
|
|
2623
2814
|
${linkedDocsCount > 0 ? `<span class="linked-docs-badge" onclick="event.stopPropagation(); showSessionInfoModal('${session.id}')" title="${linkedDocsCount} linked document${linkedDocsCount > 1 ? 's' : ''}">${linkSvg(10)}${linkedDocsCount}</span>` : ''}
|
|
@@ -2715,6 +2906,7 @@ function renderSessions() {
|
|
|
2715
2906
|
const folderName = projectPath.split(/[/\\]/).pop();
|
|
2716
2907
|
const isCollapsed = collapsedProjectGroups.has(projectPath);
|
|
2717
2908
|
const escapedPath = escapeHtml(projectPath);
|
|
2909
|
+
const activeCount = projectSessions.reduce((n, s) => n + (isSessionActive(s) ? 1 : 0), 0);
|
|
2718
2910
|
const breadcrumbParts = projectPath
|
|
2719
2911
|
.replace(/^\/home\/[^/]+/, '~')
|
|
2720
2912
|
.split(/[/\\]/)
|
|
@@ -2727,7 +2919,7 @@ function renderSessions() {
|
|
|
2727
2919
|
<div class="project-group-header${isCollapsed ? ' collapsed' : ''}" data-group-path="${escapedPath}">
|
|
2728
2920
|
<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>
|
|
2729
2921
|
<span class="group-name">${escapeHtml(folderName)}</span>
|
|
2730
|
-
<span class="group-count">${projectSessions.length}</span>
|
|
2922
|
+
<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>
|
|
2731
2923
|
<span class="project-view-btn" data-project-path="${escapedPath}" title="Open project view — combined tasks from all sessions">
|
|
2732
2924
|
<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>
|
|
2733
2925
|
</span>
|
|
@@ -2741,11 +2933,12 @@ function renderSessions() {
|
|
|
2741
2933
|
|
|
2742
2934
|
if (ungrouped.length > 0 && sortedGroups.length > 0) {
|
|
2743
2935
|
const isCollapsed = collapsedProjectGroups.has('__ungrouped__');
|
|
2936
|
+
const ungroupedActive = ungrouped.reduce((n, s) => n + (isSessionActive(s) ? 1 : 0), 0);
|
|
2744
2937
|
html += `
|
|
2745
2938
|
<div class="project-group-header${isCollapsed ? ' collapsed' : ''}" data-group-path="__ungrouped__">
|
|
2746
2939
|
<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>
|
|
2747
2940
|
<span class="group-name">Ungrouped</span>
|
|
2748
|
-
<span class="group-count">${ungrouped.length}</span>
|
|
2941
|
+
<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>
|
|
2749
2942
|
</div>
|
|
2750
2943
|
<div class="project-group-sessions${isCollapsed ? ' collapsed' : ''}">
|
|
2751
2944
|
${renderGroupSessions(ungrouped, '__pinned___ungrouped__')}
|
|
@@ -2935,7 +3128,7 @@ function renderKanban() {
|
|
|
2935
3128
|
document.querySelector(`.task-card[data-task-id="${selectedTaskId}"][data-session-id="${selectedSessionId}"]`) ||
|
|
2936
3129
|
document.querySelector(`.task-card[data-task-id="${selectedTaskId}"]`);
|
|
2937
3130
|
if (card) {
|
|
2938
|
-
if (focusZone === 'board') card.classList.add('selected');
|
|
3131
|
+
if (focusZone === 'board' && !taskHighlightDimmed) card.classList.add('selected');
|
|
2939
3132
|
} else {
|
|
2940
3133
|
selectedTaskId = null;
|
|
2941
3134
|
selectedSessionId = null;
|
|
@@ -3023,6 +3216,7 @@ function selectTask(taskId, sessionId) {
|
|
|
3023
3216
|
if (prev) prev.classList.remove('selected');
|
|
3024
3217
|
selectedTaskId = taskId;
|
|
3025
3218
|
selectedSessionId = sessionId;
|
|
3219
|
+
taskHighlightDimmed = false;
|
|
3026
3220
|
if (!taskId) return;
|
|
3027
3221
|
const card =
|
|
3028
3222
|
document.querySelector(`.task-card[data-task-id="${taskId}"][data-session-id="${sessionId}"]`) ||
|
|
@@ -3263,6 +3457,8 @@ function setFocusZone(zone) {
|
|
|
3263
3457
|
}
|
|
3264
3458
|
}
|
|
3265
3459
|
} else {
|
|
3460
|
+
// Focusing the board is an explicit nav gesture — restore the highlight.
|
|
3461
|
+
taskHighlightDimmed = false;
|
|
3266
3462
|
// Session changed while in sidebar — reset stale selection
|
|
3267
3463
|
if (selectedSessionId && selectedSessionId !== currentSessionId) {
|
|
3268
3464
|
selectedTaskId = null;
|
|
@@ -3565,6 +3761,10 @@ async function addNote(event, taskId, sessionId) {
|
|
|
3565
3761
|
function closeDetailPanel() {
|
|
3566
3762
|
detailPanel.classList.remove('visible');
|
|
3567
3763
|
document.getElementById('delete-task-btn').style.display = 'none';
|
|
3764
|
+
// Keep the task selected for keyboard nav, but drop its white highlight.
|
|
3765
|
+
taskHighlightDimmed = true;
|
|
3766
|
+
const sel = document.querySelector('.task-card.selected');
|
|
3767
|
+
if (sel) sel.classList.remove('selected');
|
|
3568
3768
|
}
|
|
3569
3769
|
|
|
3570
3770
|
let deleteTaskId = null;
|
|
@@ -5139,6 +5339,8 @@ function isWaitingFresh() {
|
|
|
5139
5339
|
function showWaitingDetail() {
|
|
5140
5340
|
if (!isWaitingFresh()) return;
|
|
5141
5341
|
currentMsgDetailIdx = MSG_DETAIL_WAITING_IDX;
|
|
5342
|
+
msgHighlightDimmed = false;
|
|
5343
|
+
highlightSelectedMsg();
|
|
5142
5344
|
const tool = currentWaiting.toolName || 'unknown';
|
|
5143
5345
|
const label = getWaitingLabel(currentWaiting.kind, tool);
|
|
5144
5346
|
const body = document.getElementById('msg-detail-body');
|
|
@@ -5778,9 +5980,9 @@ function showInfoModal(session, teamConfig, tasks, planContent, parentInfo) {
|
|
|
5778
5980
|
infoRows.push(['Team Config', teamConfig.configPath, { openPath: configDir, openFile: teamConfig.configPath }]);
|
|
5779
5981
|
}
|
|
5780
5982
|
const clickableStyle =
|
|
5781
|
-
|
|
5983
|
+
'font-family: var(--mono); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; color: var(--accent-text); text-decoration: underline; text-decoration-style: dotted; text-underline-offset: 3px;';
|
|
5782
5984
|
const plainStyle =
|
|
5783
|
-
|
|
5985
|
+
'font-family: var(--mono); font-size: 12px; color: var(--text-primary); user-select: all; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;';
|
|
5784
5986
|
html += `<div class="team-modal-meta info-grid">`;
|
|
5785
5987
|
infoRows.forEach(([label, value, opts]) => {
|
|
5786
5988
|
const copyVal = escapeHtml(value).replace(/"/g, '"');
|
|
@@ -5803,6 +6005,13 @@ function showInfoModal(session, teamConfig, tasks, planContent, parentInfo) {
|
|
|
5803
6005
|
});
|
|
5804
6006
|
html += `</div>`;
|
|
5805
6007
|
|
|
6008
|
+
if (session.goal?.condition) {
|
|
6009
|
+
html += `<div class="info-goal-card">
|
|
6010
|
+
<div class="info-goal-head"><span class="info-goal-icon">◎</span>Goal</div>
|
|
6011
|
+
<div class="info-goal-text">${escapeHtml(session.goal.condition)}</div>
|
|
6012
|
+
</div>`;
|
|
6013
|
+
}
|
|
6014
|
+
|
|
5806
6015
|
if (session.contextStatus) {
|
|
5807
6016
|
html += `<hr style="border: none; border-top: 1px solid var(--border); margin: 12px 0;">`;
|
|
5808
6017
|
html += renderContextDetail(session.contextStatus);
|
|
@@ -6044,6 +6253,16 @@ function renderLoopModalBody(data) {
|
|
|
6044
6253
|
body.innerHTML = section('Wakeups', wakeups, 'wakeup') + section('Cron jobs', crons, 'cron');
|
|
6045
6254
|
}
|
|
6046
6255
|
|
|
6256
|
+
function renderGoalSubtitle(session) {
|
|
6257
|
+
const g = session.goal;
|
|
6258
|
+
if (!g?.condition) return '';
|
|
6259
|
+
const short = g.condition.length > 70 ? `${g.condition.slice(0, 70)}…` : g.condition;
|
|
6260
|
+
// Only active (unmet) goals reach here — a met goal auto-clears. Clicking
|
|
6261
|
+
// opens the info modal (full text); stopPropagation so it doesn't also
|
|
6262
|
+
// trigger the card's fetchTasks.
|
|
6263
|
+
return `<div class="session-goal" onclick="event.stopPropagation(); showSessionInfoModal('${session.id}')" title="${escapeHtml(g.condition)}"><span class="session-goal-icon">◎</span>${escapeHtml(short)}</div>`;
|
|
6264
|
+
}
|
|
6265
|
+
|
|
6047
6266
|
function renderLoopBadge(session) {
|
|
6048
6267
|
const li = session.loopInfo;
|
|
6049
6268
|
const total = (li?.wakeupCount || 0) + (li?.cronCount || 0);
|
|
@@ -6181,13 +6400,14 @@ function showToolStatsModal(sessionId) {
|
|
|
6181
6400
|
}
|
|
6182
6401
|
|
|
6183
6402
|
function renderToolStatsBody(data) {
|
|
6184
|
-
const { totalCalls, uniqueTools, totalFailed, tools } = data;
|
|
6403
|
+
const { totalCalls, uniqueTools, totalFailed, totalRejected, tools } = data;
|
|
6185
6404
|
|
|
6186
6405
|
const summary = `
|
|
6187
6406
|
<div class="tool-stats-summary">
|
|
6188
6407
|
<div class="tool-stats-chip"><span class="tool-stats-chip-val">${totalCalls}</span><span class="tool-stats-chip-lbl">Total calls</span></div>
|
|
6189
6408
|
<div class="tool-stats-chip"><span class="tool-stats-chip-val">${uniqueTools}</span><span class="tool-stats-chip-lbl">Unique tools</span></div>
|
|
6190
6409
|
<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>
|
|
6410
|
+
<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>
|
|
6191
6411
|
</div>`;
|
|
6192
6412
|
|
|
6193
6413
|
if (!tools?.length) {
|
|
@@ -6212,6 +6432,7 @@ function renderToolStatsBody(data) {
|
|
|
6212
6432
|
<th onclick="toolStatsSortBy('count')">Calls${arrow('count')}</th>
|
|
6213
6433
|
<th onclick="toolStatsSortBy('success')">✓ Success${arrow('success')}</th>
|
|
6214
6434
|
<th onclick="toolStatsSortBy('failed')">✗ Failed${arrow('failed')}</th>
|
|
6435
|
+
<th onclick="toolStatsSortBy('rejected')">⊘ Rejected${arrow('rejected')}</th>
|
|
6215
6436
|
<th onclick="toolStatsSortBy('impact')" title="Share of total tool output by character count">Impact${arrow('impact')}</th>
|
|
6216
6437
|
</tr></thead>
|
|
6217
6438
|
<tbody>${sorted
|
|
@@ -6221,6 +6442,7 @@ function renderToolStatsBody(data) {
|
|
|
6221
6442
|
<td>${t.count}</td>
|
|
6222
6443
|
<td>${t.success > 0 ? `<span class="badge-success">${t.success}</span>` : '—'}</td>
|
|
6223
6444
|
<td>${t.failed > 0 ? `<span class="badge-failed">${t.failed}</span>` : '—'}</td>
|
|
6445
|
+
<td>${t.rejected > 0 ? `<span class="badge-rejected">${t.rejected}</span>` : '—'}</td>
|
|
6224
6446
|
<td class="impact-cell">${
|
|
6225
6447
|
t.impact != null
|
|
6226
6448
|
? `<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>`
|