claude-code-kanban 4.5.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 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
  }
@@ -669,6 +698,7 @@ function readRecentMessages(jsonlPath, limit = 10) {
669
698
  texts.push(block.text);
670
699
  } else if (block.type === 'image' && block.source && block.source.type === 'base64') {
671
700
  images.push({
701
+ kind: 'base64',
672
702
  blockIndex: idx,
673
703
  mediaType: block.source.media_type || 'image/png',
674
704
  dataLen: typeof block.source.data === 'string' ? block.source.data.length : 0
@@ -709,18 +739,31 @@ function readRecentMessages(jsonlPath, limit = 10) {
709
739
  }
710
740
  });
711
741
  const joined = texts.join('\n').trim();
712
- const hasText = joined && joined !== INTERRUPT_MARKER;
713
- const hasImages = images.length > 0;
742
+ // Prefer inline base64 blocks when present; otherwise fall back to
743
+ // file-cache references parsed from the text markers.
744
+ const { refs, text: displayText } = parseImageMarkers(joined);
745
+ const allImages = images.length ? images : refs;
746
+ const hasText = displayText && displayText !== INTERRUPT_MARKER;
747
+ const hasImages = allImages.length > 0;
714
748
  if (hasText || hasImages) {
715
749
  pushUserMessage(
716
750
  messages,
717
- joined,
751
+ displayText,
718
752
  obj.timestamp,
719
- getSystemMessageLabel(joined),
720
- { uuid: obj.uuid, images, toolResultRefs: hasText ? toolResultRefs : [] }
753
+ getSystemMessageLabel(displayText),
754
+ { uuid: obj.uuid, images: allImages, toolResultRefs: hasText ? toolResultRefs : [] }
721
755
  );
722
756
  }
723
757
  }
758
+ } else if (obj.type === 'queue-operation' && obj.operation === 'enqueue') {
759
+ // Queued messages are stored as top-level `content`, not under
760
+ // message.content, and never re-emitted as a type:'user' line.
761
+ // Surface them so the Session Log reflects what the user sent.
762
+ const qt = typeof obj.content === 'string' ? obj.content : '';
763
+ if (qt) {
764
+ const { refs, text } = parseImageMarkers(qt);
765
+ pushUserMessage(messages, text, obj.timestamp, null, { queued: true, images: refs });
766
+ }
724
767
  }
725
768
  } catch (e) { /* partial line */ }
726
769
  }
@@ -821,6 +864,31 @@ function readUserImage(jsonlPath, msgUuid, blockIndex) {
821
864
  return null;
822
865
  }
823
866
 
867
+ const CACHED_IMAGE_MIME = {
868
+ png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp',
869
+ };
870
+
871
+ // Serve pasted image N from ~/.claude/image-cache/<sessionId>/. The cached file may
872
+ // be any image format, so resolve it by index rather than assuming .png. sessionId
873
+ // is validated to a bare id to prevent path traversal.
874
+ function readCachedImage(sessionId, n) {
875
+ const idx = Number(n);
876
+ if (!sessionId || !/^[A-Za-z0-9._-]+$/.test(sessionId)) return null;
877
+ if (!Number.isInteger(idx) || idx < 0) return null;
878
+ const dir = path.join(require('os').homedir(), '.claude', 'image-cache', sessionId);
879
+ try {
880
+ const match = readdirSync(dir).find((f) => {
881
+ const dot = f.lastIndexOf('.');
882
+ return dot > 0 && f.slice(0, dot) === String(idx) && CACHED_IMAGE_MIME[f.slice(dot + 1).toLowerCase()];
883
+ });
884
+ if (!match) return null;
885
+ const ext = match.slice(match.lastIndexOf('.') + 1).toLowerCase();
886
+ return { mediaType: CACHED_IMAGE_MIME[ext], buffer: readFileSync(path.join(dir, match)) };
887
+ } catch (_) {
888
+ return null;
889
+ }
890
+ }
891
+
824
892
  function readMessagesPage(jsonlPath, limit = 10, beforeTimestamp = null) {
825
893
  const fetchLimit = limit + 1;
826
894
  const applyFilter = beforeTimestamp
@@ -1237,6 +1305,7 @@ module.exports = {
1237
1305
  readMessagesPage,
1238
1306
  readFullToolResult,
1239
1307
  readUserImage,
1308
+ readCachedImage,
1240
1309
  updateLoopInfo,
1241
1310
  buildLoopInfoFromState,
1242
1311
  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.6.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;
@@ -1029,7 +1031,7 @@ function renderToolItem(m, i, compact) {
1029
1031
  ? `onclick="showAgentModal('${escapeHtml(m.agentId)}')" ${combinedStyle}`
1030
1032
  : `onclick="msgDetailFollowLatest=false;showMsgDetail(${i})" ${combinedStyle}`;
1031
1033
  const pinBtn = renderMsgPinBtn(m, i);
1032
- return `<div class="msg-item msg-tool${compactClass}" ${itemClickAttr}>
1034
+ return `<div class="msg-item msg-tool${compactClass}" data-msg-idx="${i}" ${itemClickAttr}>
1033
1035
  ${getToolIcon(m.tool)}
1034
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}
1035
1037
  </div>`;
@@ -1071,7 +1073,7 @@ function renderMessageList(messages) {
1071
1073
  continue;
1072
1074
  }
1073
1075
 
1074
- 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"`;
1075
1077
  const pinBtn = renderMsgPinBtn(m, i);
1076
1078
  if (m.type === 'user') {
1077
1079
  if (m.systemLabel) {
@@ -1097,9 +1099,10 @@ function renderMessageList(messages) {
1097
1099
  if (displayText) textHtml = isCmd ? `<code>${escapeHtml(displayText)}</code>` : displayText;
1098
1100
  else if (chips.length) textHtml = '<em class="msg-text-muted">(attachment)</em>';
1099
1101
  else textHtml = '';
1100
- parts.push(`<div class="msg-item msg-user${isCmd ? ' msg-cmd' : ''}" ${clickable}>
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}>
1101
1104
  ${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}
1105
+ <div class="msg-body"><div class="msg-text">${textHtml}${cmdArgsHtml}</div>${chipsHtml}<div class="msg-time">${formatDate(m.timestamp)}${queuedTag}</div></div>${pinBtn}
1103
1106
  </div>`);
1104
1107
  }
1105
1108
  } else if (m.type === 'assistant') {
@@ -1255,6 +1258,7 @@ function renderMessages(messages) {
1255
1258
  ? `<div class="msg-limit-banner">Showing last ${MSG_MAX_LOADED} messages</div>`
1256
1259
  : '';
1257
1260
  container.innerHTML = limitBanner + msgsHtml + renderWaitingEntry();
1261
+ highlightSelectedMsg();
1258
1262
  if (!msgUserScrolledUp) container.scrollTop = container.scrollHeight;
1259
1263
  // Auto-load more if content doesn't overflow yet
1260
1264
  if (
@@ -1269,6 +1273,8 @@ function renderMessages(messages) {
1269
1273
 
1270
1274
  let currentMsgDetailIdx = null;
1271
1275
  let msgDetailFollowLatest = false;
1276
+ // Message stays tracked but its highlight is dimmed once the detail modal closes.
1277
+ let msgHighlightDimmed = false;
1272
1278
  const MSG_DETAIL_WAITING_IDX = -2;
1273
1279
  let currentPins = [];
1274
1280
  let pinnedCollapsed = false;
@@ -1287,6 +1293,8 @@ const MSG_ICON_TEAMMATE =
1287
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>';
1288
1294
  const MSG_ICON_IDLE =
1289
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>';
1290
1298
  const ICON_TASK =
1291
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>';
1292
1300
  const ICON_WEB =
@@ -1309,7 +1317,7 @@ const TOOL_ICONS = {
1309
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>',
1310
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>',
1311
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>',
1312
- Agent: MSG_ICON_TEAMMATE,
1320
+ Agent: ICON_AGENT,
1313
1321
  SendMessage:
1314
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>',
1315
1323
  TaskCreate: ICON_TASK,
@@ -1646,10 +1654,20 @@ const linkSvg = (size) =>
1646
1654
  //#region MODALS
1647
1655
  function renderUserAttachments(m) {
1648
1656
  const parts = [];
1649
- if (m.images?.length && m.uuid && currentSessionId) {
1657
+ if (m.images?.length && currentSessionId) {
1658
+ const sid = encodeURIComponent(currentSessionId);
1650
1659
  const imgs = m.images
1651
1660
  .map((img) => {
1652
- const url = `/api/sessions/${encodeURIComponent(currentSessionId)}/user-image/${encodeURIComponent(m.uuid)}/${img.blockIndex}`;
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
+ }
1653
1671
  return `<img src="${url}" loading="lazy" alt="user image" class="user-attach-image" />`;
1654
1672
  })
1655
1673
  .join('');
@@ -1678,10 +1696,22 @@ function renderUserAttachments(m) {
1678
1696
  return parts.join('');
1679
1697
  }
1680
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
+
1681
1709
  function showMsgDetail(idx) {
1682
1710
  currentMsgDetailIdx = idx;
1711
+ msgHighlightDimmed = false;
1683
1712
  const m = currentMessages[idx];
1684
1713
  if (!m) return;
1714
+ highlightSelectedMsg();
1685
1715
  const body = document.getElementById('msg-detail-body');
1686
1716
  if (m.type === 'tool_use') {
1687
1717
  document.getElementById('msg-detail-title').textContent = m.tool;
@@ -1785,6 +1815,12 @@ function showMsgDetail(idx) {
1785
1815
  function closeMsgDetailModal() {
1786
1816
  resetModalFullscreen('msg-detail-modal');
1787
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
+ }
1788
1824
  }
1789
1825
 
1790
1826
  function _setModalWidth(modal, slot, on, maxWidth, width) {
@@ -2365,7 +2401,8 @@ function renderAgentFooter() {
2365
2401
  : '';
2366
2402
  const agentColor = resolveNamedColor(a.color);
2367
2403
  const colorStyle = agentColor ? ` style="border-left:3px solid ${agentColor.color}"` : '';
2368
- return `<div class="agent-card"${colorStyle} onclick="showAgentModal('${a.agentId}')">
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}')">
2369
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>
2370
2407
  <div class="agent-status-row"><span class="agent-dot ${a.status}"></span><span class="agent-status">${statusText}</span></div>
2371
2408
  ${msgHtml}
@@ -2457,6 +2494,7 @@ function showAgentModal(agentId) {
2457
2494
  const agent = findAgentById(agentId);
2458
2495
  if (!agent) return;
2459
2496
  currentAgentModalId = agentId;
2497
+ highlightSelectedAgent();
2460
2498
  const modal = document.getElementById('agent-modal');
2461
2499
  const title = document.getElementById('agent-modal-title');
2462
2500
  const body = document.getElementById('agent-modal-body');
@@ -2526,6 +2564,16 @@ function showAgentModal(agentId) {
2526
2564
  function closeAgentModal() {
2527
2565
  resetModalFullscreen('agent-modal');
2528
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');
2529
2577
  }
2530
2578
 
2531
2579
  //#endregion
@@ -2760,7 +2808,7 @@ function renderSessions() {
2760
2808
  <span class="session-indicators">
2761
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>` : ''}
2762
2810
  ${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>` : ''}
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>` : ''}
2764
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>` : ''}
2765
2813
  ${renderLoopBadge(session)}
2766
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>` : ''}
@@ -3080,7 +3128,7 @@ function renderKanban() {
3080
3128
  document.querySelector(`.task-card[data-task-id="${selectedTaskId}"][data-session-id="${selectedSessionId}"]`) ||
3081
3129
  document.querySelector(`.task-card[data-task-id="${selectedTaskId}"]`);
3082
3130
  if (card) {
3083
- if (focusZone === 'board') card.classList.add('selected');
3131
+ if (focusZone === 'board' && !taskHighlightDimmed) card.classList.add('selected');
3084
3132
  } else {
3085
3133
  selectedTaskId = null;
3086
3134
  selectedSessionId = null;
@@ -3168,6 +3216,7 @@ function selectTask(taskId, sessionId) {
3168
3216
  if (prev) prev.classList.remove('selected');
3169
3217
  selectedTaskId = taskId;
3170
3218
  selectedSessionId = sessionId;
3219
+ taskHighlightDimmed = false;
3171
3220
  if (!taskId) return;
3172
3221
  const card =
3173
3222
  document.querySelector(`.task-card[data-task-id="${taskId}"][data-session-id="${sessionId}"]`) ||
@@ -3408,6 +3457,8 @@ function setFocusZone(zone) {
3408
3457
  }
3409
3458
  }
3410
3459
  } else {
3460
+ // Focusing the board is an explicit nav gesture — restore the highlight.
3461
+ taskHighlightDimmed = false;
3411
3462
  // Session changed while in sidebar — reset stale selection
3412
3463
  if (selectedSessionId && selectedSessionId !== currentSessionId) {
3413
3464
  selectedTaskId = null;
@@ -3710,6 +3761,10 @@ async function addNote(event, taskId, sessionId) {
3710
3761
  function closeDetailPanel() {
3711
3762
  detailPanel.classList.remove('visible');
3712
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');
3713
3768
  }
3714
3769
 
3715
3770
  let deleteTaskId = null;
@@ -5284,6 +5339,8 @@ function isWaitingFresh() {
5284
5339
  function showWaitingDetail() {
5285
5340
  if (!isWaitingFresh()) return;
5286
5341
  currentMsgDetailIdx = MSG_DETAIL_WAITING_IDX;
5342
+ msgHighlightDimmed = false;
5343
+ highlightSelectedMsg();
5287
5344
  const tool = currentWaiting.toolName || 'unknown';
5288
5345
  const label = getWaitingLabel(currentWaiting.kind, tool);
5289
5346
  const body = document.getElementById('msg-detail-body');
@@ -5923,9 +5980,9 @@ function showInfoModal(session, teamConfig, tasks, planContent, parentInfo) {
5923
5980
  infoRows.push(['Team Config', teamConfig.configPath, { openPath: configDir, openFile: teamConfig.configPath }]);
5924
5981
  }
5925
5982
  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;";
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;';
5927
5984
  const plainStyle =
5928
- "font-family: 'IBM Plex Mono', monospace; font-size: 12px; user-select: all; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;";
5985
+ 'font-family: var(--mono); font-size: 12px; color: var(--text-primary); user-select: all; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;';
5929
5986
  html += `<div class="team-modal-meta info-grid">`;
5930
5987
  infoRows.forEach(([label, value, opts]) => {
5931
5988
  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;
@@ -510,7 +516,7 @@ body::before {
510
516
  color: var(--text-secondary);
511
517
  margin-top: 2px;
512
518
  display: block;
513
- font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
519
+ font-family: var(--mono);
514
520
  white-space: nowrap;
515
521
  overflow: hidden;
516
522
  text-overflow: ellipsis;
@@ -656,10 +662,10 @@ body::before {
656
662
  gap: 8px;
657
663
  }
658
664
  .detail-context-stats .stat-label {
659
- color: var(--text-tertiary);
665
+ color: var(--text-secondary);
660
666
  }
661
667
  .detail-context-stats .stat-value {
662
- color: var(--text-secondary);
668
+ color: var(--text-primary);
663
669
  }
664
670
  .detail-context-stats .stat-divider {
665
671
  grid-column: 1 / -1;
@@ -811,7 +817,7 @@ body::before {
811
817
 
812
818
  /* #region HEADER */
813
819
  .view-header {
814
- padding: 16px 24px;
820
+ padding: 12px 24px;
815
821
  border-bottom: 1px solid var(--border);
816
822
  background: var(--bg-surface);
817
823
  display: flex;
@@ -821,9 +827,11 @@ body::before {
821
827
 
822
828
  .view-title {
823
829
  font-family: var(--serif);
824
- font-size: 26px;
830
+ font-size: 20px;
825
831
  font-weight: 400;
826
832
  letter-spacing: -0.03em;
833
+ line-height: 1.15;
834
+ text-wrap: balance;
827
835
  display: flex;
828
836
  align-items: center;
829
837
  gap: 12px;
@@ -1018,12 +1026,12 @@ body::before {
1018
1026
 
1019
1027
  /* #region TASK_CARD */
1020
1028
  .task-card {
1021
- padding: 16px;
1022
- padding-left: 18px;
1029
+ padding: 10px 12px;
1030
+ padding-left: 14px;
1023
1031
  background: var(--bg-surface);
1024
1032
  border: 1px solid var(--border);
1025
1033
  border-left: 2px solid var(--text-muted);
1026
- border-radius: 8px;
1034
+ border-radius: 6px;
1027
1035
  cursor: pointer;
1028
1036
  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
1029
1037
  }
@@ -1067,18 +1075,18 @@ body::before {
1067
1075
  .task-card.selected,
1068
1076
  .task-card.selected:hover {
1069
1077
  background: var(--bg-elevated);
1070
- border-color: var(--team);
1071
- border-left: 2px solid var(--team);
1078
+ border-color: var(--accent);
1079
+ border-left: 2px solid var(--accent);
1072
1080
  box-shadow:
1073
- 0 0 0 1px var(--team-dim),
1074
- 0 0 12px var(--team-dim);
1081
+ 0 0 0 1px var(--accent-dim),
1082
+ 0 0 12px var(--accent-dim);
1075
1083
  opacity: 1;
1076
1084
  }
1077
1085
 
1078
1086
  .task-id {
1079
1087
  font-size: 11px;
1080
1088
  color: var(--text-muted);
1081
- margin-bottom: 6px;
1089
+ margin-bottom: 4px;
1082
1090
  display: flex;
1083
1091
  align-items: center;
1084
1092
  gap: 8px;
@@ -1099,9 +1107,9 @@ body::before {
1099
1107
  }
1100
1108
 
1101
1109
  .task-title {
1102
- font-size: 14px;
1110
+ font-size: 13px;
1103
1111
  color: var(--text-primary);
1104
- line-height: 1.4;
1112
+ line-height: 1.35;
1105
1113
  }
1106
1114
 
1107
1115
  .task-card.completed .task-title {
@@ -1112,15 +1120,15 @@ body::before {
1112
1120
  .task-session {
1113
1121
  font-size: 12px;
1114
1122
  color: var(--accent);
1115
- margin-top: 6px;
1123
+ margin-top: 4px;
1116
1124
  }
1117
1125
 
1118
1126
  .task-active {
1119
1127
  display: flex;
1120
1128
  align-items: center;
1121
1129
  gap: 6px;
1122
- margin-top: 10px;
1123
- padding-top: 10px;
1130
+ margin-top: 8px;
1131
+ padding-top: 8px;
1124
1132
  border-top: 1px solid var(--border);
1125
1133
  font-size: 11px;
1126
1134
  font-weight: 500;
@@ -1147,7 +1155,7 @@ body::before {
1147
1155
  font-size: 12px;
1148
1156
  font-weight: 450;
1149
1157
  color: var(--text-tertiary);
1150
- margin-top: 8px;
1158
+ margin-top: 6px;
1151
1159
  display: -webkit-box;
1152
1160
  -webkit-line-clamp: 2;
1153
1161
  -webkit-box-orient: vertical;
@@ -1164,7 +1172,7 @@ body::before {
1164
1172
  width: var(--detail-panel-width, 540px);
1165
1173
  height: 100vh;
1166
1174
  background: var(--bg-surface);
1167
- border-left: 1px solid var(--border);
1175
+ border-left: 1px solid color-mix(in srgb, var(--border) 35%, transparent);
1168
1176
  box-shadow: -8px 0 24px rgba(0, 0, 0, 0.15);
1169
1177
  display: none;
1170
1178
  flex-direction: column;
@@ -1441,13 +1449,10 @@ body::before {
1441
1449
  font-size: 12px;
1442
1450
  }
1443
1451
 
1444
- .detail-desc pre code.hljs {
1445
- padding: 12px;
1446
- border-radius: 6px;
1447
- }
1448
-
1452
+ .detail-desc pre code.hljs,
1449
1453
  .detail-desc pre code {
1450
- background: var(--bg-elevated);
1454
+ background: color-mix(in srgb, var(--text-muted) 8%, transparent);
1455
+ border: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
1451
1456
  padding: 12px;
1452
1457
  border-radius: 6px;
1453
1458
  display: block;
@@ -1455,9 +1460,10 @@ body::before {
1455
1460
  }
1456
1461
 
1457
1462
  .detail-desc code {
1458
- background: var(--bg-elevated);
1459
- padding: 2px 6px;
1460
- border-radius: 3px;
1463
+ background: color-mix(in srgb, var(--text-muted) 12%, transparent);
1464
+ border: 1px solid color-mix(in srgb, var(--border) 45%, transparent);
1465
+ padding: 1px 5px;
1466
+ border-radius: 4px;
1461
1467
  font-size: 0.9em;
1462
1468
  }
1463
1469
 
@@ -1588,18 +1594,19 @@ body::before {
1588
1594
  align-items: center;
1589
1595
  justify-content: center;
1590
1596
  font-size: 11px;
1591
- background: var(--bg-deep);
1597
+ background: transparent;
1592
1598
  border: 1px solid transparent;
1593
1599
  border-radius: 4px;
1594
- color: var(--team);
1600
+ color: var(--text-muted);
1595
1601
  cursor: pointer;
1596
1602
  flex-shrink: 0;
1597
1603
  transition: all 0.15s ease;
1598
1604
  }
1599
1605
 
1600
1606
  .team-info-btn:hover {
1601
- background: var(--team-dim);
1602
- border-color: var(--team);
1607
+ background: var(--bg-hover);
1608
+ border-color: var(--border);
1609
+ color: var(--text-secondary);
1603
1610
  }
1604
1611
 
1605
1612
  .plan-indicator {
@@ -1737,10 +1744,10 @@ body::before {
1737
1744
  }
1738
1745
 
1739
1746
  .loop-field-val code {
1740
- background: var(--bg);
1747
+ background: var(--bg-deep);
1741
1748
  padding: 2px 6px;
1742
1749
  border-radius: 4px;
1743
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1750
+ font-family: var(--mono);
1744
1751
  font-size: 12px;
1745
1752
  }
1746
1753
 
@@ -1830,8 +1837,8 @@ body::before {
1830
1837
  font-size: 11px;
1831
1838
  font-weight: 500;
1832
1839
  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));
1840
+ border: 1px solid var(--border);
1841
+ background: var(--bg-surface);
1835
1842
  color: var(--text-secondary);
1836
1843
  white-space: nowrap;
1837
1844
  line-height: 1.4;
@@ -2002,8 +2009,8 @@ body::before {
2002
2009
  right: 0;
2003
2010
  width: var(--message-panel-width, 540px);
2004
2011
  height: 100vh;
2005
- background: var(--bg-surface);
2006
- border-left: 1px solid var(--border);
2012
+ background: var(--bg-deep);
2013
+ border-left: 1px solid color-mix(in srgb, var(--border) 35%, transparent);
2007
2014
  box-shadow: -8px 0 24px rgba(0, 0, 0, 0.15);
2008
2015
  display: none;
2009
2016
  flex-direction: column;
@@ -2047,22 +2054,39 @@ body::before {
2047
2054
  padding: 8px 10px;
2048
2055
  border-radius: 6px;
2049
2056
  font-size: 12px;
2050
- line-height: 1.5;
2051
- background: var(--bg-elevated);
2052
- border: 1px solid var(--border);
2057
+ line-height: 1.4;
2058
+ background: var(--bg-surface);
2059
+ border: 1px solid color-mix(in srgb, var(--border) 65%, transparent);
2053
2060
  }
2054
2061
  .msg-item.msg-user {
2055
- border-left: 3px solid var(--text-muted);
2062
+ border-left: 3px solid var(--gold);
2063
+ }
2064
+ .msg-queued-tag {
2065
+ margin-left: 8px;
2066
+ padding: 0 5px;
2067
+ border-radius: 3px;
2068
+ font-size: 9px;
2069
+ letter-spacing: 0.06em;
2070
+ text-transform: uppercase;
2071
+ color: var(--gold);
2072
+ background: color-mix(in srgb, var(--gold) 14%, transparent);
2056
2073
  }
2057
2074
  .msg-item.msg-assistant {
2058
2075
  border-left: 3px solid var(--accent);
2059
2076
  }
2060
2077
  .msg-item.msg-tool {
2061
- border-left: 3px solid var(--team);
2078
+ border-left: 2px solid color-mix(in srgb, var(--text-muted) 45%, transparent);
2062
2079
  font-size: 11px;
2063
2080
  padding: 5px 10px;
2064
2081
  color: var(--text-secondary);
2065
2082
  }
2083
+ .msg-item.selected,
2084
+ .msg-item.selected:hover {
2085
+ background: var(--bg-elevated);
2086
+ box-shadow:
2087
+ 0 0 0 1px var(--accent),
2088
+ 0 0 12px var(--accent-dim);
2089
+ }
2066
2090
  .msg-icon {
2067
2091
  flex-shrink: 0;
2068
2092
  width: 16px;
@@ -2093,7 +2117,7 @@ body::before {
2093
2117
  display: inline-flex;
2094
2118
  align-items: center;
2095
2119
  justify-content: center;
2096
- background: var(--hover);
2120
+ background: var(--bg-hover);
2097
2121
  color: var(--text-secondary);
2098
2122
  font-size: 10px;
2099
2123
  font-weight: 600;
@@ -2213,7 +2237,7 @@ body::before {
2213
2237
  }
2214
2238
  .pinned-item-unpin:hover {
2215
2239
  opacity: 1;
2216
- color: var(--danger, #e55);
2240
+ color: var(--danger);
2217
2241
  }
2218
2242
  .msg-pin-btn.pinned svg,
2219
2243
  #msg-detail-pin-btn.active svg,
@@ -2313,7 +2337,7 @@ body::before {
2313
2337
  }
2314
2338
  .agent-tab-copy {
2315
2339
  margin-left: auto;
2316
- background: var(--surface-hover);
2340
+ background: var(--bg-hover);
2317
2341
  border: 1px solid var(--border);
2318
2342
  border-radius: 6px;
2319
2343
  padding: 4px 6px;
@@ -2431,8 +2455,9 @@ body::before {
2431
2455
  font-size: 0.85rem;
2432
2456
  }
2433
2457
  .msg-detail-pre-tinted {
2434
- background: rgba(127, 127, 127, 0.15);
2435
- border-radius: 4px;
2458
+ background: color-mix(in srgb, var(--text-muted) 8%, transparent);
2459
+ border: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
2460
+ border-radius: 6px;
2436
2461
  padding: 8px 10px;
2437
2462
  }
2438
2463
  .expand-toggle-btn {
@@ -2546,7 +2571,7 @@ body::before {
2546
2571
  .msg-waiting-discard:hover {
2547
2572
  /* biome-ignore lint/complexity/noImportantStyles: override parent hover opacity */
2548
2573
  opacity: 1 !important;
2549
- color: var(--danger, #e54d4d);
2574
+ color: var(--danger);
2550
2575
  }
2551
2576
  .msg-waiting .msg-text {
2552
2577
  font-weight: 500;
@@ -2666,7 +2691,7 @@ body::before {
2666
2691
  display: inline-block;
2667
2692
  padding: 2px 10px;
2668
2693
  border-radius: 4px;
2669
- background: var(--bg-secondary);
2694
+ background: var(--bg-surface);
2670
2695
  color: var(--text-secondary);
2671
2696
  font-size: 0.8rem;
2672
2697
  font-weight: 600;
@@ -2706,7 +2731,7 @@ body::before {
2706
2731
  left: 50%;
2707
2732
  transform: translateX(-50%);
2708
2733
  background: var(--accent);
2709
- color: var(--bg);
2734
+ color: var(--bg-deep);
2710
2735
  border: none;
2711
2736
  border-radius: 16px;
2712
2737
  padding: 6px 16px;
@@ -2722,7 +2747,7 @@ body::before {
2722
2747
  .msg-loading-more {
2723
2748
  text-align: center;
2724
2749
  padding: 8px;
2725
- color: var(--text-dim);
2750
+ color: var(--text-muted);
2726
2751
  font-size: 12px;
2727
2752
  }
2728
2753
  .msg-limit-banner {
@@ -2783,18 +2808,28 @@ body::before {
2783
2808
  flex-direction: column;
2784
2809
  gap: 3px;
2785
2810
  padding: 8px 12px;
2786
- background: var(--bg-elevated);
2787
- border: 1px solid var(--border);
2811
+ background: var(--bg-deep);
2812
+ border: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
2788
2813
  border-radius: 8px;
2789
2814
  white-space: nowrap;
2790
2815
  min-width: 0;
2791
2816
  overflow: hidden;
2792
- transition: opacity 0.3s;
2817
+ transition:
2818
+ opacity 0.3s,
2819
+ border-color 0.15s ease;
2793
2820
  cursor: pointer;
2794
2821
  position: relative;
2795
2822
  }
2796
2823
  .agent-card:hover {
2824
+ border-color: color-mix(in srgb, var(--accent) 60%, var(--border));
2825
+ }
2826
+ .agent-card.selected,
2827
+ .agent-card.selected:hover {
2828
+ background: var(--bg-elevated);
2797
2829
  border-color: var(--accent);
2830
+ box-shadow:
2831
+ 0 0 0 1px var(--accent-dim),
2832
+ 0 0 12px var(--accent-dim);
2798
2833
  }
2799
2834
  .agent-card.fading {
2800
2835
  opacity: 0.4;
@@ -2867,7 +2902,7 @@ body::before {
2867
2902
  .agent-dismiss-btn {
2868
2903
  font-size: 12px;
2869
2904
  padding: 4px 10px;
2870
- background: var(--bg-tertiary);
2905
+ background: var(--bg-elevated);
2871
2906
  color: var(--text-secondary);
2872
2907
  border: 1px solid var(--border);
2873
2908
  }
@@ -2983,10 +3018,10 @@ body::before {
2983
3018
  /* #region LIGHT_THEME */
2984
3019
  body.light {
2985
3020
  --bg-deep: #e8e6e3;
2986
- --bg-surface: #f4f3f1;
2987
- --bg-elevated: #dddbd8;
3021
+ --bg-surface: #efede9;
3022
+ --bg-elevated: #fbfaf9;
2988
3023
  --bg-hover: #d2d0cc;
2989
- --border: #a09b94;
3024
+ --border: #cfcbc4;
2990
3025
  --text-primary: #0a0a0a;
2991
3026
  --text-secondary: #444444;
2992
3027
  --text-tertiary: #666666;
@@ -2994,10 +3029,13 @@ body.light {
2994
3029
  --accent-text: #b85a20;
2995
3030
  --accent-dim: rgba(232, 111, 51, 0.18);
2996
3031
  --accent-glow: rgba(232, 111, 51, 0.5);
3032
+ --gold: #a8842f;
2997
3033
  --success: #1a8a5a;
2998
3034
  --success-dim: rgba(26, 138, 90, 0.15);
2999
3035
  --warning: #b07d0a;
3000
3036
  --warning-dim: rgba(176, 125, 10, 0.15);
3037
+ --danger: #c0392b;
3038
+ --danger-dim: rgba(192, 57, 43, 0.15);
3001
3039
  --plan: #5a7a5a;
3002
3040
  --plan-dim: rgba(90, 122, 90, 0.15);
3003
3041
  }
@@ -3126,7 +3164,9 @@ body.light .msg-assistant .msg-text {
3126
3164
  max-width: 860px;
3127
3165
  max-height: 90vh;
3128
3166
  padding: 24px;
3129
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
3167
+ box-shadow:
3168
+ 0 16px 44px rgba(0, 0, 0, 0.38),
3169
+ 0 4px 12px rgba(0, 0, 0, 0.2);
3130
3170
  overflow-y: auto;
3131
3171
  }
3132
3172
 
@@ -3209,7 +3249,7 @@ body.light .msg-assistant .msg-text {
3209
3249
  }
3210
3250
 
3211
3251
  .preview-fm .fm-v {
3212
- color: var(--text, inherit);
3252
+ color: var(--text-primary);
3213
3253
  word-break: break-word;
3214
3254
  white-space: pre-wrap;
3215
3255
  }
@@ -3397,13 +3437,14 @@ select.form-input option:checked {
3397
3437
  }
3398
3438
 
3399
3439
  .btn-secondary {
3400
- background: var(--bg-elevated);
3401
- color: var(--text-primary);
3440
+ background: transparent;
3441
+ color: var(--text-secondary);
3402
3442
  border: 1px solid var(--border);
3403
3443
  }
3404
3444
 
3405
3445
  .btn-secondary:hover {
3406
3446
  background: var(--bg-hover);
3447
+ color: var(--text-primary);
3407
3448
  border-color: var(--text-muted);
3408
3449
  }
3409
3450
 
@@ -3434,7 +3475,7 @@ select.form-input option:checked {
3434
3475
  display: flex;
3435
3476
  flex-direction: column;
3436
3477
  align-items: center;
3437
- background: var(--bg-secondary);
3478
+ background: var(--bg-surface);
3438
3479
  border: 1px solid var(--border);
3439
3480
  border-radius: 8px;
3440
3481
  padding: 10px 16px;
@@ -3449,7 +3490,7 @@ select.form-input option:checked {
3449
3490
  }
3450
3491
 
3451
3492
  .tool-stats-chip-val.failed {
3452
- color: var(--status-failed, #ef4444);
3493
+ color: var(--danger);
3453
3494
  }
3454
3495
 
3455
3496
  .tool-stats-chip-lbl {
@@ -3490,14 +3531,14 @@ select.form-input option:checked {
3490
3531
  }
3491
3532
 
3492
3533
  .tool-stats-table tbody tr:nth-child(even) {
3493
- background: var(--bg-secondary);
3534
+ background: var(--bg-surface);
3494
3535
  }
3495
3536
 
3496
3537
  .tool-stats-table tbody td {
3497
3538
  padding: 6px 10px;
3498
3539
  text-align: right;
3499
3540
  color: var(--text-primary);
3500
- border-bottom: 1px solid var(--border-subtle, var(--border));
3541
+ border-bottom: 1px solid var(--border);
3501
3542
  }
3502
3543
 
3503
3544
  .tool-stats-table tbody td.tool-name {
@@ -3551,7 +3592,7 @@ select.form-input option:checked {
3551
3592
  width: 48px;
3552
3593
  height: 6px;
3553
3594
  border-radius: 3px;
3554
- background: var(--bg-hover, var(--bg-secondary));
3595
+ background: var(--bg-hover);
3555
3596
  flex-shrink: 0;
3556
3597
  overflow: hidden;
3557
3598
  }
@@ -3584,11 +3625,11 @@ select.form-input option:checked {
3584
3625
  max-height: 50vh;
3585
3626
  resize: vertical;
3586
3627
  padding: 12px;
3587
- font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;
3628
+ font-family: var(--mono);
3588
3629
  font-size: 13px;
3589
3630
  line-height: 1.6;
3590
3631
  color: var(--text-primary);
3591
- background: var(--bg-primary);
3632
+ background: var(--bg-deep);
3592
3633
  border: 1px solid var(--border);
3593
3634
  border-radius: 8px;
3594
3635
  outline: none;
@@ -3842,20 +3883,23 @@ select.form-input option:checked {
3842
3883
  @media (prefers-color-scheme: light) {
3843
3884
  body:not(.dark-forced) {
3844
3885
  --bg-deep: #e8e6e3;
3845
- --bg-surface: #f4f3f1;
3846
- --bg-elevated: #dddbd8;
3886
+ --bg-surface: #efede9;
3887
+ --bg-elevated: #fbfaf9;
3847
3888
  --bg-hover: #d2d0cc;
3848
- --border: #a09b94;
3889
+ --border: #cfcbc4;
3849
3890
  --text-primary: #0a0a0a;
3850
3891
  --text-secondary: #444444;
3851
3892
  --text-tertiary: #666666;
3852
3893
  --text-muted: #888888;
3853
3894
  --accent-dim: rgba(232, 111, 51, 0.18);
3854
3895
  --accent-glow: rgba(232, 111, 51, 0.5);
3896
+ --gold: #a8842f;
3855
3897
  --success: #1a8a5a;
3856
3898
  --success-dim: rgba(26, 138, 90, 0.15);
3857
3899
  --warning: #b07d0a;
3858
3900
  --warning-dim: rgba(176, 125, 10, 0.15);
3901
+ --danger: #c0392b;
3902
+ --danger-dim: rgba(192, 57, 43, 0.15);
3859
3903
  --plan: #5a7a5a;
3860
3904
  --plan-dim: rgba(90, 122, 90, 0.15);
3861
3905
  }
@@ -4197,8 +4241,8 @@ pre.mermaid svg {
4197
4241
  .session-item.kb-selected:hover,
4198
4242
  .session-item.active.kb-selected {
4199
4243
  background: var(--bg-elevated);
4200
- border-color: var(--team);
4201
- box-shadow: 0 0 0 1px var(--team-dim);
4244
+ border-color: var(--accent);
4245
+ box-shadow: 0 0 0 1px var(--accent-dim);
4202
4246
  }
4203
4247
 
4204
4248
  .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 });