claude-code-kanban 7.18.0 → 7.19.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-kanban",
3
- "version": "7.18.0",
3
+ "version": "7.19.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
@@ -3433,24 +3433,34 @@ function agentIdentHtml(a) {
3433
3433
 
3434
3434
  // The count names every running agent; the rows stop at ZEN_AGENTS_MAX, and the Agents Log
3435
3435
  // below the board lists the rest.
3436
- function zenAgentsHtml(agents) {
3437
- const active = agents.filter((a) => a.status === 'active');
3436
+ function zenAgentsHtml(active) {
3438
3437
  if (!active.length) return '';
3439
- const now = Date.now();
3440
3438
  const rows = active.slice(0, ZEN_AGENTS_MAX).map((a) => {
3441
3439
  const task = truncateAtWord(agentTaskText(a), 200);
3442
3440
  return `<button type="button" class="zen-agent" title="${escapeHtml(task)}" onclick="showAgentModal('${escAttrJs(a.agentId)}')">
3443
3441
  <span class="agent-dot active"></span>
3444
3442
  <span class="zen-agent-text"><span class="zen-agent-task">${escapeHtml(task)}</span><span class="agent-ident-row">${agentIdentHtml(a)}</span></span>
3445
- <span class="zen-agent-time">${formatDuration(agentElapsed(a, now))}</span>
3443
+ <span class="zen-agent-time"></span>
3446
3444
  </button>`;
3447
3445
  });
3448
3446
  return `<div class="panel-section-header"><span>Agents</span><span class="panel-section-count">${active.length}</span></div>${rows.join('')}`;
3449
3447
  }
3450
3448
 
3449
+ // The 1 s duration tick calls this. Rewriting the rows each time would drop the focus of a row,
3450
+ // so the rows are rewritten only when they change and the times are set in place.
3451
3451
  function renderZenAgents(agents) {
3452
3452
  const el = zenMode && sessionsList.querySelector('.zen-agents');
3453
- if (el) el.innerHTML = zenAgentsHtml(agents);
3453
+ if (!el) return;
3454
+ const active = agents.filter((a) => a.status === 'active');
3455
+ const html = zenAgentsHtml(active);
3456
+ if (html !== el.dataset.shape) {
3457
+ el.dataset.shape = html;
3458
+ el.innerHTML = html;
3459
+ }
3460
+ const now = Date.now();
3461
+ el.querySelectorAll('.zen-agent-time').forEach((t, i) => {
3462
+ t.textContent = formatDuration(agentElapsed(active[i], now));
3463
+ });
3454
3464
  }
3455
3465
 
3456
3466
  // Filter shutdown ghosts: for same-type agents, keep if they overlapped (parallel)
@@ -4111,11 +4121,13 @@ function renderSessions() {
4111
4121
  ${renderZenBookmarksHtml(zenSession.id)}
4112
4122
  </div>
4113
4123
  ${zenSession.hasWorkflow ? renderWorkflowLiveHtml(zenSession.id) : ''}
4114
- <div class="zen-agents">${zenAgentsHtml(visibleAgents())}</div>`;
4124
+ <div class="zen-agents"></div>`;
4125
+ renderZenAgents(visibleAgents());
4115
4126
  bindLinkedDocsHandlers(sessionsList.querySelector('.linked-docs-section'), zenSession.id);
4116
4127
  artifactsSection.load(zenSession.id);
4117
4128
  scratchFilesSection.load(zenSession.id);
4118
4129
  if (zenSession.hasWorkflow) pollWorkflowLive(zenSession.id);
4130
+ restoreSessionCursor();
4119
4131
  return;
4120
4132
  }
4121
4133
 
@@ -4364,7 +4376,10 @@ function renderSessions() {
4364
4376
  const tail = sectioned ? sectionHtml(SECTION_SESSIONS, 'Sessions', true, restHtml, sgRest.length === 0) : restHtml;
4365
4377
  sessionsList.innerHTML = html + sgSectionHtml(true) + tail + outsideHtml;
4366
4378
  }
4379
+ restoreSessionCursor();
4380
+ }
4367
4381
 
4382
+ function restoreSessionCursor() {
4368
4383
  const navItems = getNavigableItems();
4369
4384
  const allSessions = getSessionItems();
4370
4385
  const activeIdx = allSessions.findIndex((el) => el.classList.contains('active'));
@@ -7949,6 +7964,21 @@ document.addEventListener('keydown', (e) => {
7949
7964
  toggleZenMode();
7950
7965
  return;
7951
7966
  }
7967
+ if (e.key === '$' && plainShift) {
7968
+ e.preventDefault();
7969
+ openCost(contextSessionId());
7970
+ return;
7971
+ }
7972
+ if (matchKey(e, 'KeyM')) {
7973
+ e.preventDefault();
7974
+ openMarketplace(contextSessionId());
7975
+ return;
7976
+ }
7977
+ if (ClaudeHub.comboOf(e) === 'ctrl+m') {
7978
+ e.preventDefault();
7979
+ openMemory(contextSessionId());
7980
+ return;
7981
+ }
7952
7982
  if (e.key === '.' || e.key === '>') {
7953
7983
  const sid = sessionsList.querySelector('.kb-selected')?.dataset.sessionId || currentSessionId;
7954
7984
  if (sid) {
@@ -8089,23 +8119,6 @@ document.addEventListener('keydown', (e) => {
8089
8119
  else toggleScratchpad();
8090
8120
  return;
8091
8121
  }
8092
- if (e.key === '$' && !e.ctrlKey && !e.altKey && !e.metaKey) {
8093
- e.preventDefault();
8094
- openCost(contextSid);
8095
- return;
8096
- }
8097
- if (matchKey(e, 'KeyM')) {
8098
- e.preventDefault();
8099
- const mSession = contextSid ? sessions.find((s) => s.id === contextSid) : null;
8100
- openMarketplace(mSession?.project);
8101
- return;
8102
- }
8103
- if (ClaudeHub.comboOf(e) === 'ctrl+m') {
8104
- e.preventDefault();
8105
- const mSession = contextSid ? sessions.find((s) => s.id === contextSid) : null;
8106
- openMemory(mSession?.project);
8107
- return;
8108
- }
8109
8122
  if (ClaudeHub.comboOf(e) === 'ctrl+d') {
8110
8123
  e.preventDefault();
8111
8124
  dismissContextSession();
@@ -8709,7 +8722,9 @@ const ZOOM_CLAIMS = ['ctrlKey', 'metaKey'].flatMap((mod) =>
8709
8722
  // first keeps it, and text fields keep every key.
8710
8723
  const PANE_CLAIMS = [
8711
8724
  ...['\\', '[', ']', 'ContextMenu', 'Escape'].map((key) => ({ key, code: '' })),
8712
- ...['|', '{', '}', 'F10'].map((key) => ({ shiftKey: true, key, code: '' })),
8725
+ ...['|', '{', '}', 'F10', '$'].map((key) => ({ shiftKey: true, key, code: '' })),
8726
+ { key: 'Unidentified', code: 'KeyM' },
8727
+ { ctrlKey: true, key: 'm', code: '' },
8713
8728
  ...['BracketLeft', 'BracketRight', 'KeyW'].map((code) => ({ altKey: true, key: 'Unidentified', code })),
8714
8729
  ...['KeyL', 'KeyM', 'KeyP', 'KeyS', 'KeyZ'].map((code) => ({ shiftKey: true, key: 'Unidentified', code })),
8715
8730
  ...'123456789'.split('').map((n) => ({ ctrlKey: true, altKey: true, key: 'Unidentified', code: `Digit${n}` })),
@@ -9197,7 +9212,7 @@ function createPreviewFrame(className, srcdoc, sandbox = 'allow-scripts allow-po
9197
9212
  frame.setAttribute('referrerpolicy', 'no-referrer');
9198
9213
  frame.srcdoc = srcdoc;
9199
9214
  bridgedFrames.add(frame);
9200
- frame.addEventListener('load', () => sendBridgeClaimsAll([frame], keyClaims()));
9215
+ frame.addEventListener('load', () => sendBridgeClaimsAll([frame], bridgeClaims(keyClaims())));
9201
9216
  return frame;
9202
9217
  }
9203
9218
 
@@ -10275,7 +10290,7 @@ function setupEventSource() {
10275
10290
  // Alone, the page would open a tab without a user gesture, which the browser blocks; the
10276
10291
  // CLI prints the link instead.
10277
10292
  if (data.type === 'session:cost' && hub.inHub) {
10278
- openCost(data.id, data.agent);
10293
+ openCost(data.id, { agent: data.agent, pane: false });
10279
10294
  }
10280
10295
 
10281
10296
  if (data.type === 'session:pin') {
@@ -13148,31 +13163,33 @@ function openFolderInEditor(folder, file) {
13148
13163
  postAndToast('/api/open-folder', body, 'folder');
13149
13164
  }
13150
13165
 
13151
- function openCost(sessionId, agentId) {
13152
- if (sessionId && agentId && hub.can('agent.cost'))
13153
- return hub.invoke('agent.cost', { session: sessionId, agent: agentId });
13154
- hub.invoke('session.cost', sessionId ? { session: sessionId } : {});
13166
+ // Opens the app in a pane of `sid` and brings the pane into view. With no session, or `pane: false`
13167
+ // (a Shift-click), the hub switches to the app's tab instead.
13168
+ async function openAppAction(action, params, sid, { pane = true, label }) {
13169
+ if (!sid || !pane) return hub.invoke(action, params);
13170
+ const { url } = await hub.invoke(action, params, { open: 'pane' });
13171
+ if (!url) return;
13172
+ const onScreen = await postPane(sid, { target: url, title: label });
13173
+ if (onScreen !== null) revealPane(sid, onScreen);
13155
13174
  }
13156
13175
 
13157
- function openMarketplace(projectPath) {
13158
- hub.invoke('project.plugins', projectPath ? { project: projectPath } : {});
13176
+ function openCost(sid, { agent, pane } = {}) {
13177
+ const opts = { pane, label: 'Cost' };
13178
+ if (sid && agent && hub.can('agent.cost')) return openAppAction('agent.cost', { session: sid, agent }, sid, opts);
13179
+ return openAppAction('session.cost', sid ? { session: sid } : {}, sid, opts);
13159
13180
  }
13160
13181
 
13161
- function openMemory(projectPath) {
13162
- hub.invoke('project.memory', projectPath ? { project: projectPath } : {});
13182
+ function openProjectApp(action, label, sid, { pane } = {}) {
13183
+ const project = sessions.find((s) => s.id === sid)?.project;
13184
+ return openAppAction(action, project ? { project } : {}, sid, { pane, label });
13163
13185
  }
13186
+ const openMarketplace = (sid, opts) => openProjectApp('project.plugins', 'Marketplace', sid, opts);
13187
+ const openMemory = (sid, opts) => openProjectApp('project.memory', 'Memory', sid, opts);
13164
13188
 
13165
- function openForInfoModalProject(open) {
13166
- const s = sessions.find((x) => x.id === _infoModalSessionId);
13167
- if (s?.project) open(s.project);
13168
- }
13169
- // biome-ignore lint/correctness/noUnusedVariables: used in HTML
13170
- function openMarketplaceForInfoModal() {
13171
- openForInfoModalProject(openMarketplace);
13172
- }
13173
13189
  // biome-ignore lint/correctness/noUnusedVariables: used in HTML
13174
- function openMemoryForInfoModal() {
13175
- openForInfoModalProject(openMemory);
13190
+ function openAppForInfoModal(open, e) {
13191
+ closeTeamModal();
13192
+ open(_infoModalSessionId, { pane: !e.shiftKey });
13176
13193
  }
13177
13194
 
13178
13195
  //#endregion
@@ -13883,6 +13900,11 @@ async function loadPaneView(sid, pane, view, scroll) {
13883
13900
  frame.setAttribute('referrerpolicy', 'no-referrer');
13884
13901
  frame.title = pane.title;
13885
13902
  frame.src = pane.target;
13903
+ frame.addEventListener('load', () => {
13904
+ view.classList.remove('keys-bridged');
13905
+ frame.hostKeysSig = null;
13906
+ pushTerminalClaims();
13907
+ });
13886
13908
  view.appendChild(frame);
13887
13909
  view.insertAdjacentHTML('beforeend', paneUrlBarHtml());
13888
13910
  // Framed at once; a refusal found by the probe swaps the frame for the card.
@@ -15574,23 +15596,49 @@ function bridgeClaims(claims) {
15574
15596
  return { ...claims, keys: [...claims.keys, ...ZOOM_CLAIMS, ...PANE_CLAIMS, ...review] };
15575
15597
  }
15576
15598
 
15577
- function sendBridgeClaimsAll(frames, claims) {
15578
- const base = bridgeClaims(claims);
15599
+ function sendBridgeClaimsAll(frames, base) {
15579
15600
  const rest = frames.filter((f) => f !== showState.frame);
15580
15601
  sendBridgeClaims(rest, base);
15581
15602
  if (rest.length < frames.length)
15582
15603
  sendBridgeClaims([showState.frame], { ...base, keys: [...base.keys, ...SHOW_CLAIMS] });
15583
15604
  }
15584
15605
 
15606
+ // A hub app in a URL pane runs the SDK, which hands back these keys (hub protocol §5.2). Only keys with
15607
+ // Ctrl, Alt or Meta: the page keeps Escape, brackets and its own letter keys.
15608
+ function sendHostKeys(frames, bridged) {
15609
+ const keys = bridged.keys.filter((k) => k.ctrlKey || k.altKey || k.metaKey);
15610
+ const sig = JSON.stringify(keys);
15611
+ for (const f of frames) {
15612
+ const origin = URL.parse(f.src)?.origin;
15613
+ if (!origin || origin === 'null' || f.hostKeysSig === sig) continue;
15614
+ f.hostKeysSig = sig;
15615
+ f.contentWindow?.postMessage({ type: 'hub:hostKeys', keys }, origin);
15616
+ }
15617
+ }
15618
+
15619
+ const urlPaneFrames = () => [...paneViews.querySelectorAll('.pane-url-frame')];
15620
+
15621
+ window.addEventListener('message', (e) => {
15622
+ const type = e.data?.type;
15623
+ if (type !== 'hub:hostKey' && type !== 'hub:hostReady') return;
15624
+ const frame = urlPaneFrames().find((f) => f.contentWindow === e.source);
15625
+ if (!frame || URL.parse(frame.src)?.origin !== e.origin) return;
15626
+ if (type === 'hub:hostReady') frame.parentElement.classList.add('keys-bridged');
15627
+ else replayKey(e.data, frame);
15628
+ });
15629
+
15585
15630
  function pushTerminalClaims() {
15586
15631
  if (terminalClaimsQueued) return;
15587
15632
  terminalClaimsQueued = true;
15588
15633
  queueMicrotask(() => {
15589
15634
  terminalClaimsQueued = false;
15590
15635
  const bridged = bridgedFrameEls();
15591
- if (!termFrame.inited && !kanbotState.inited && !bridged.length) return;
15636
+ const urlFrames = urlPaneFrames();
15637
+ if (!termFrame.inited && !kanbotState.inited && !bridged.length && !urlFrames.length) return;
15592
15638
  const claims = keyClaims();
15593
- sendBridgeClaimsAll(bridged, claims);
15639
+ const base = bridgeClaims(claims);
15640
+ sendBridgeClaimsAll(bridged, base);
15641
+ sendHostKeys(urlFrames, base);
15594
15642
  const sig = JSON.stringify(claims);
15595
15643
  sendFrameClaims(kanbotState, claims, sig, postToKanbotFrame);
15596
15644
  sendFrameClaims(termFrame, claims, sig, terminalFrameSend);
@@ -16999,10 +17047,15 @@ async function openShowInPane() {
16999
17047
  }
17000
17048
 
17001
17049
  // `onScreen` is postPane's result: null failed, true is already in view unless the terminal covers it.
17050
+ function revealPane(sid, onScreen) {
17051
+ if (onScreen && !wantsTerminal()) syncPanes();
17052
+ else if (sid === currentSessionId && wantsTerminal()) toggleTerminal();
17053
+ else openSession(sid);
17054
+ }
17055
+
17002
17056
  function toastPaneAdded(sid, onScreen, label) {
17003
17057
  if (onScreen === null || (onScreen && !wantsTerminal())) return;
17004
- const open = () => (sid === currentSessionId && wantsTerminal() ? toggleTerminal() : openSession(sid));
17005
- showToast(`${label} added as a pane`, 'success', { label: 'Open', onClick: open });
17058
+ showToast(`${label} added as a pane`, 'success', { label: 'Open', onClick: () => revealPane(sid, false) });
17006
17059
  }
17007
17060
 
17008
17061
  function showCommand(cmd) {
package/public/index.html CHANGED
@@ -557,13 +557,13 @@
557
557
  <button id="session-info-sticky-btn" class="icon-btn" style="display:none" title="Sticky pin — always show in the list" onclick="toggleSessionSticky(_infoModalSessionId); updateStickyBtnState()">
558
558
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2 L15 9 L22 9 L17 14 L19 22 L12 18 L5 22 L7 14 L2 9 L9 9 Z"/></svg>
559
559
  </button>
560
- <button id="session-info-cost-btn" class="icon-btn" style="display:none" title="Open in Cost" onclick="openCost(_infoModalSessionId)">
560
+ <button id="session-info-cost-btn" class="icon-btn" style="display:none" title="Open in Cost (Shift: Cost tab)" onclick="openAppForInfoModal(openCost, event)">
561
561
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"/><path d="M12 18V6"/></svg>
562
562
  </button>
563
- <button id="session-info-marketplace-btn" class="icon-btn" style="display:none" title="Open in Marketplace" onclick="openMarketplaceForInfoModal()">
563
+ <button id="session-info-marketplace-btn" class="icon-btn" style="display:none" title="Open in Marketplace (Shift: Marketplace tab)" onclick="openAppForInfoModal(openMarketplace, event)">
564
564
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 01-8 0"/></svg>
565
565
  </button>
566
- <button id="session-info-memory-btn" class="icon-btn" style="display:none" title="Open in Memory" onclick="openMemoryForInfoModal()">
566
+ <button id="session-info-memory-btn" class="icon-btn" style="display:none" title="Open in Memory (Shift: Memory tab)" onclick="openAppForInfoModal(openMemory, event)">
567
567
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>
568
568
  </button>
569
569
  <button class="modal-close" aria-label="Close dialog" onclick="closeTeamModal()">
package/public/style.css CHANGED
@@ -5058,7 +5058,7 @@ body.show-resizing iframe {
5058
5058
  color: var(--accent);
5059
5059
  }
5060
5060
 
5061
- .pane-view.frame-focus .pane-keys {
5061
+ .pane-view.frame-focus:not(.keys-bridged) .pane-keys {
5062
5062
  display: flex;
5063
5063
  }
5064
5064
 
@@ -1,4 +1,4 @@
1
- // claude-hub-sdk 1.7.0 (sha256 904e53d87ec4). Copied by npm run sdk:sync in claude-code-hub. Do not edit.
1
+ // claude-hub-sdk 1.8.0 (sha256 9a67dd2aa534). Copied by npm run sdk:sync in claude-code-hub. Do not edit.
2
2
  // Claude Code Hub SDK: the one rule that names a key press, shared by the hub page, client.js and
3
3
  // stub.js. The hub serves it to its page as /sdk/keys.js; mount() and sdk:sync put it in front of
4
4
  // client.js and stub.js, so an app still loads one file.
@@ -33,7 +33,21 @@ var ClaudeHubKeys = (() => {
33
33
  return combo.split('+').map((k) => mods[k] ?? named[k] ?? (k.length === 1 ? k.toUpperCase() : k));
34
34
  }
35
35
 
36
- return { comboOf, keyParts };
36
+ // A host claim (protocol §5.2) names a press by its physical code, or by its character when the layout
37
+ // moves the key: {ctrlKey?, altKey?, shiftKey?, metaKey?, code, key: 'Unidentified'} or {…, key, code: ''}.
38
+ // A press matches a claim when one of its two signatures is the claim's first. AltGr types a
39
+ // character, so its press matches by character only. cck's frames use the same format.
40
+ function keySigs(e) {
41
+ const mods = [e.ctrlKey && 'ctrl', e.altKey && 'alt', e.shiftKey && 'shift', e.metaKey && 'meta']
42
+ .filter(Boolean)
43
+ .join('+');
44
+ return [
45
+ e.code && !e.getModifierState?.('AltGraph') && `${mods}|c:${e.code}`,
46
+ typeof e.key === 'string' && e.key !== 'Unidentified' && `${mods}|k:${e.key.toLowerCase()}`,
47
+ ].filter(Boolean);
48
+ }
49
+
50
+ return { comboOf, keyParts, keySigs };
37
51
  })();
38
52
 
39
53
  if (typeof module === 'object' && module.exports) module.exports = ClaudeHubKeys;
@@ -69,9 +83,10 @@ if (typeof module === 'object' && module.exports) module.exports = ClaudeHubKeys
69
83
  bindTheme: () => off,
70
84
  handle() {},
71
85
  publish() {},
72
- invoke(action, params = {}) {
86
+ invoke(action, params = {}, opts = {}) {
73
87
  const target = standaloneFn(action)?.(params);
74
88
  if (!target) return Promise.resolve({ ok: false, reason: 'unhandled' });
89
+ if (opts.open === 'pane') return Promise.resolve({ ok: true, handledBy: 'standalone', url: target });
75
90
  win.open(target, '_blank', 'noopener');
76
91
  return Promise.resolve({ ok: true, handledBy: 'standalone' });
77
92
  },