claude-code-kanban 7.20.0 → 7.21.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.
@@ -24,6 +24,10 @@ function getParentVerdict(sessionId, meta) {
24
24
  return v.result;
25
25
  }
26
26
 
27
+ // No staleness check, so no file access: for the session list's fork badge, where a stale
28
+ // verdict costs a wrong icon until the next lookup.
29
+ const peekParentVerdict = (sessionId) => verdicts.get(sessionId)?.result || null;
30
+
27
31
  // self: the child's stat from the lookup. parentIno: null when no parent was found.
28
32
  function setParentVerdict(sessionId, meta, self, parentIno, result) {
29
33
  verdicts.set(sessionId, {
@@ -55,4 +59,4 @@ function importParentVerdicts(entries) {
55
59
  }
56
60
  }
57
61
 
58
- module.exports = { getParentVerdict, setParentVerdict, parentVerdictsDirty, exportParentVerdicts, importParentVerdicts };
62
+ module.exports = { getParentVerdict, setParentVerdict, peekParentVerdict, parentVerdictsDirty, exportParentVerdicts, importParentVerdicts };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-kanban",
3
- "version": "7.20.0",
3
+ "version": "7.21.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
  "type": "commonjs",
package/public/app.js CHANGED
@@ -1939,6 +1939,8 @@ const ICON_AGENT_ACTIVE =
1939
1939
  const ICON_TERMINAL =
1940
1940
  '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>';
1941
1941
  const TERMINAL_TITLE = 'Running in a terminal here';
1942
+ const ICON_FORK =
1943
+ '<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="18" r="3"/><circle cx="6" cy="6" r="3"/><circle cx="18" cy="6" r="3"/><path d="M18 9v2c0 .6-.4 1-1 1H7c-.6 0-1-.4-1-1V9"/><path d="M12 12v3"/></svg>';
1942
1944
  const ICON_DISPATCHED =
1943
1945
  '<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.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"/><path d="m21.854 2.147-10.94 10.939"/></svg>';
1944
1946
  const ICON_CHAT =
@@ -3982,16 +3984,15 @@ function renderSessions() {
3982
3984
  return out;
3983
3985
  };
3984
3986
 
3985
- const renderSessionCard = (session) => {
3986
- if (session.placeholder) return renderPlaceholderCard(session, childDepth.get(session.id));
3987
+ const renderSessionCard = (session, hostProject = null) => {
3988
+ if (session.placeholder) return renderPlaceholderCard(session, childDepth.get(session.id), hostProject);
3987
3989
  const total = session.taskCount;
3988
3990
  const pct = (n) => Math.round((n / total) * 100);
3989
3991
  const isActive = session.id === currentSessionId && viewMode === 'session';
3990
3992
  const isLive = isSessionLive(session);
3991
3993
  const sessionName = sessionDisplayName(session);
3992
- const projectHtml = renderProjectIdentity(session);
3994
+ const projectHtml = renderProjectIdentity(session, hostProject);
3993
3995
 
3994
- const gitBranch = session.gitBranch ? escapeHtml(session.gitBranch) : null;
3995
3996
  const isTeam = session.isTeam;
3996
3997
  const memberCount = session.memberCount || 0;
3997
3998
 
@@ -4025,8 +4026,7 @@ function renderSessions() {
4025
4026
  <button onclick="openSession('${sid}')" draggable="true" data-session-id="${escapeHtml(session.id)}" class="session-item ${isActive ? 'active' : ''} ${sessionBarClass(session)} ${tempClass} ${idleClass} ${justFinishedClass}${childDepthClass(childDepth.get(session.id))}">
4026
4027
  <span class="session-pin-btn${pinClass}" onclick="event.stopPropagation();toggleSessionPin('${sid}')" title="${pinTitle} session">${pinState === 'sticky' ? SESSION_STAR_SVG : SESSION_PIN_SVG}</span>
4027
4028
  <div class="session-name">${escapeHtml(sessionName)}</div>
4028
- ${projectHtml ? `<div class="session-secondary">${projectHtml}</div>` : ''}
4029
- ${gitBranch ? `<div class="session-branch">${gitBranch}</div>` : ''}
4029
+ ${renderSessionIdentity(projectHtml, session.gitBranch)}
4030
4030
  ${metricsHtml ? `<div class="session-metrics">${metricsHtml}</div>` : ''}
4031
4031
  <div class="session-footer">
4032
4032
  <div class="session-time">${formatDate(session.modifiedAt)}</div>
@@ -4042,6 +4042,7 @@ function renderSessions() {
4042
4042
  ${session.planSourceSessionId ? `<span class="plan-indicator" title="Implements plan — click to reveal plan session" onclick="event.stopPropagation(); revealPlanSession('${escAttrJs(session.planSourceSessionId)}')">${ICON_PLAN}</span>` : ''}
4043
4043
  ${session.sharedTaskList ? `<span class="shared-tasklist-badge" title="Shared task list: ${escapeHtml(session.sharedTaskList)}">${linkSvg(12)}</span>` : ''}
4044
4044
  ${session.dispatched ? `<span class="dispatched-badge"${session.dispatched.parent ? ` onclick="event.stopPropagation(); revealSession('${escAttrJs(session.dispatched.parent)}')"` : ''} title="${escapeHtml(dispatchedTitle(session.dispatched))}">${ICON_DISPATCHED}</span>` : ''}
4045
+ ${session.forkedFromSessionId ? `<span class="fork-badge" onclick="event.stopPropagation(); revealSession('${escAttrJs(session.forkedFromSessionId)}')" title="Forked from ${escapeHtml(session.forkedFromSessionId)}, click to reveal">${ICON_FORK}</span>` : ''}
4045
4046
  ${runningTerminals.has(session.id) ? `<span class="terminal-badge" onclick="event.stopPropagation(); showSessionTerminal('${escAttrJs(session.id)}')" title="${TERMINAL_TITLE}">${ICON_TERMINAL}</span>` : ''}
4046
4047
  ${isWaitingSession(session) ? `<span class="agent-badge agent-badge-waiting" title="Waiting for user">${ICON_AGENT_WAITING}</span>` : ''}
4047
4048
  ${session.hasRunningAgents && !isWaitingSession(session) ? `<span class="agent-badge agent-badge-active" title="Agents running">${ICON_AGENT_ACTIVE}</span>` : ''}
@@ -4072,7 +4073,7 @@ function renderSessions() {
4072
4073
  SECTION_OUTSIDE,
4073
4074
  `Outside current filters ${countHtml(outside.rows)}`,
4074
4075
  false,
4075
- collapsedProjectGroups.has(SECTION_OUTSIDE) ? '' : outside.rows.map(renderSessionCard).join(''),
4076
+ collapsedProjectGroups.has(SECTION_OUTSIDE) ? '' : outside.rows.map((s) => renderSessionCard(s)).join(''),
4076
4077
  )
4077
4078
  : '';
4078
4079
 
@@ -4134,10 +4135,11 @@ function renderSessions() {
4134
4135
  const groupPinned = store.getItem(GROUP_PINNED_KEY) !== 'false';
4135
4136
  const pinWeight = (s) => (isPlacedSticky(s.id) ? 2 : isInPinnedGroup(s) ? 1 : 0);
4136
4137
  const pinSort = (a, b) => pinWeight(b) - pinWeight(a);
4137
- const renderGroupSessions = (list, pinKey) => {
4138
- if (!groupPinned || pinnedSessionIds.size === 0) return nestChildren(list).map(renderSessionCard).join('');
4138
+ const renderGroupSessions = (list, pinKey, hostProject = null) => {
4139
+ const card = (s) => renderSessionCard(s, hostProject);
4140
+ if (!groupPinned || pinnedSessionIds.size === 0) return nestChildren(list).map(card).join('');
4139
4141
  const gPinned = nestChildren(list.filter(isInPinnedGroup));
4140
- if (gPinned.length === 0) return nestChildren(list).map(renderSessionCard).join('');
4142
+ if (gPinned.length === 0) return nestChildren(list).map(card).join('');
4141
4143
  const gUnpinned = nestChildren(list.filter((s) => !isInPinnedGroup(s)));
4142
4144
  const pinCollapsed = collapsedProjectGroups.has(pinKey);
4143
4145
  return (
@@ -4157,10 +4159,10 @@ function renderSessions() {
4157
4159
  '<div class="pinned-sub-items' +
4158
4160
  (pinCollapsed ? ' collapsed' : '') +
4159
4161
  '">' +
4160
- gPinned.map(renderSessionCard).join('') +
4162
+ gPinned.map(card).join('') +
4161
4163
  '</div>' +
4162
4164
  '</div>' +
4163
- gUnpinned.map(renderSessionCard).join('')
4165
+ gUnpinned.map(card).join('')
4164
4166
  );
4165
4167
  };
4166
4168
 
@@ -4192,7 +4194,7 @@ function renderSessions() {
4192
4194
  </div>
4193
4195
  <div class="project-group-breadcrumb${nestedCls}" data-full-path="${escapedPath}" title="Click to copy path">${breadcrumbHtml}</div>
4194
4196
  <div class="project-group-sessions${isCollapsed ? ' collapsed' : ''}${nestedCls}" data-project-path="${escapedPath}">
4195
- ${renderGroupSessions(projectSessions, pinKey(projectPath))}
4197
+ ${renderGroupSessions(projectSessions, pinKey(projectPath), projectPath)}
4196
4198
  </div>
4197
4199
  `;
4198
4200
  };
@@ -4255,7 +4257,7 @@ function renderSessions() {
4255
4257
  let body = '';
4256
4258
  if (flat) {
4257
4259
  body = nestChildren([...bucket].sort(byMemberOrder))
4258
- .map(renderSessionCard)
4260
+ .map((s) => renderSessionCard(s))
4259
4261
  .join('');
4260
4262
  } else {
4261
4263
  const loose = [];
@@ -4361,7 +4363,7 @@ function renderSessions() {
4361
4363
  </div>
4362
4364
  `;
4363
4365
  } else {
4364
- projectsHtml += ungrouped.map(renderSessionCard).join('');
4366
+ projectsHtml += ungrouped.map((s) => renderSessionCard(s)).join('');
4365
4367
  }
4366
4368
  }
4367
4369
 
@@ -4372,7 +4374,7 @@ function renderSessions() {
4372
4374
  } else {
4373
4375
  const sectioned = sessionGroups.length > 0;
4374
4376
  const restHtml =
4375
- sectioned && collapsedProjectGroups.has(SECTION_SESSIONS) ? '' : sgRest.map(renderSessionCard).join('');
4377
+ sectioned && collapsedProjectGroups.has(SECTION_SESSIONS) ? '' : sgRest.map((s) => renderSessionCard(s)).join('');
4376
4378
  const tail = sectioned ? sectionHtml(SECTION_SESSIONS, 'Sessions', true, restHtml, sgRest.length === 0) : restHtml;
4377
4379
  sessionsList.innerHTML = html + sgSectionHtml(true) + tail + outsideHtml;
4378
4380
  }
@@ -4444,7 +4446,7 @@ function renderSession() {
4444
4446
  const completed = currentTasks.filter((t) => t.status === 'completed').length;
4445
4447
  const percent = currentTasks.length > 0 ? Math.round((completed / currentTasks.length) * 100) : 0;
4446
4448
 
4447
- progressPercent.textContent = `${percent}%`;
4449
+ progressPercent.textContent = `${percent}% done`;
4448
4450
  progressBar.style.width = `${percent}%`;
4449
4451
  const hasInProgress = currentTasks.some((t) => t.status === 'in_progress');
4450
4452
  progressBar.classList.toggle('shimmer', hasInProgress && percent < 100);
@@ -4468,7 +4470,7 @@ function renderProjectView() {
4468
4470
  const completed = currentTasks.filter((t) => t.status === 'completed').length;
4469
4471
  const percent = currentTasks.length > 0 ? Math.round((completed / currentTasks.length) * 100) : 0;
4470
4472
 
4471
- progressPercent.textContent = `${percent}%`;
4473
+ progressPercent.textContent = `${percent}% done`;
4472
4474
  progressBar.style.width = `${percent}%`;
4473
4475
  const hasInProgress = currentTasks.some((t) => t.status === 'in_progress');
4474
4476
  progressBar.classList.toggle('shimmer', hasInProgress && percent < 100);
@@ -9447,7 +9449,7 @@ async function openPreviewByPath(filePath, base, onUnsupported, sessionId = prev
9447
9449
  try {
9448
9450
  const r = await api('/api/preview', { query: { path: filePath, base: base || null } });
9449
9451
  if (!r.ok) {
9450
- showToast('Preview file unavailable', 'error');
9452
+ showToast('Preview file unavailable', 'error', { label: 'Copy path', onClick: () => copyText(filePath, 'path') });
9451
9453
  return;
9452
9454
  }
9453
9455
  const data = await r.json();
@@ -10371,6 +10373,8 @@ const CONTEXT_COLORS = {
10371
10373
  red: 'var(--danger)',
10372
10374
  };
10373
10375
  const COST_THRESHOLDS = { green: 0.5, yellow: 2, orange: 5 };
10376
+ // Status colors as small text measure 3.7–3.9:1 on light surfaces; bars and dots keep the pure color.
10377
+ const textTone = (color) => `color-mix(in srgb, ${color} 70%, var(--text-primary))`;
10374
10378
  const MODEL_THRESHOLDS = [
10375
10379
  { match: /sonnet/i, yellow: 120000, orange: 200000, red: 300000 },
10376
10380
  { match: /haiku/i, yellow: 100000, orange: 130000, red: 150000 },
@@ -10477,7 +10481,7 @@ function renderContextBar(session) {
10477
10481
  ${renderMarkers(ctx.markers)}
10478
10482
  </div>
10479
10483
  <div class="context-bar-labels">
10480
- <span style="color:${color}">${Math.round(ctx.pct)}% (${used})</span>
10484
+ <span>ctx <span style="color:${textTone(color)}">${Math.round(ctx.pct)}% (${used})</span></span>
10481
10485
  <span>${Math.round(ctx.remaining)}% free</span>
10482
10486
  </div>
10483
10487
  </div>`;
@@ -10518,7 +10522,7 @@ function formatCacheLeft(expiresAt, expiredText) {
10518
10522
  if (left === 0) return { text: expiredText, color: 'var(--text-muted)' };
10519
10523
  const text = `${Math.floor(left / 60)}:${String(left % 60).padStart(2, '0')}`;
10520
10524
  const color = left < 120 ? CONTEXT_COLORS.red : left < 300 ? CONTEXT_COLORS.orange : CONTEXT_COLORS.green;
10521
- return { text, color };
10525
+ return { text, color: textTone(color) };
10522
10526
  }
10523
10527
 
10524
10528
  let cacheTimerInterval = null;
@@ -10587,7 +10591,7 @@ function renderContextDetail(session, { tokens = true } = {}) {
10587
10591
  </div>
10588
10592
  </div>
10589
10593
  <div class="detail-context-summary">
10590
- <span style="color:${color}">${Math.round(ctx.pct)}% used</span>
10594
+ <span style="color:${textTone(color)}">${Math.round(ctx.pct)}% used</span>
10591
10595
  <span${ctx.compacts ? ` title="Auto-compact window · model window ${escapeHtml(formatTokens(ctx.modelSize / 1000))}"` : ''}>${formatTokens(ctx.usedTokens / 1000)} / ${formatTokens(ctx.size / 1000)}</span>
10592
10596
  </div>`
10593
10597
  : '';
@@ -10599,11 +10603,11 @@ function renderContextDetail(session, { tokens = true } = {}) {
10599
10603
  <div class="detail-context-stats">
10600
10604
  ${usageRows}
10601
10605
  ${totalRows}
10602
- ${ctx ? `<div class="stat-item"><span class="stat-label">Cost</span><span class="stat-value" style="color:${getCostColor(cost.total_cost_usd)}">${formatCost(cost.total_cost_usd)}</span></div>` : ''}
10606
+ ${ctx ? `<div class="stat-item"><span class="stat-label">Cost</span><span class="stat-value" style="color:${textTone(getCostColor(cost.total_cost_usd))}">${formatCost(cost.total_cost_usd)}</span></div>` : ''}
10603
10607
  ${reply ? renderCacheTimer(reply) : ''}
10604
10608
  ${cost.total_duration_ms != null ? `<div class="stat-item"><span class="stat-label">Duration</span><span class="stat-value">${formatDuration(cost.total_duration_ms)}</span></div>` : ''}
10605
10609
  ${cost.total_api_duration_ms != null ? `<div class="stat-item"><span class="stat-label">API time</span><span class="stat-value">${formatDuration(cost.total_api_duration_ms)}</span></div>` : ''}
10606
- ${cost.total_lines_added != null ? `<div class="stat-item"><span class="stat-label">Lines</span><span class="stat-value"><span style="color:${CONTEXT_COLORS.green}">+${(cost.total_lines_added || 0).toLocaleString()}</span> / <span style="color:${CONTEXT_COLORS.red}">-${(cost.total_lines_removed || 0).toLocaleString()}</span></span></div>` : ''}
10610
+ ${cost.total_lines_added != null ? `<div class="stat-item"><span class="stat-label">Lines</span><span class="stat-value"><span style="color:${textTone(CONTEXT_COLORS.green)}">+${(cost.total_lines_added || 0).toLocaleString()}</span> / <span style="color:${textTone(CONTEXT_COLORS.red)}">-${(cost.total_lines_removed || 0).toLocaleString()}</span></span></div>` : ''}
10607
10611
  </div>
10608
10612
  </div>`;
10609
10613
  }
@@ -10876,8 +10880,8 @@ function showWaitingDetail() {
10876
10880
  // With actions on screen the form/buttons are the content — tuck the raw
10877
10881
  // JSON behind a toggle, but keep what the user is approving visible: for a
10878
10882
  // permission ask surface the command (or description) as a one-line summary
10883
+ let summaryHtml = '';
10879
10884
  if ((actionsHtml || footerHtml) && inputHtml) {
10880
- let summaryHtml = '';
10881
10885
  if (currentWaiting.kind === 'plan' && typeof params?.plan === 'string' && params.plan) {
10882
10886
  // A plan ask IS the plan — render it in full so it can be reviewed and
10883
10887
  // approved right here (follow mode included) instead of a JSON dump. The
@@ -10900,7 +10904,8 @@ function showWaitingDetail() {
10900
10904
  autoSizeModal(modal, body);
10901
10905
  setMsgDetailFollow(msgDetailFollowLatest);
10902
10906
  const overlay = document.getElementById('msg-detail-modal');
10903
- const meta = [formatDate(currentWaiting.timestamp), 'waiting'];
10907
+ // The permission card's header already shows how long the ask has waited.
10908
+ const meta = summaryHtml ? [] : [formatDate(currentWaiting.timestamp), 'waiting'];
10904
10909
  document.getElementById('msg-detail-meta').textContent = meta.join(' · ');
10905
10910
  currentPinDetailId = null;
10906
10911
  updateMsgDetailPinState();
@@ -10930,17 +10935,34 @@ function pathBasename(p) {
10930
10935
 
10931
10936
  // `session.worktree` is resolved server-side from the `.git` pointer file, so it holds for a
10932
10937
  // worktree parked beside its repo as well as one under <repo>/.claude/worktrees/.
10933
- function renderProjectIdentity(session) {
10938
+ // Under its own project group the card leaves the repo name out: the group header shows it.
10939
+ function renderProjectIdentity(session, hostProject) {
10934
10940
  if (!session.project) return null;
10941
+ const underHost = !!hostProject && sessionProjectKey(session) === hostProject;
10935
10942
  const wt = session.worktree;
10936
- if (!wt) return escapeHtml(pathBasename(session.project));
10943
+ if (!wt) {
10944
+ return underHost ? null : `<span class="session-project">${escapeHtml(pathBasename(session.project))}</span>`;
10945
+ }
10937
10946
  const repo = pathBasename(wt.repo);
10938
10947
  // Worktrees beside the repo are conventionally named `<repo>-<branch>`, and repeating the
10939
10948
  // repo inside a chip that already sits next to it wastes the whole width of the card.
10940
10949
  const name = wt.name.startsWith(repo) ? wt.name.slice(repo.length).replace(/^[-._]/, '') || wt.name : wt.name;
10941
- return `${escapeHtml(repo)}<span class="wt-chip" title="Worktree of ${escapeHtml(wt.repo)}">⑂ ${escapeHtml(name)}</span>`;
10950
+ const chip = `<span class="wt-chip" title="Worktree of ${escapeHtml(wt.repo)}">⑂ ${escapeHtml(name)}</span>`;
10951
+ return underHost ? chip : `<span class="session-project">${escapeHtml(repo)}</span>${chip}`;
10952
+ }
10953
+
10954
+ function renderSessionIdentity(projectHtml, gitBranch) {
10955
+ if (!projectHtml && !gitBranch) return '';
10956
+ const branch = gitBranch
10957
+ ? `<span class="session-branch" title="${escapeHtml(gitBranch)}">${escapeHtml(gitBranch)}</span>`
10958
+ : '';
10959
+ const sep = projectHtml && branch ? '<span class="agent-ident-sep">·</span>' : '';
10960
+ return `<div class="session-secondary">${projectHtml || ''}${sep}${branch}</div>`;
10942
10961
  }
10943
10962
 
10963
+ const SHORT_DATE = new Intl.DateTimeFormat(undefined, { month: 'short', day: 'numeric' });
10964
+ const SHORT_DATE_YEAR = new Intl.DateTimeFormat(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
10965
+
10944
10966
  function formatDate(dateStr) {
10945
10967
  const date = new Date(dateStr);
10946
10968
  const now = new Date();
@@ -10949,7 +10971,8 @@ function formatDate(dateStr) {
10949
10971
  if (diff < JUST_NOW_MS) return 'just now';
10950
10972
  if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
10951
10973
  if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
10952
- return date.toLocaleDateString();
10974
+ if (diff < 7 * 86400000) return `${Math.floor(diff / 86400000)}d ago`;
10975
+ return (date.getFullYear() === now.getFullYear() ? SHORT_DATE : SHORT_DATE_YEAR).format(date);
10953
10976
  }
10954
10977
 
10955
10978
  // Terminal output reaches the transcript raw. A CSI-only pattern left OSC
@@ -11275,8 +11298,9 @@ function loadMermaid() {
11275
11298
  }
11276
11299
 
11277
11300
  let mermaidQueue = Promise.resolve();
11301
+ let mermaidSeq = 0;
11278
11302
 
11279
- // mermaid.run is async, and two runs that overlap draw diagrams over each other.
11303
+ // mermaid.render is async, and two renders that overlap draw diagrams over each other.
11280
11304
  function queueMermaid(work) {
11281
11305
  mermaidQueue = mermaidQueue.then(work).catch((err) => console.warn(err.message));
11282
11306
  }
@@ -11303,10 +11327,15 @@ function initMermaidBlocks(container) {
11303
11327
  return;
11304
11328
  }
11305
11329
  queueMermaid(async () => {
11306
- // One block per run, so a bad diagram shows its own error and the others still render.
11330
+ // One block per render, so a bad diagram shows its own error and the others still render.
11331
+ // render, not run: run measures labels in place, and under a modal's CSS zoom the measured
11332
+ // widths are off, so node boxes clip their text.
11307
11333
  for (const b of stampPending()) {
11308
11334
  try {
11309
- await mermaid.run({ nodes: [b] });
11335
+ const { svg, bindFunctions } = await mermaid.render(`mermaid-${++mermaidSeq}`, b.textContent.trim());
11336
+ b.innerHTML = svg;
11337
+ b.setAttribute('data-processed', 'true');
11338
+ bindFunctions?.(b);
11310
11339
  } catch (err) {
11311
11340
  showMermaidError(b, err);
11312
11341
  }
@@ -12363,8 +12392,8 @@ function updateDismissBtnState() {
12363
12392
  const btn = document.getElementById('session-info-dismiss-btn');
12364
12393
  if (!btn || !_infoModalSessionId) return;
12365
12394
  const isDismissed = dismissedSessionIds.has(_infoModalSessionId);
12366
- btn.textContent = isDismissed ? 'Restore' : 'Dismiss';
12367
- btn.title = isDismissed ? 'Restore — show in active list again' : 'Dismiss — hide from active list';
12395
+ btn.textContent = isDismissed ? 'Show session' : 'Hide session';
12396
+ btn.title = isDismissed ? 'Show in the active list again' : 'Hide from the active list';
12368
12397
  }
12369
12398
 
12370
12399
  let _planSessionId = null;
@@ -17468,13 +17497,13 @@ function forgetPlaceholder(id) {
17468
17497
  return true;
17469
17498
  }
17470
17499
 
17471
- function renderPlaceholderCard(session, depth) {
17500
+ function renderPlaceholderCard(session, depth, hostProject) {
17472
17501
  const isActive = session.id === currentSessionId && viewMode === 'session';
17473
- const projectHtml = renderProjectIdentity(session);
17502
+ const projectHtml = renderProjectIdentity(session, hostProject);
17474
17503
  return `
17475
17504
  <button onclick="openSession('${escAttrJs(session.id)}')" data-session-id="${escapeHtml(session.id)}" class="session-item session-placeholder ${isActive ? 'active' : ''}${childDepthClass(depth)}" title="${escapeHtml(`${session.id} | ${session.project}`)}">
17476
17505
  <div class="session-name">${escapeHtml(session.name)}</div>
17477
- ${projectHtml ? `<div class="session-secondary">${projectHtml}</div>` : ''}
17506
+ ${renderSessionIdentity(projectHtml, null)}
17478
17507
  ${
17479
17508
  trustWaiting.has(session.id)
17480
17509
  ? `<div class="session-waiting"><span class="agent-badge agent-badge-waiting" title="Waiting for user">${ICON_AGENT_WAITING}</span>trust this folder?</div>`
@@ -18187,7 +18216,13 @@ function makeLimitCell(label, bucket) {
18187
18216
  function makeLimitSpan(rl) {
18188
18217
  const span = document.createElement('span');
18189
18218
  span.className = 'footer-limits';
18190
- span.append(makeLimitCell('5h', rl?.five_hour), document.createTextNode(' · '), makeLimitCell('7d', rl?.seven_day));
18219
+ span.title = 'Plan usage: 5-hour and 7-day limits, used % (time until reset)';
18220
+ span.append(
18221
+ 'Usage ',
18222
+ makeLimitCell('5h', rl?.five_hour),
18223
+ document.createTextNode(' · '),
18224
+ makeLimitCell('7d', rl?.seven_day),
18225
+ );
18191
18226
  return span;
18192
18227
  }
18193
18228
  function renderSidebarFooter(rateLimits) {
package/public/index.html CHANGED
@@ -243,7 +243,7 @@
243
243
  <div class="progress-bar">
244
244
  <div id="progress-bar" class="progress-fill" style="width: 0%"></div>
245
245
  </div>
246
- <span id="progress-percent" class="progress-text">0%</span>
246
+ <span id="progress-percent" class="progress-text">0% done</span>
247
247
  </div>
248
248
  <button id="terminal-toggle" class="icon-btn" onclick="event.shiftKey ? openTerminalManager() : toggleTerminal()" title="Show / hide terminal (Ctrl+`) · Focus (Alt+`) · End (Alt+Shift+`) · Shift+click or Ctrl+Shift+` for all terminals" aria-label="Toggle terminal" style="display: none;">
249
249
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -596,7 +596,7 @@
596
596
  </div>
597
597
  <div id="team-modal-body" class="modal-body modal-zoomable"></div>
598
598
  <div class="modal-footer">
599
- <button id="session-info-dismiss-btn" class="btn btn-secondary" onclick="toggleDismissSession(_infoModalSessionId); closeTeamModal()">Dismiss</button>
599
+ <button id="session-info-dismiss-btn" class="btn btn-secondary" onclick="toggleDismissSession(_infoModalSessionId); closeTeamModal()">Hide session</button>
600
600
  <button class="btn btn-secondary" onclick="showToolStatsModal(_infoModalSessionId)" title="Tool invocation statistics">Tool Stats</button>
601
601
  <button class="btn btn-secondary" onclick="closeTeamModal()">Close</button>
602
602
  </div>
@@ -727,7 +727,7 @@
727
727
  </div>
728
728
  <div id="agent-modal-body" class="modal-body modal-zoomable" style="overflow-y: auto; overflow-x: hidden; flex: 0 1 auto; min-height: 0;"></div>
729
729
  <div class="modal-footer">
730
- <button id="agent-modal-dismiss-btn" class="btn agent-dismiss-btn" style="display:none" onclick="dismissAgent(currentAgentModalId);closeAgentModal()">Dismiss</button>
730
+ <button id="agent-modal-dismiss-btn" class="btn btn-secondary" style="display:none" onclick="dismissAgent(currentAgentModalId);closeAgentModal()" title="Mark this agent as stopped">Mark stopped</button>
731
731
  <button class="btn btn-secondary" onclick="closeAgentModal()">Close</button>
732
732
  </div>
733
733
  </div>