claude-code-kanban 4.5.0 → 4.7.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 CHANGED
@@ -107,6 +107,15 @@ function parseJsonlLine(line) {
107
107
  };
108
108
  }
109
109
 
110
+ if (obj.type === 'queue-operation' && obj.operation === 'enqueue') {
111
+ return {
112
+ ...base,
113
+ role: 'user',
114
+ queued: true,
115
+ content: typeof obj.content === 'string' ? obj.content : null
116
+ };
117
+ }
118
+
110
119
  if (obj.type === 'progress') {
111
120
  return { ...base, cwd: obj.cwd || null, version: obj.version || null, slug: obj.slug || null };
112
121
  }
@@ -118,6 +127,25 @@ const TOOL_RESULT_MAX = 1500;
118
127
  const USER_TEXT_MAX = 500;
119
128
  const INTERRUPT_MARKER = '[Request interrupted by user]';
120
129
 
130
+ // Newer Claude Code no longer inlines pasted images as base64 blocks. It writes
131
+ // them to ~/.claude/image-cache/<sessionId>/<N>.<ext> and leaves only a text marker
132
+ // in the message — an inline "[Image #N]" placeholder and/or a standalone
133
+ // "[Image: source: ...\N.ext]" line. Pull out the N references (so the UI can serve
134
+ // the cached file) and strip the verbose source lines from the displayed text — the
135
+ // rendered preview replaces them; the short "[Image #N]" placeholders stay.
136
+ // Runs on the per-message scan hot path, so it skips the regexes entirely unless a
137
+ // marker is present. Returns { refs: [{ kind: 'cache', n }], text }.
138
+ function parseImageMarkers(text) {
139
+ const raw = text || '';
140
+ if (!raw.includes('[Image')) return { refs: [], text: raw.trim() };
141
+ const ns = new Set();
142
+ for (const m of raw.matchAll(/\[Image #(\d+)\]/g)) ns.add(Number(m[1]));
143
+ for (const m of raw.matchAll(/\[Image: source:[^\]]*?(\d+)\.[a-z0-9]+\]/gi)) ns.add(Number(m[1]));
144
+ const refs = [...ns].sort((a, b) => a - b).map((n) => ({ kind: 'cache', n }));
145
+ const cleaned = raw.replace(/\[Image: source:[^\]]*\]/gi, '').trim();
146
+ return { refs, text: cleaned };
147
+ }
148
+
121
149
  function pushUserMessage(messages, text, timestamp, sysLabel, extras) {
122
150
  if (sysLabel === '__skip__') return;
123
151
  const safeText = text || '';
@@ -133,6 +161,7 @@ function pushUserMessage(messages, text, timestamp, sysLabel, extras) {
133
161
  if (extras.uuid) msg.uuid = extras.uuid;
134
162
  if (extras.images && extras.images.length) msg.images = extras.images;
135
163
  if (extras.toolResultRefs && extras.toolResultRefs.length) msg.toolResultRefs = extras.toolResultRefs;
164
+ if (extras.queued) msg.queued = true;
136
165
  }
137
166
  messages.push(msg);
138
167
  }
@@ -413,13 +442,20 @@ function getSystemMessageLabel(text) {
413
442
  const statusMatch = text.match(/<status>([^<]+)<\/status>/);
414
443
  return statusMatch ? `Background task ${statusMatch[1]}` : 'Background task notification';
415
444
  }
416
- if (text.includes('<local-command-stdout>') && text.includes('Compacted')) return 'Compacted';
445
+ // The "Compacted (ctrl+o…)" stdout echo is redundant with the isCompactSummary
446
+ // chip (modern Claude Code stores the summary inline). Skipping it lets the one
447
+ // summary-bearing chip stand alone instead of trailing a bare marker that the
448
+ // resume prompt sorts away from, preventing collapse.
449
+ if (text.includes('<local-command-stdout>') && text.includes('Compacted')) return '__skip__';
417
450
  if (text.includes('<local-command-stdout>')) return 'Command output';
418
451
  if (text.includes('<local-command-caveat>')) return 'System notification';
419
452
  if (text.includes('.output completed') && text.includes('Background command')) return 'Background task completed';
420
453
  if (text.startsWith('This session is being continued from a previous conversation')) return '__skip__';
421
454
  if (text.includes('<command-name>/clear</command-name>')) return '__skip__';
422
- if (text.includes('<command-name>/compact</command-name>')) return 'Compacted';
455
+ // The /compact trigger record is redundant: the boundary stdout + isCompactSummary
456
+ // record already render as one expandable "Compacted" chip. Skip it so the chip
457
+ // collapses cleanly instead of leaving a separate marker where the user typed it.
458
+ if (text.includes('<command-name>/compact</command-name>')) return '__skip__';
423
459
  return null;
424
460
  }
425
461
 
@@ -612,6 +648,26 @@ function readRecentMessages(jsonlPath, limit = 10) {
612
648
  }
613
649
  }
614
650
  } else if (obj.type === 'user' && obj.message?.role === 'user' && !obj.isMeta) {
651
+ if (obj.isCompactSummary) {
652
+ // Surface the compaction summary as a single expandable "Compacted"
653
+ // chip (compactSummary), never a raw bubble. The boundary marker plus
654
+ // the /compact command/stdout records collapse into this one entry below.
655
+ const raw = typeof obj.message.content === 'string'
656
+ ? obj.message.content
657
+ : Array.isArray(obj.message.content)
658
+ ? obj.message.content.filter((b) => b.type === 'text').map((b) => b.text).join('\n')
659
+ : '';
660
+ const summary = raw
661
+ .replace(/^This session is being continued[^\n]*\n+(The summary below[^\n]*\n+)?/i, '')
662
+ .trim();
663
+ messages.push({
664
+ type: 'user',
665
+ systemLabel: 'Compacted',
666
+ compactSummary: summary || null,
667
+ timestamp: obj.timestamp
668
+ });
669
+ continue;
670
+ }
615
671
  if (typeof obj.message.content === 'string') {
616
672
  const t = obj.message.content;
617
673
  const tmMatch = t.match(/<teammate-message\s+([^>]*)>([\s\S]*?)<\/teammate-message>/);
@@ -669,6 +725,7 @@ function readRecentMessages(jsonlPath, limit = 10) {
669
725
  texts.push(block.text);
670
726
  } else if (block.type === 'image' && block.source && block.source.type === 'base64') {
671
727
  images.push({
728
+ kind: 'base64',
672
729
  blockIndex: idx,
673
730
  mediaType: block.source.media_type || 'image/png',
674
731
  dataLen: typeof block.source.data === 'string' ? block.source.data.length : 0
@@ -709,18 +766,31 @@ function readRecentMessages(jsonlPath, limit = 10) {
709
766
  }
710
767
  });
711
768
  const joined = texts.join('\n').trim();
712
- const hasText = joined && joined !== INTERRUPT_MARKER;
713
- const hasImages = images.length > 0;
769
+ // Prefer inline base64 blocks when present; otherwise fall back to
770
+ // file-cache references parsed from the text markers.
771
+ const { refs, text: displayText } = parseImageMarkers(joined);
772
+ const allImages = images.length ? images : refs;
773
+ const hasText = displayText && displayText !== INTERRUPT_MARKER;
774
+ const hasImages = allImages.length > 0;
714
775
  if (hasText || hasImages) {
715
776
  pushUserMessage(
716
777
  messages,
717
- joined,
778
+ displayText,
718
779
  obj.timestamp,
719
- getSystemMessageLabel(joined),
720
- { uuid: obj.uuid, images, toolResultRefs: hasText ? toolResultRefs : [] }
780
+ getSystemMessageLabel(displayText),
781
+ { uuid: obj.uuid, images: allImages, toolResultRefs: hasText ? toolResultRefs : [] }
721
782
  );
722
783
  }
723
784
  }
785
+ } else if (obj.type === 'queue-operation' && obj.operation === 'enqueue') {
786
+ // Queued messages are stored as top-level `content`, not under
787
+ // message.content, and never re-emitted as a type:'user' line.
788
+ // Surface them so the Session Log reflects what the user sent.
789
+ const qt = typeof obj.content === 'string' ? obj.content : '';
790
+ if (qt) {
791
+ const { refs, text } = parseImageMarkers(qt);
792
+ pushUserMessage(messages, text, obj.timestamp, null, { queued: true, images: refs });
793
+ }
724
794
  }
725
795
  } catch (e) { /* partial line */ }
726
796
  }
@@ -758,6 +828,12 @@ function readRecentMessages(jsonlPath, limit = 10) {
758
828
  messages.sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
759
829
  for (let i = messages.length - 1; i > 0; i--) {
760
830
  if (messages[i].systemLabel === 'Compacted' && messages[i - 1].systemLabel === 'Compacted') {
831
+ // Carry the summary body onto the surviving chip so collapsing the
832
+ // boundary + /compact command + stdout + isCompactSummary records into
833
+ // one entry never drops the expandable summary.
834
+ if (messages[i].compactSummary && !messages[i - 1].compactSummary) {
835
+ messages[i - 1].compactSummary = messages[i].compactSummary;
836
+ }
761
837
  messages.splice(i, 1);
762
838
  }
763
839
  }
@@ -821,6 +897,31 @@ function readUserImage(jsonlPath, msgUuid, blockIndex) {
821
897
  return null;
822
898
  }
823
899
 
900
+ const CACHED_IMAGE_MIME = {
901
+ png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp',
902
+ };
903
+
904
+ // Serve pasted image N from ~/.claude/image-cache/<sessionId>/. The cached file may
905
+ // be any image format, so resolve it by index rather than assuming .png. sessionId
906
+ // is validated to a bare id to prevent path traversal.
907
+ function readCachedImage(sessionId, n) {
908
+ const idx = Number(n);
909
+ if (!sessionId || !/^[A-Za-z0-9._-]+$/.test(sessionId)) return null;
910
+ if (!Number.isInteger(idx) || idx < 0) return null;
911
+ const dir = path.join(require('os').homedir(), '.claude', 'image-cache', sessionId);
912
+ try {
913
+ const match = readdirSync(dir).find((f) => {
914
+ const dot = f.lastIndexOf('.');
915
+ return dot > 0 && f.slice(0, dot) === String(idx) && CACHED_IMAGE_MIME[f.slice(dot + 1).toLowerCase()];
916
+ });
917
+ if (!match) return null;
918
+ const ext = match.slice(match.lastIndexOf('.') + 1).toLowerCase();
919
+ return { mediaType: CACHED_IMAGE_MIME[ext], buffer: readFileSync(path.join(dir, match)) };
920
+ } catch (_) {
921
+ return null;
922
+ }
923
+ }
924
+
824
925
  function readMessagesPage(jsonlPath, limit = 10, beforeTimestamp = null) {
825
926
  const fetchLimit = limit + 1;
826
927
  const applyFilter = beforeTimestamp
@@ -1237,6 +1338,7 @@ module.exports = {
1237
1338
  readMessagesPage,
1238
1339
  readFullToolResult,
1239
1340
  readUserImage,
1341
+ readCachedImage,
1240
1342
  updateLoopInfo,
1241
1343
  buildLoopInfoFromState,
1242
1344
  buildAgentProgressMap,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-kanban",
3
- "version": "4.5.0",
3
+ "version": "4.7.0",
4
4
  "description": "A web-based Kanban board for viewing Claude Code tasks with agent teams support",
5
5
  "main": "server.js",
6
6
  "bin": {
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;
@@ -509,6 +511,8 @@ function setActivityFilter(kind) {
509
511
  // waiting is a sub-state of active — couple them so one click covers all running sessions
510
512
  toggleActivityKind('active');
511
513
  toggleActivityKind('waiting');
514
+ // Expand sections that have active sessions so they're visible on click
515
+ if (activityFilter.has('active')) expandActiveGroups();
512
516
  } else {
513
517
  toggleActivityKind(kind);
514
518
  }
@@ -1029,7 +1033,7 @@ function renderToolItem(m, i, compact) {
1029
1033
  ? `onclick="showAgentModal('${escapeHtml(m.agentId)}')" ${combinedStyle}`
1030
1034
  : `onclick="msgDetailFollowLatest=false;showMsgDetail(${i})" ${combinedStyle}`;
1031
1035
  const pinBtn = renderMsgPinBtn(m, i);
1032
- return `<div class="msg-item msg-tool${compactClass}" ${itemClickAttr}>
1036
+ return `<div class="msg-item msg-tool${compactClass}" data-msg-idx="${i}" ${itemClickAttr}>
1033
1037
  ${getToolIcon(m.tool)}
1034
1038
  <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}
1035
1039
  </div>`;
@@ -1071,7 +1075,7 @@ function renderMessageList(messages) {
1071
1075
  continue;
1072
1076
  }
1073
1077
 
1074
- const clickable = `onclick="msgDetailFollowLatest=false;showMsgDetail(${i})" style="cursor:pointer"`;
1078
+ const clickable = `data-msg-idx="${i}" onclick="msgDetailFollowLatest=false;showMsgDetail(${i})" style="cursor:pointer"`;
1075
1079
  const pinBtn = renderMsgPinBtn(m, i);
1076
1080
  if (m.type === 'user') {
1077
1081
  if (m.systemLabel) {
@@ -1097,9 +1101,10 @@ function renderMessageList(messages) {
1097
1101
  if (displayText) textHtml = isCmd ? `<code>${escapeHtml(displayText)}</code>` : displayText;
1098
1102
  else if (chips.length) textHtml = '<em class="msg-text-muted">(attachment)</em>';
1099
1103
  else textHtml = '';
1100
- parts.push(`<div class="msg-item msg-user${isCmd ? ' msg-cmd' : ''}" ${clickable}>
1104
+ const queuedTag = m.queued ? '<span class="msg-queued-tag">queued</span>' : '';
1105
+ parts.push(`<div class="msg-item msg-user${isCmd ? ' msg-cmd' : ''}${m.queued ? ' msg-queued' : ''}" ${clickable}>
1101
1106
  ${MSG_ICON_USER}
1102
- <div class="msg-body"><div class="msg-text">${textHtml}${cmdArgsHtml}</div>${chipsHtml}<div class="msg-time">${formatDate(m.timestamp)}</div></div>${pinBtn}
1107
+ <div class="msg-body"><div class="msg-text">${textHtml}${cmdArgsHtml}</div>${chipsHtml}<div class="msg-time">${formatDate(m.timestamp)}${queuedTag}</div></div>${pinBtn}
1103
1108
  </div>`);
1104
1109
  }
1105
1110
  } else if (m.type === 'assistant') {
@@ -1255,6 +1260,7 @@ function renderMessages(messages) {
1255
1260
  ? `<div class="msg-limit-banner">Showing last ${MSG_MAX_LOADED} messages</div>`
1256
1261
  : '';
1257
1262
  container.innerHTML = limitBanner + msgsHtml + renderWaitingEntry();
1263
+ highlightSelectedMsg();
1258
1264
  if (!msgUserScrolledUp) container.scrollTop = container.scrollHeight;
1259
1265
  // Auto-load more if content doesn't overflow yet
1260
1266
  if (
@@ -1269,6 +1275,8 @@ function renderMessages(messages) {
1269
1275
 
1270
1276
  let currentMsgDetailIdx = null;
1271
1277
  let msgDetailFollowLatest = false;
1278
+ // Message stays tracked but its highlight is dimmed once the detail modal closes.
1279
+ let msgHighlightDimmed = false;
1272
1280
  const MSG_DETAIL_WAITING_IDX = -2;
1273
1281
  let currentPins = [];
1274
1282
  let pinnedCollapsed = false;
@@ -1287,6 +1295,8 @@ const MSG_ICON_TEAMMATE =
1287
1295
  '<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>';
1288
1296
  const MSG_ICON_IDLE =
1289
1297
  '<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>';
1298
+ const ICON_AGENT =
1299
+ '<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>';
1290
1300
  const ICON_TASK =
1291
1301
  '<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>';
1292
1302
  const ICON_WEB =
@@ -1309,7 +1319,7 @@ const TOOL_ICONS = {
1309
1319
  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>',
1310
1320
  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>',
1311
1321
  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>',
1312
- Agent: MSG_ICON_TEAMMATE,
1322
+ Agent: ICON_AGENT,
1313
1323
  SendMessage:
1314
1324
  '<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>',
1315
1325
  TaskCreate: ICON_TASK,
@@ -1646,10 +1656,20 @@ const linkSvg = (size) =>
1646
1656
  //#region MODALS
1647
1657
  function renderUserAttachments(m) {
1648
1658
  const parts = [];
1649
- if (m.images?.length && m.uuid && currentSessionId) {
1659
+ if (m.images?.length && currentSessionId) {
1660
+ const sid = encodeURIComponent(currentSessionId);
1650
1661
  const imgs = m.images
1651
1662
  .map((img) => {
1652
- const url = `/api/sessions/${encodeURIComponent(currentSessionId)}/user-image/${encodeURIComponent(m.uuid)}/${img.blockIndex}`;
1663
+ // Cache-kind images live on disk (image-cache/<sessionId>/<n>.png); base64
1664
+ // images are read from the JSONL block and need the message uuid.
1665
+ let url;
1666
+ if (img.kind === 'cache') {
1667
+ url = `/api/sessions/${sid}/cached-image/${img.n}`;
1668
+ } else if (m.uuid) {
1669
+ url = `/api/sessions/${sid}/user-image/${encodeURIComponent(m.uuid)}/${img.blockIndex}`;
1670
+ } else {
1671
+ return '';
1672
+ }
1653
1673
  return `<img src="${url}" loading="lazy" alt="user image" class="user-attach-image" />`;
1654
1674
  })
1655
1675
  .join('');
@@ -1678,10 +1698,22 @@ function renderUserAttachments(m) {
1678
1698
  return parts.join('');
1679
1699
  }
1680
1700
 
1701
+ // Highlight the message whose detail is open, mirroring task-card selection.
1702
+ function highlightSelectedMsg() {
1703
+ const container = document.getElementById('message-panel-content');
1704
+ if (!container) return;
1705
+ for (const el of container.querySelectorAll('.msg-item.selected')) el.classList.remove('selected');
1706
+ if (msgHighlightDimmed || currentMsgDetailIdx == null || currentMsgDetailIdx < 0) return;
1707
+ const el = container.querySelector(`.msg-item[data-msg-idx="${currentMsgDetailIdx}"]`);
1708
+ if (el) el.classList.add('selected');
1709
+ }
1710
+
1681
1711
  function showMsgDetail(idx) {
1682
1712
  currentMsgDetailIdx = idx;
1713
+ msgHighlightDimmed = false;
1683
1714
  const m = currentMessages[idx];
1684
1715
  if (!m) return;
1716
+ highlightSelectedMsg();
1685
1717
  const body = document.getElementById('msg-detail-body');
1686
1718
  if (m.type === 'tool_use') {
1687
1719
  document.getElementById('msg-detail-title').textContent = m.tool;
@@ -1785,6 +1817,12 @@ function showMsgDetail(idx) {
1785
1817
  function closeMsgDetailModal() {
1786
1818
  resetModalFullscreen('msg-detail-modal');
1787
1819
  msgDetailFollowLatest = false;
1820
+ // Drop the message highlight on close, mirroring task-card behavior.
1821
+ msgHighlightDimmed = true;
1822
+ const container = document.getElementById('message-panel-content');
1823
+ if (container) {
1824
+ for (const el of container.querySelectorAll('.msg-item.selected')) el.classList.remove('selected');
1825
+ }
1788
1826
  }
1789
1827
 
1790
1828
  function _setModalWidth(modal, slot, on, maxWidth, width) {
@@ -2365,7 +2403,8 @@ function renderAgentFooter() {
2365
2403
  : '';
2366
2404
  const agentColor = resolveNamedColor(a.color);
2367
2405
  const colorStyle = agentColor ? ` style="border-left:3px solid ${agentColor.color}"` : '';
2368
- return `<div class="agent-card"${colorStyle} onclick="showAgentModal('${a.agentId}')">
2406
+ const selectedClass = a.agentId === currentAgentModalId ? ' selected' : '';
2407
+ return `<div class="agent-card${selectedClass}" data-agent-id="${escapeHtml(a.agentId)}"${colorStyle} onclick="showAgentModal('${a.agentId}')">
2369
2408
  <div class="agent-type-row">${typeNs ? `<span class="agent-type-ns">${escapeHtml(typeNs)}</span>` : ''}<span class="agent-type-name">${escapeHtml(typeName)}</span>${nameBadgeHtml}</div>
2370
2409
  <div class="agent-status-row"><span class="agent-dot ${a.status}"></span><span class="agent-status">${statusText}</span></div>
2371
2410
  ${msgHtml}
@@ -2457,6 +2496,7 @@ function showAgentModal(agentId) {
2457
2496
  const agent = findAgentById(agentId);
2458
2497
  if (!agent) return;
2459
2498
  currentAgentModalId = agentId;
2499
+ highlightSelectedAgent();
2460
2500
  const modal = document.getElementById('agent-modal');
2461
2501
  const title = document.getElementById('agent-modal-title');
2462
2502
  const body = document.getElementById('agent-modal-body');
@@ -2526,6 +2566,16 @@ function showAgentModal(agentId) {
2526
2566
  function closeAgentModal() {
2527
2567
  resetModalFullscreen('agent-modal');
2528
2568
  currentAgentModalId = null;
2569
+ highlightSelectedAgent();
2570
+ }
2571
+
2572
+ function highlightSelectedAgent() {
2573
+ const content = document.getElementById('agent-footer-content');
2574
+ if (!content) return;
2575
+ for (const el of content.querySelectorAll('.agent-card.selected')) el.classList.remove('selected');
2576
+ if (currentAgentModalId == null) return;
2577
+ const el = content.querySelector(`.agent-card[data-agent-id="${currentAgentModalId}"]`);
2578
+ if (el) el.classList.add('selected');
2529
2579
  }
2530
2580
 
2531
2581
  //#endregion
@@ -2760,7 +2810,7 @@ function renderSessions() {
2760
2810
  <span class="session-indicators">
2761
2811
  ${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>` : ''}
2762
2812
  ${session.sharedTaskList ? `<span class="shared-tasklist-badge" title="Shared task list: ${escapeHtml(session.sharedTaskList)}">${linkSvg(12)}</span>` : ''}
2763
- ${isTeam || session.project || showCtx ? `<span class="team-info-btn" onclick="event.stopPropagation(); showSessionInfoModal('${session.id}')" title="View session info">ℹ</span>` : ''}
2813
+ ${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>` : ''}
2764
2814
  ${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>` : ''}
2765
2815
  ${renderLoopBadge(session)}
2766
2816
  ${linkedDocsCount > 0 ? `<span class="linked-docs-badge" onclick="event.stopPropagation(); showSessionInfoModal('${session.id}')" title="${linkedDocsCount} linked document${linkedDocsCount > 1 ? 's' : ''}">${linkSvg(10)}${linkedDocsCount}</span>` : ''}
@@ -2781,6 +2831,8 @@ function renderSessions() {
2781
2831
 
2782
2832
  // Group active sessions by project
2783
2833
  if (sessionFilter === 'active') {
2834
+ // Auto-expand a collapsed section when newly-active work lands in it (live refresh).
2835
+ expandActiveGroups({ onlyNew: true });
2784
2836
  const groups = new Map();
2785
2837
  const ungrouped = [];
2786
2838
  for (const session of filteredSessions) {
@@ -3080,7 +3132,7 @@ function renderKanban() {
3080
3132
  document.querySelector(`.task-card[data-task-id="${selectedTaskId}"][data-session-id="${selectedSessionId}"]`) ||
3081
3133
  document.querySelector(`.task-card[data-task-id="${selectedTaskId}"]`);
3082
3134
  if (card) {
3083
- if (focusZone === 'board') card.classList.add('selected');
3135
+ if (focusZone === 'board' && !taskHighlightDimmed) card.classList.add('selected');
3084
3136
  } else {
3085
3137
  selectedTaskId = null;
3086
3138
  selectedSessionId = null;
@@ -3164,10 +3216,10 @@ async function onColumnDrop(e) {
3164
3216
 
3165
3217
  //#region KEYBOARD_NAV
3166
3218
  function selectTask(taskId, sessionId) {
3167
- const prev = document.querySelector('.task-card.selected');
3168
- if (prev) prev.classList.remove('selected');
3219
+ clearTaskSelection();
3169
3220
  selectedTaskId = taskId;
3170
3221
  selectedSessionId = sessionId;
3222
+ taskHighlightDimmed = false;
3171
3223
  if (!taskId) return;
3172
3224
  const card =
3173
3225
  document.querySelector(`.task-card[data-task-id="${taskId}"][data-session-id="${sessionId}"]`) ||
@@ -3281,6 +3333,11 @@ function clearKbSelection() {
3281
3333
  if (prev) prev.classList.remove('kb-selected');
3282
3334
  }
3283
3335
 
3336
+ function clearTaskSelection() {
3337
+ const prev = document.querySelector('.task-card.selected');
3338
+ if (prev) prev.classList.remove('selected');
3339
+ }
3340
+
3284
3341
  function selectSessionByIndex(idx, items) {
3285
3342
  items = items || getNavigableItems();
3286
3343
  if (items.length === 0) return;
@@ -3319,11 +3376,37 @@ function setGroupCollapsed(header, collapsed) {
3319
3376
  header.classList.toggle('collapsed', collapsed);
3320
3377
  const container = getGroupSessionsContainer(header);
3321
3378
  if (container) container.classList.toggle('collapsed', collapsed);
3379
+ persistCollapsedGroups();
3380
+ }
3381
+
3382
+ function persistCollapsedGroups() {
3322
3383
  try {
3323
3384
  localStorage.setItem('collapsedGroups', JSON.stringify([...collapsedProjectGroups]));
3324
3385
  } catch (_) {}
3325
3386
  }
3326
3387
 
3388
+ let prevActiveSessionIds = null; // null until primed by the first onlyNew pass
3389
+
3390
+ // Expand collapsed project sections that contain active sessions; leave the rest collapsed.
3391
+ // onlyNew: expand only for sessions that became active since the last call, so a section the
3392
+ // user manually collapsed stays collapsed until NEW active work lands in it (live refresh path).
3393
+ // The first onlyNew pass only primes the tracking set — it respects saved collapse state on load.
3394
+ // onlyNew=false force-expands every section with active work (explicit chip click).
3395
+ function expandActiveGroups({ onlyNew = false } = {}) {
3396
+ const primed = prevActiveSessionIds !== null;
3397
+ const activeIds = new Set();
3398
+ let changed = false;
3399
+ for (const s of sessions) {
3400
+ if (!isSessionActive(s)) continue;
3401
+ activeIds.add(s.id);
3402
+ if (onlyNew && (!primed || prevActiveSessionIds.has(s.id))) continue;
3403
+ if (collapsedProjectGroups.delete(s.project || '__ungrouped__')) changed = true;
3404
+ }
3405
+ prevActiveSessionIds = activeIds;
3406
+ if (changed) persistCollapsedGroups();
3407
+ // renderSessions() re-renders headers/containers from the updated set
3408
+ }
3409
+
3327
3410
  function isGroupHeader(el) {
3328
3411
  return el.classList.contains('project-group-header') || el.classList.contains('pinned-sub-header');
3329
3412
  }
@@ -3385,8 +3468,7 @@ function setFocusZone(zone) {
3385
3468
  // Clear all zone visuals
3386
3469
  sidebar.classList.remove('sidebar-focused');
3387
3470
  clearKbSelection();
3388
- const selCard = document.querySelector('.task-card.selected');
3389
- if (selCard) selCard.classList.remove('selected');
3471
+ clearTaskSelection();
3390
3472
 
3391
3473
  focusZone = zone;
3392
3474
  if (zone === 'sidebar') {
@@ -3408,6 +3490,8 @@ function setFocusZone(zone) {
3408
3490
  }
3409
3491
  }
3410
3492
  } else {
3493
+ // Focusing the board is an explicit nav gesture — restore the highlight.
3494
+ taskHighlightDimmed = false;
3411
3495
  // Session changed while in sidebar — reset stale selection
3412
3496
  if (selectedSessionId && selectedSessionId !== currentSessionId) {
3413
3497
  selectedTaskId = null;
@@ -3710,6 +3794,8 @@ async function addNote(event, taskId, sessionId) {
3710
3794
  function closeDetailPanel() {
3711
3795
  detailPanel.classList.remove('visible');
3712
3796
  document.getElementById('delete-task-btn').style.display = 'none';
3797
+ // Keep the task selected AND highlighted after closing (no dim-off).
3798
+ taskHighlightDimmed = false;
3713
3799
  }
3714
3800
 
3715
3801
  let deleteTaskId = null;
@@ -4563,7 +4649,10 @@ document.addEventListener('keydown', (e) => {
4563
4649
  return;
4564
4650
  }
4565
4651
  if (e.key === 'Escape') {
4566
- setFocusZone('board');
4652
+ // Plain unfocus — drop the keyboard cursor without jumping into the board
4653
+ document.querySelector('.sidebar').classList.remove('sidebar-focused');
4654
+ clearKbSelection();
4655
+ focusZone = 'board';
4567
4656
  return;
4568
4657
  }
4569
4658
  }
@@ -4614,6 +4703,11 @@ document.addEventListener('keydown', (e) => {
4614
4703
  if (detailPanel.classList.contains('visible')) closeDetailPanel();
4615
4704
  else if (agentLogMode) exitAgentLogMode();
4616
4705
  else if (messagePanelOpen) toggleMessagePanel();
4706
+ else {
4707
+ // Nothing open — plain unfocus: drop the task-card selection highlight
4708
+ clearTaskSelection();
4709
+ selectedTaskId = null;
4710
+ }
4617
4711
  return;
4618
4712
  }
4619
4713
 
@@ -5284,6 +5378,8 @@ function isWaitingFresh() {
5284
5378
  function showWaitingDetail() {
5285
5379
  if (!isWaitingFresh()) return;
5286
5380
  currentMsgDetailIdx = MSG_DETAIL_WAITING_IDX;
5381
+ msgHighlightDimmed = false;
5382
+ highlightSelectedMsg();
5287
5383
  const tool = currentWaiting.toolName || 'unknown';
5288
5384
  const label = getWaitingLabel(currentWaiting.kind, tool);
5289
5385
  const body = document.getElementById('msg-detail-body');
@@ -5923,9 +6019,9 @@ function showInfoModal(session, teamConfig, tasks, planContent, parentInfo) {
5923
6019
  infoRows.push(['Team Config', teamConfig.configPath, { openPath: configDir, openFile: teamConfig.configPath }]);
5924
6020
  }
5925
6021
  const clickableStyle =
5926
- "font-family: 'IBM Plex Mono', monospace; 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;";
6022
+ '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;';
5927
6023
  const plainStyle =
5928
- "font-family: 'IBM Plex Mono', monospace; font-size: 12px; user-select: all; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;";
6024
+ 'font-family: var(--mono); font-size: 12px; color: var(--text-primary); user-select: all; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;';
5929
6025
  html += `<div class="team-modal-meta info-grid">`;
5930
6026
  infoRows.forEach(([label, value, opts]) => {
5931
6027
  const copyVal = escapeHtml(value).replace(/"/g, '&quot;');
package/public/style.css CHANGED
@@ -18,16 +18,22 @@
18
18
  --accent-text: #f0a070;
19
19
  --accent-dim: rgba(232, 111, 51, 0.22);
20
20
  --accent-glow: rgba(232, 111, 51, 0.55);
21
+ --gold: #d9b667;
21
22
  --success: #3ecf8e;
22
23
  --success-dim: rgba(62, 207, 142, 0.18);
23
24
  --warning: #f0b429;
24
25
  --warning-dim: rgba(240, 180, 41, 0.18);
26
+ --danger: #ef5350;
27
+ --danger-dim: rgba(239, 83, 80, 0.18);
25
28
  --team: #60a5fa;
26
29
  --team-dim: rgba(96, 165, 250, 0.18);
27
30
  --plan: #86a886;
28
31
  --plan-dim: rgba(134, 168, 134, 0.18);
29
32
  --mono: 'IBM Plex Mono', monospace;
30
33
  --serif: 'Playfair Display', serif;
34
+ /* aliases: many rules reference --font-mono/--font-serif; keep them on-brand */
35
+ --font-mono: var(--mono);
36
+ --font-serif: var(--serif);
31
37
  }
32
38
 
33
39
  * {
@@ -85,7 +91,7 @@ body::before {
85
91
  .sidebar {
86
92
  width: var(--sidebar-width, 300px);
87
93
  background: var(--bg-surface);
88
- border-right: 1px solid var(--border);
94
+ border-right: 1px solid color-mix(in srgb, var(--border) 35%, transparent);
89
95
  box-shadow: 1px 0 12px rgba(0, 0, 0, 0.04);
90
96
  display: flex;
91
97
  flex-direction: column;
@@ -448,22 +454,15 @@ body::before {
448
454
  padding: 12px 14px;
449
455
  margin-bottom: 4px;
450
456
  background: transparent;
451
- border: 1px solid transparent;
457
+ border: 1px solid var(--border);
452
458
  border-radius: 8px;
453
459
  text-align: left;
454
460
  cursor: pointer;
455
461
  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
456
- box-shadow:
457
- 0 1px 3px rgba(0, 0, 0, 0.12),
458
- 0 1px 2px rgba(0, 0, 0, 0.08);
459
- }
460
-
461
- .session-item:hover {
462
- background: var(--bg-hover);
463
462
  }
464
463
 
465
464
  .session-item.active {
466
- background: var(--bg-elevated);
465
+ background: var(--bg-deep);
467
466
  border-color: var(--accent);
468
467
  }
469
468
 
@@ -510,7 +509,7 @@ body::before {
510
509
  color: var(--text-secondary);
511
510
  margin-top: 2px;
512
511
  display: block;
513
- font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
512
+ font-family: var(--mono);
514
513
  white-space: nowrap;
515
514
  overflow: hidden;
516
515
  text-overflow: ellipsis;
@@ -656,10 +655,10 @@ body::before {
656
655
  gap: 8px;
657
656
  }
658
657
  .detail-context-stats .stat-label {
659
- color: var(--text-tertiary);
658
+ color: var(--text-secondary);
660
659
  }
661
660
  .detail-context-stats .stat-value {
662
- color: var(--text-secondary);
661
+ color: var(--text-primary);
663
662
  }
664
663
  .detail-context-stats .stat-divider {
665
664
  grid-column: 1 / -1;
@@ -811,7 +810,7 @@ body::before {
811
810
 
812
811
  /* #region HEADER */
813
812
  .view-header {
814
- padding: 16px 24px;
813
+ padding: 12px 24px;
815
814
  border-bottom: 1px solid var(--border);
816
815
  background: var(--bg-surface);
817
816
  display: flex;
@@ -821,9 +820,11 @@ body::before {
821
820
 
822
821
  .view-title {
823
822
  font-family: var(--serif);
824
- font-size: 26px;
823
+ font-size: 20px;
825
824
  font-weight: 400;
826
825
  letter-spacing: -0.03em;
826
+ line-height: 1.15;
827
+ text-wrap: balance;
827
828
  display: flex;
828
829
  align-items: center;
829
830
  gap: 12px;
@@ -1018,12 +1019,12 @@ body::before {
1018
1019
 
1019
1020
  /* #region TASK_CARD */
1020
1021
  .task-card {
1021
- padding: 16px;
1022
- padding-left: 18px;
1022
+ padding: 10px 12px;
1023
+ padding-left: 14px;
1023
1024
  background: var(--bg-surface);
1024
1025
  border: 1px solid var(--border);
1025
1026
  border-left: 2px solid var(--text-muted);
1026
- border-radius: 8px;
1027
+ border-radius: 6px;
1027
1028
  cursor: pointer;
1028
1029
  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
1029
1030
  }
@@ -1067,18 +1068,18 @@ body::before {
1067
1068
  .task-card.selected,
1068
1069
  .task-card.selected:hover {
1069
1070
  background: var(--bg-elevated);
1070
- border-color: var(--team);
1071
- border-left: 2px solid var(--team);
1071
+ border-color: var(--accent);
1072
+ border-left: 2px solid var(--accent);
1072
1073
  box-shadow:
1073
- 0 0 0 1px var(--team-dim),
1074
- 0 0 12px var(--team-dim);
1074
+ 0 0 0 1px var(--accent-dim),
1075
+ 0 0 12px var(--accent-dim);
1075
1076
  opacity: 1;
1076
1077
  }
1077
1078
 
1078
1079
  .task-id {
1079
1080
  font-size: 11px;
1080
1081
  color: var(--text-muted);
1081
- margin-bottom: 6px;
1082
+ margin-bottom: 4px;
1082
1083
  display: flex;
1083
1084
  align-items: center;
1084
1085
  gap: 8px;
@@ -1099,9 +1100,9 @@ body::before {
1099
1100
  }
1100
1101
 
1101
1102
  .task-title {
1102
- font-size: 14px;
1103
+ font-size: 13px;
1103
1104
  color: var(--text-primary);
1104
- line-height: 1.4;
1105
+ line-height: 1.35;
1105
1106
  }
1106
1107
 
1107
1108
  .task-card.completed .task-title {
@@ -1112,15 +1113,15 @@ body::before {
1112
1113
  .task-session {
1113
1114
  font-size: 12px;
1114
1115
  color: var(--accent);
1115
- margin-top: 6px;
1116
+ margin-top: 4px;
1116
1117
  }
1117
1118
 
1118
1119
  .task-active {
1119
1120
  display: flex;
1120
1121
  align-items: center;
1121
1122
  gap: 6px;
1122
- margin-top: 10px;
1123
- padding-top: 10px;
1123
+ margin-top: 8px;
1124
+ padding-top: 8px;
1124
1125
  border-top: 1px solid var(--border);
1125
1126
  font-size: 11px;
1126
1127
  font-weight: 500;
@@ -1147,7 +1148,7 @@ body::before {
1147
1148
  font-size: 12px;
1148
1149
  font-weight: 450;
1149
1150
  color: var(--text-tertiary);
1150
- margin-top: 8px;
1151
+ margin-top: 6px;
1151
1152
  display: -webkit-box;
1152
1153
  -webkit-line-clamp: 2;
1153
1154
  -webkit-box-orient: vertical;
@@ -1164,8 +1165,8 @@ body::before {
1164
1165
  width: var(--detail-panel-width, 540px);
1165
1166
  height: 100vh;
1166
1167
  background: var(--bg-surface);
1167
- border-left: 1px solid var(--border);
1168
- box-shadow: -8px 0 24px rgba(0, 0, 0, 0.15);
1168
+ border-left: 1px solid color-mix(in srgb, var(--border) 35%, transparent);
1169
+ box-shadow: -1px 0 12px rgba(0, 0, 0, 0.04);
1169
1170
  display: none;
1170
1171
  flex-direction: column;
1171
1172
  z-index: 100;
@@ -1441,13 +1442,10 @@ body::before {
1441
1442
  font-size: 12px;
1442
1443
  }
1443
1444
 
1444
- .detail-desc pre code.hljs {
1445
- padding: 12px;
1446
- border-radius: 6px;
1447
- }
1448
-
1445
+ .detail-desc pre code.hljs,
1449
1446
  .detail-desc pre code {
1450
- background: var(--bg-elevated);
1447
+ background: color-mix(in srgb, var(--text-muted) 8%, transparent);
1448
+ border: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
1451
1449
  padding: 12px;
1452
1450
  border-radius: 6px;
1453
1451
  display: block;
@@ -1455,9 +1453,10 @@ body::before {
1455
1453
  }
1456
1454
 
1457
1455
  .detail-desc code {
1458
- background: var(--bg-elevated);
1459
- padding: 2px 6px;
1460
- border-radius: 3px;
1456
+ background: color-mix(in srgb, var(--text-muted) 12%, transparent);
1457
+ border: 1px solid color-mix(in srgb, var(--border) 45%, transparent);
1458
+ padding: 1px 5px;
1459
+ border-radius: 4px;
1461
1460
  font-size: 0.9em;
1462
1461
  }
1463
1462
 
@@ -1588,18 +1587,19 @@ body::before {
1588
1587
  align-items: center;
1589
1588
  justify-content: center;
1590
1589
  font-size: 11px;
1591
- background: var(--bg-deep);
1590
+ background: transparent;
1592
1591
  border: 1px solid transparent;
1593
1592
  border-radius: 4px;
1594
- color: var(--team);
1593
+ color: var(--text-muted);
1595
1594
  cursor: pointer;
1596
1595
  flex-shrink: 0;
1597
1596
  transition: all 0.15s ease;
1598
1597
  }
1599
1598
 
1600
1599
  .team-info-btn:hover {
1601
- background: var(--team-dim);
1602
- border-color: var(--team);
1600
+ background: var(--bg-hover);
1601
+ border-color: var(--border);
1602
+ color: var(--text-secondary);
1603
1603
  }
1604
1604
 
1605
1605
  .plan-indicator {
@@ -1737,10 +1737,10 @@ body::before {
1737
1737
  }
1738
1738
 
1739
1739
  .loop-field-val code {
1740
- background: var(--bg);
1740
+ background: var(--bg-deep);
1741
1741
  padding: 2px 6px;
1742
1742
  border-radius: 4px;
1743
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1743
+ font-family: var(--mono);
1744
1744
  font-size: 12px;
1745
1745
  }
1746
1746
 
@@ -1830,8 +1830,8 @@ body::before {
1830
1830
  font-size: 11px;
1831
1831
  font-weight: 500;
1832
1832
  border-radius: 999px;
1833
- border: 1px solid var(--border-color, rgba(127, 127, 127, 0.25));
1834
- background: var(--bg-secondary, rgba(127, 127, 127, 0.08));
1833
+ border: 1px solid var(--border);
1834
+ background: var(--bg-surface);
1835
1835
  color: var(--text-secondary);
1836
1836
  white-space: nowrap;
1837
1837
  line-height: 1.4;
@@ -2002,9 +2002,9 @@ body::before {
2002
2002
  right: 0;
2003
2003
  width: var(--message-panel-width, 540px);
2004
2004
  height: 100vh;
2005
- background: var(--bg-surface);
2006
- border-left: 1px solid var(--border);
2007
- box-shadow: -8px 0 24px rgba(0, 0, 0, 0.15);
2005
+ background: var(--bg-deep);
2006
+ border-left: 1px solid color-mix(in srgb, var(--border) 35%, transparent);
2007
+ box-shadow: -1px 0 12px rgba(0, 0, 0, 0.04);
2008
2008
  display: none;
2009
2009
  flex-direction: column;
2010
2010
  z-index: 99;
@@ -2047,22 +2047,39 @@ body::before {
2047
2047
  padding: 8px 10px;
2048
2048
  border-radius: 6px;
2049
2049
  font-size: 12px;
2050
- line-height: 1.5;
2051
- background: var(--bg-elevated);
2052
- border: 1px solid var(--border);
2050
+ line-height: 1.4;
2051
+ background: var(--bg-surface);
2052
+ border: 1px solid color-mix(in srgb, var(--border) 65%, transparent);
2053
2053
  }
2054
2054
  .msg-item.msg-user {
2055
- border-left: 3px solid var(--text-muted);
2055
+ border-left: 3px solid var(--gold);
2056
+ }
2057
+ .msg-queued-tag {
2058
+ margin-left: 8px;
2059
+ padding: 0 5px;
2060
+ border-radius: 3px;
2061
+ font-size: 9px;
2062
+ letter-spacing: 0.06em;
2063
+ text-transform: uppercase;
2064
+ color: var(--gold);
2065
+ background: color-mix(in srgb, var(--gold) 14%, transparent);
2056
2066
  }
2057
2067
  .msg-item.msg-assistant {
2058
2068
  border-left: 3px solid var(--accent);
2059
2069
  }
2060
2070
  .msg-item.msg-tool {
2061
- border-left: 3px solid var(--team);
2071
+ border-left: 2px solid color-mix(in srgb, var(--text-muted) 45%, transparent);
2062
2072
  font-size: 11px;
2063
2073
  padding: 5px 10px;
2064
2074
  color: var(--text-secondary);
2065
2075
  }
2076
+ .msg-item.selected,
2077
+ .msg-item.selected:hover {
2078
+ background: var(--bg-elevated);
2079
+ box-shadow:
2080
+ 0 0 0 1px var(--accent),
2081
+ 0 0 12px var(--accent-dim);
2082
+ }
2066
2083
  .msg-icon {
2067
2084
  flex-shrink: 0;
2068
2085
  width: 16px;
@@ -2093,7 +2110,7 @@ body::before {
2093
2110
  display: inline-flex;
2094
2111
  align-items: center;
2095
2112
  justify-content: center;
2096
- background: var(--hover);
2113
+ background: var(--bg-hover);
2097
2114
  color: var(--text-secondary);
2098
2115
  font-size: 10px;
2099
2116
  font-weight: 600;
@@ -2213,7 +2230,7 @@ body::before {
2213
2230
  }
2214
2231
  .pinned-item-unpin:hover {
2215
2232
  opacity: 1;
2216
- color: var(--danger, #e55);
2233
+ color: var(--danger);
2217
2234
  }
2218
2235
  .msg-pin-btn.pinned svg,
2219
2236
  #msg-detail-pin-btn.active svg,
@@ -2313,7 +2330,7 @@ body::before {
2313
2330
  }
2314
2331
  .agent-tab-copy {
2315
2332
  margin-left: auto;
2316
- background: var(--surface-hover);
2333
+ background: var(--bg-hover);
2317
2334
  border: 1px solid var(--border);
2318
2335
  border-radius: 6px;
2319
2336
  padding: 4px 6px;
@@ -2431,8 +2448,9 @@ body::before {
2431
2448
  font-size: 0.85rem;
2432
2449
  }
2433
2450
  .msg-detail-pre-tinted {
2434
- background: rgba(127, 127, 127, 0.15);
2435
- border-radius: 4px;
2451
+ background: color-mix(in srgb, var(--text-muted) 8%, transparent);
2452
+ border: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
2453
+ border-radius: 6px;
2436
2454
  padding: 8px 10px;
2437
2455
  }
2438
2456
  .expand-toggle-btn {
@@ -2546,7 +2564,7 @@ body::before {
2546
2564
  .msg-waiting-discard:hover {
2547
2565
  /* biome-ignore lint/complexity/noImportantStyles: override parent hover opacity */
2548
2566
  opacity: 1 !important;
2549
- color: var(--danger, #e54d4d);
2567
+ color: var(--danger);
2550
2568
  }
2551
2569
  .msg-waiting .msg-text {
2552
2570
  font-weight: 500;
@@ -2666,7 +2684,7 @@ body::before {
2666
2684
  display: inline-block;
2667
2685
  padding: 2px 10px;
2668
2686
  border-radius: 4px;
2669
- background: var(--bg-secondary);
2687
+ background: var(--bg-surface);
2670
2688
  color: var(--text-secondary);
2671
2689
  font-size: 0.8rem;
2672
2690
  font-weight: 600;
@@ -2706,7 +2724,7 @@ body::before {
2706
2724
  left: 50%;
2707
2725
  transform: translateX(-50%);
2708
2726
  background: var(--accent);
2709
- color: var(--bg);
2727
+ color: var(--bg-deep);
2710
2728
  border: none;
2711
2729
  border-radius: 16px;
2712
2730
  padding: 6px 16px;
@@ -2722,7 +2740,7 @@ body::before {
2722
2740
  .msg-loading-more {
2723
2741
  text-align: center;
2724
2742
  padding: 8px;
2725
- color: var(--text-dim);
2743
+ color: var(--text-muted);
2726
2744
  font-size: 12px;
2727
2745
  }
2728
2746
  .msg-limit-banner {
@@ -2783,18 +2801,28 @@ body::before {
2783
2801
  flex-direction: column;
2784
2802
  gap: 3px;
2785
2803
  padding: 8px 12px;
2786
- background: var(--bg-elevated);
2787
- border: 1px solid var(--border);
2804
+ background: var(--bg-deep);
2805
+ border: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
2788
2806
  border-radius: 8px;
2789
2807
  white-space: nowrap;
2790
2808
  min-width: 0;
2791
2809
  overflow: hidden;
2792
- transition: opacity 0.3s;
2810
+ transition:
2811
+ opacity 0.3s,
2812
+ border-color 0.15s ease;
2793
2813
  cursor: pointer;
2794
2814
  position: relative;
2795
2815
  }
2796
2816
  .agent-card:hover {
2817
+ border-color: color-mix(in srgb, var(--accent) 60%, var(--border));
2818
+ }
2819
+ .agent-card.selected,
2820
+ .agent-card.selected:hover {
2821
+ background: var(--bg-elevated);
2797
2822
  border-color: var(--accent);
2823
+ box-shadow:
2824
+ 0 0 0 1px var(--accent-dim),
2825
+ 0 0 12px var(--accent-dim);
2798
2826
  }
2799
2827
  .agent-card.fading {
2800
2828
  opacity: 0.4;
@@ -2867,7 +2895,7 @@ body::before {
2867
2895
  .agent-dismiss-btn {
2868
2896
  font-size: 12px;
2869
2897
  padding: 4px 10px;
2870
- background: var(--bg-tertiary);
2898
+ background: var(--bg-elevated);
2871
2899
  color: var(--text-secondary);
2872
2900
  border: 1px solid var(--border);
2873
2901
  }
@@ -2983,10 +3011,10 @@ body::before {
2983
3011
  /* #region LIGHT_THEME */
2984
3012
  body.light {
2985
3013
  --bg-deep: #e8e6e3;
2986
- --bg-surface: #f4f3f1;
2987
- --bg-elevated: #dddbd8;
3014
+ --bg-surface: #efede9;
3015
+ --bg-elevated: #fbfaf9;
2988
3016
  --bg-hover: #d2d0cc;
2989
- --border: #a09b94;
3017
+ --border: #cfcbc4;
2990
3018
  --text-primary: #0a0a0a;
2991
3019
  --text-secondary: #444444;
2992
3020
  --text-tertiary: #666666;
@@ -2994,10 +3022,13 @@ body.light {
2994
3022
  --accent-text: #b85a20;
2995
3023
  --accent-dim: rgba(232, 111, 51, 0.18);
2996
3024
  --accent-glow: rgba(232, 111, 51, 0.5);
3025
+ --gold: #a8842f;
2997
3026
  --success: #1a8a5a;
2998
3027
  --success-dim: rgba(26, 138, 90, 0.15);
2999
3028
  --warning: #b07d0a;
3000
3029
  --warning-dim: rgba(176, 125, 10, 0.15);
3030
+ --danger: #c0392b;
3031
+ --danger-dim: rgba(192, 57, 43, 0.15);
3001
3032
  --plan: #5a7a5a;
3002
3033
  --plan-dim: rgba(90, 122, 90, 0.15);
3003
3034
  }
@@ -3126,7 +3157,9 @@ body.light .msg-assistant .msg-text {
3126
3157
  max-width: 860px;
3127
3158
  max-height: 90vh;
3128
3159
  padding: 24px;
3129
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
3160
+ box-shadow:
3161
+ 0 16px 44px rgba(0, 0, 0, 0.38),
3162
+ 0 4px 12px rgba(0, 0, 0, 0.2);
3130
3163
  overflow-y: auto;
3131
3164
  }
3132
3165
 
@@ -3209,7 +3242,7 @@ body.light .msg-assistant .msg-text {
3209
3242
  }
3210
3243
 
3211
3244
  .preview-fm .fm-v {
3212
- color: var(--text, inherit);
3245
+ color: var(--text-primary);
3213
3246
  word-break: break-word;
3214
3247
  white-space: pre-wrap;
3215
3248
  }
@@ -3397,13 +3430,14 @@ select.form-input option:checked {
3397
3430
  }
3398
3431
 
3399
3432
  .btn-secondary {
3400
- background: var(--bg-elevated);
3401
- color: var(--text-primary);
3433
+ background: transparent;
3434
+ color: var(--text-secondary);
3402
3435
  border: 1px solid var(--border);
3403
3436
  }
3404
3437
 
3405
3438
  .btn-secondary:hover {
3406
3439
  background: var(--bg-hover);
3440
+ color: var(--text-primary);
3407
3441
  border-color: var(--text-muted);
3408
3442
  }
3409
3443
 
@@ -3434,7 +3468,7 @@ select.form-input option:checked {
3434
3468
  display: flex;
3435
3469
  flex-direction: column;
3436
3470
  align-items: center;
3437
- background: var(--bg-secondary);
3471
+ background: var(--bg-surface);
3438
3472
  border: 1px solid var(--border);
3439
3473
  border-radius: 8px;
3440
3474
  padding: 10px 16px;
@@ -3449,7 +3483,7 @@ select.form-input option:checked {
3449
3483
  }
3450
3484
 
3451
3485
  .tool-stats-chip-val.failed {
3452
- color: var(--status-failed, #ef4444);
3486
+ color: var(--danger);
3453
3487
  }
3454
3488
 
3455
3489
  .tool-stats-chip-lbl {
@@ -3490,14 +3524,14 @@ select.form-input option:checked {
3490
3524
  }
3491
3525
 
3492
3526
  .tool-stats-table tbody tr:nth-child(even) {
3493
- background: var(--bg-secondary);
3527
+ background: var(--bg-surface);
3494
3528
  }
3495
3529
 
3496
3530
  .tool-stats-table tbody td {
3497
3531
  padding: 6px 10px;
3498
3532
  text-align: right;
3499
3533
  color: var(--text-primary);
3500
- border-bottom: 1px solid var(--border-subtle, var(--border));
3534
+ border-bottom: 1px solid var(--border);
3501
3535
  }
3502
3536
 
3503
3537
  .tool-stats-table tbody td.tool-name {
@@ -3551,7 +3585,7 @@ select.form-input option:checked {
3551
3585
  width: 48px;
3552
3586
  height: 6px;
3553
3587
  border-radius: 3px;
3554
- background: var(--bg-hover, var(--bg-secondary));
3588
+ background: var(--bg-hover);
3555
3589
  flex-shrink: 0;
3556
3590
  overflow: hidden;
3557
3591
  }
@@ -3584,11 +3618,11 @@ select.form-input option:checked {
3584
3618
  max-height: 50vh;
3585
3619
  resize: vertical;
3586
3620
  padding: 12px;
3587
- font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;
3621
+ font-family: var(--mono);
3588
3622
  font-size: 13px;
3589
3623
  line-height: 1.6;
3590
3624
  color: var(--text-primary);
3591
- background: var(--bg-primary);
3625
+ background: var(--bg-deep);
3592
3626
  border: 1px solid var(--border);
3593
3627
  border-radius: 8px;
3594
3628
  outline: none;
@@ -3842,20 +3876,23 @@ select.form-input option:checked {
3842
3876
  @media (prefers-color-scheme: light) {
3843
3877
  body:not(.dark-forced) {
3844
3878
  --bg-deep: #e8e6e3;
3845
- --bg-surface: #f4f3f1;
3846
- --bg-elevated: #dddbd8;
3879
+ --bg-surface: #efede9;
3880
+ --bg-elevated: #fbfaf9;
3847
3881
  --bg-hover: #d2d0cc;
3848
- --border: #a09b94;
3882
+ --border: #cfcbc4;
3849
3883
  --text-primary: #0a0a0a;
3850
3884
  --text-secondary: #444444;
3851
3885
  --text-tertiary: #666666;
3852
3886
  --text-muted: #888888;
3853
3887
  --accent-dim: rgba(232, 111, 51, 0.18);
3854
3888
  --accent-glow: rgba(232, 111, 51, 0.5);
3889
+ --gold: #a8842f;
3855
3890
  --success: #1a8a5a;
3856
3891
  --success-dim: rgba(26, 138, 90, 0.15);
3857
3892
  --warning: #b07d0a;
3858
3893
  --warning-dim: rgba(176, 125, 10, 0.15);
3894
+ --danger: #c0392b;
3895
+ --danger-dim: rgba(192, 57, 43, 0.15);
3859
3896
  --plan: #5a7a5a;
3860
3897
  --plan-dim: rgba(90, 122, 90, 0.15);
3861
3898
  }
@@ -4184,11 +4221,6 @@ pre.mermaid svg {
4184
4221
  width: 0;
4185
4222
  }
4186
4223
 
4187
- .sidebar-focused .session-item.active {
4188
- background: transparent;
4189
- border-color: var(--accent);
4190
- }
4191
-
4192
4224
  .sidebar-focused .session-item.active::before {
4193
4225
  width: 0;
4194
4226
  }
@@ -4197,8 +4229,8 @@ pre.mermaid svg {
4197
4229
  .session-item.kb-selected:hover,
4198
4230
  .session-item.active.kb-selected {
4199
4231
  background: var(--bg-elevated);
4200
- border-color: var(--team);
4201
- box-shadow: 0 0 0 1px var(--team-dim);
4232
+ border-color: var(--accent);
4233
+ box-shadow: 0 0 0 1px var(--accent-dim);
4202
4234
  }
4203
4235
 
4204
4236
  .session-item.kb-selected::before {
package/server.js CHANGED
@@ -22,6 +22,7 @@ const {
22
22
  extractModelFromTranscript,
23
23
  readFullToolResult,
24
24
  readUserImage,
25
+ readCachedImage,
25
26
  updateLoopInfo,
26
27
  buildLoopInfoFromState
27
28
  } = require('./lib/parsers');
@@ -1912,6 +1913,14 @@ app.get('/api/sessions/:sessionId/user-image/:msgUuid/:blockIndex', (req, res) =
1912
1913
  res.end(buf);
1913
1914
  });
1914
1915
 
1916
+ app.get('/api/sessions/:sessionId/cached-image/:n', (req, res) => {
1917
+ const img = readCachedImage(req.params.sessionId, req.params.n);
1918
+ if (!img) return res.status(404).end();
1919
+ res.setHeader('Content-Type', img.mediaType);
1920
+ res.setHeader('Cache-Control', 'no-store');
1921
+ res.end(img.buffer);
1922
+ });
1923
+
1915
1924
  app.get('/api/version', (req, res) => {
1916
1925
  const pkg = require('./package.json');
1917
1926
  res.json({ version: pkg.version });