claude-code-kanban 7.1.2 → 7.3.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/hub-app.json CHANGED
@@ -5,6 +5,7 @@
5
5
  "icon": "columns",
6
6
  "run": { "entry": "server.js", "defaultPort": 3541 },
7
7
  "publishes": ["session.changed"],
8
+ "keys": { "keeps": ["ctrl+alt+n", "ctrl+alt+r", "ctrl+alt+s"] },
8
9
  "provides": {
9
10
  "projects": { "path": "/api/projects" },
10
11
  "terminal": { "liveWork": "/api/terminals" }
package/lib/panes.js CHANGED
@@ -168,7 +168,15 @@ function createPaneStore({ load, save, onChange = () => {}, now = Date.now }) {
168
168
  return removed;
169
169
  }
170
170
 
171
- return { get, add, remove, reorder, prune, reload };
171
+ function fileTargets() {
172
+ const out = new Set();
173
+ for (const layout of bySession.values()) {
174
+ for (const p of layout.panes) if (p.kind !== 'url') out.add(p.target);
175
+ }
176
+ return out;
177
+ }
178
+
179
+ return { get, add, remove, reorder, prune, reload, fileTargets };
172
180
  }
173
181
 
174
182
  module.exports = { createPaneStore, isOwnOrigin, portOf, MAX_PANES, MAX_TITLE };
package/lib/terminal.js CHANGED
@@ -12,6 +12,7 @@
12
12
  // gates and hands the upgraded socket over, so no keystroke or output byte waits on
13
13
  // cck's main thread.
14
14
 
15
+ const { spawnSync } = require('node:child_process');
15
16
  const crypto = require('node:crypto');
16
17
  const fs = require('node:fs');
17
18
  const os = require('node:os');
@@ -147,10 +148,25 @@ function quoteArg(arg, family) {
147
148
  return family === 'cmd' ? `"${arg}"` : `'${arg}'`;
148
149
  }
149
150
 
151
+ // Free text, unlike quoteArg's input. Inside single quotes pwsh only reads a quote mark (it
152
+ // counts the typographic ones too) and POSIX shells read nothing, so doubling or closing
153
+ // around the quote is enough. cmd has no quoting that holds % and ", so start() refuses it.
154
+ function quotePrompt(text, family) {
155
+ if (family === 'pwsh') return `'${text.replace(/['‘’‚‛]/g, '$&$&')}'`;
156
+ return `'${text.replace(/'/g, "'\\''")}'`;
157
+ }
158
+
159
+ // The prompt goes first: -w takes an optional value, so a prompt after it would become the worktree name.
160
+ function claudeCommand(shell, claudeArgs, prompt) {
161
+ const family = shellFamily(shell);
162
+ const args = claudeArgs.map((a) => quoteArg(a, family));
163
+ return ['claude', ...(prompt ? [quotePrompt(prompt, family)] : []), ...args].join(' ');
164
+ }
165
+
150
166
  function shellArgs(shell, claudeArgs) {
151
167
  const family = shellFamily(shell);
152
168
  if (!claudeArgs) return family === 'pwsh' ? ['-NoLogo'] : family === 'gitbash' ? ['--login', '-i'] : [];
153
- const cmd = ['claude', ...claudeArgs].map((a) => quoteArg(a, family)).join(' ');
169
+ const cmd = claudeCommand(shell, claudeArgs);
154
170
  if (family === 'pwsh') return ['-NoLogo', '-NoExit', '-Command', cmd];
155
171
  if (family === 'cmd') return ['/k', cmd];
156
172
  if (family === 'gitbash') return ['--login', '-i', '-c', `${cmd}; exec bash --login -i`];
@@ -196,13 +212,17 @@ function parseNewSpec(msg) {
196
212
  if (model && !MODELS.has(model)) return 'model';
197
213
  const taskList = msg.taskList || null;
198
214
  if (taskList && !(typeof taskList === 'string' && TASK_LIST_RE.test(taskList))) return 'task list id';
199
- // biome-ignore lint/suspicious/noControlCharactersInRegex: strips ESC so a prompt cannot carry terminal sequences
200
- const prompt = typeof msg.prompt === 'string' ? msg.prompt.replace(/\x1b/g, '').trim() : '';
215
+ const edit = msg.edit === true;
216
+ // ESC so a prompt cannot carry terminal sequences; on a shell line (edit) every control
217
+ // character but tab and line breaks, since it could act as a key (Ctrl+C, Ctrl+U) instead of text.
218
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: strips control characters on purpose
219
+ const strip = edit ? /[\x00-\x08\x0b-\x1f\x7f]/g : /\x1b/g;
220
+ const prompt = typeof msg.prompt === 'string' ? msg.prompt.replace(strip, '').trim() : '';
201
221
  if (prompt.length > MAX_PROMPT) return 'prompt';
202
222
  const extraArgs = msg.extraArgs ?? [];
203
223
  const bad = extraArgsError(extraArgs);
204
224
  if (bad) return bad;
205
- return { cwd: msg.cwd, name: name || null, worktree, model, taskList, prompt: prompt || null, extraArgs };
225
+ return { cwd: msg.cwd, name: name || null, worktree, model, taskList, prompt: prompt || null, extraArgs, edit };
206
226
  }
207
227
 
208
228
  function samePath(a, b) {
@@ -442,19 +462,23 @@ function createTerminalService(o) {
442
462
  return /\btrust\b/i.test(screenText(s.term));
443
463
  }
444
464
 
445
- function queuePrompt(s, prompt) {
446
- let armed = false;
465
+ // `submit` is claude's prompt: wait for its input box, then press Enter. Without it the text
466
+ // is the claude command typed at the shell prompt once the shell's startup output settles,
467
+ // and Enter is left to the user; bracketed paste keeps a prompt's line breaks from running
468
+ // the command early (checked in Git Bash and pwsh).
469
+ function queueText(s, raw, submit) {
470
+ let armed = !submit;
447
471
  let timer = null;
448
- const text = bracketed(prompt);
472
+ const text = bracketed(raw);
449
473
  s.onOutput = (data) => {
450
474
  if (!armed && data.includes('\x1b[?2004h')) armed = true;
451
475
  if (!armed) return;
452
476
  clearTimeout(timer);
453
477
  timer = setTimeout(() => {
454
- if (s.exited || onTrustScreen(s)) return;
478
+ if (s.exited || (submit && onTrustScreen(s))) return;
455
479
  s.onOutput = null;
456
480
  s.pty.write(text);
457
- setTimeout(() => { if (!s.exited) s.pty.write('\r'); }, 100);
481
+ if (submit) setTimeout(() => { if (!s.exited) s.pty.write('\r'); }, 100);
458
482
  }, PROMPT_QUIET_MS);
459
483
  };
460
484
  }
@@ -492,7 +516,10 @@ function createTerminalService(o) {
492
516
  moved = true;
493
517
  continue;
494
518
  }
519
+ const had = s.claudePid;
495
520
  const pid = inputPid(s, live);
521
+ // An edit terminal starts claude whenever the user presses Enter, after syncBoost stopped looking.
522
+ if (pid && pid !== had) syncBoost(s);
496
523
  if (!pid || (live.some((l) => l.pid === pid) && pidAlive(pid))) continue;
497
524
  s.claudeExitedAt = Date.now();
498
525
  s.claudePid = null;
@@ -537,7 +564,8 @@ function createTerminalService(o) {
537
564
  if (taskList) env.CLAUDE_CODE_TASK_LIST_ID = taskList;
538
565
  // Git Bash's login profile cds to $HOME unless this is set.
539
566
  if (shellFamily(shell) === 'gitbash') env.CHERE_INVOKING = '1';
540
- const proc = pty.spawn(shell, shellArgs(shell, claudeArgsFor(mode, id, spec)), {
567
+ const claudeArgs = claudeArgsFor(mode, id, spec);
568
+ const proc = pty.spawn(shell, shellArgs(shell, spec?.edit ? null : claudeArgs), {
541
569
  name: 'xterm-256color',
542
570
  cols,
543
571
  rows,
@@ -549,9 +577,10 @@ function createTerminalService(o) {
549
577
  term.loadAddon(serializer);
550
578
  const s = {
551
579
  id, mode, cwd, pty: proc, term, serializer, sockets: new Set(), exited: false, ended: false, paused: false, startedAt: Date.now(),
552
- name: spec?.name || null, worktree: spec?.worktree || false, taskList, onOutput: null,
580
+ name: spec?.name || null, worktree: spec?.worktree || false, taskList, onOutput: null, edit: !!spec?.edit,
553
581
  };
554
- if (spec?.prompt) queuePrompt(s, spec.prompt);
582
+ if (s.edit) queueText(s, claudeCommand(shell, claudeArgs, spec.prompt), false);
583
+ else if (spec?.prompt) queueText(s, spec.prompt, true);
555
584
  if (mode === 'pick') watchPick(s);
556
585
 
557
586
  proc.onData((data) => {
@@ -697,6 +726,9 @@ function createTerminalService(o) {
697
726
  if (mode === 'new') {
698
727
  spec = parseNewSpec(msg);
699
728
  if (typeof spec === 'string') return { code: 4002, status: 400, error: `invalid ${spec}` };
729
+ if (spec.edit && spec.prompt && shellFamily(shell) === 'cmd') {
730
+ return { code: 4002, status: 400, error: 'cmd cannot take a typed prompt; clear the prompt or set the terminal shell to pwsh or gitbash' };
731
+ }
700
732
  }
701
733
  const known = await o.resolveCwd(id);
702
734
  let cwd = known;
@@ -751,7 +783,7 @@ function createTerminalService(o) {
751
783
  function list() {
752
784
  return [...sessions.values()].filter((s) => !s.ended).map((s) => ({
753
785
  id: s.id, mode: s.mode, cwd: s.cwd, pid: s.pty.pid, clients: s.sockets.size, startedAt: s.startedAt,
754
- name: s.name, worktree: s.worktree,
786
+ name: s.name, worktree: s.worktree, edit: s.edit,
755
787
  }));
756
788
  }
757
789
 
@@ -802,6 +834,8 @@ function createTerminalService(o) {
802
834
  const s = sessions.get(id);
803
835
  if (!s || s.ended || s.exited || s.claudeExitedAt || s.onOutput || s.mode === 'shell' || s.mode === 'fork') return false;
804
836
  if (onTrustScreen(s)) return false;
837
+ // Until the user runs the typed command, the input box is the shell's command line.
838
+ if (s.edit && !s.claudePid) return false;
805
839
  s.pty.write(bracketed(text));
806
840
  return true;
807
841
  }
@@ -810,6 +844,12 @@ function createTerminalService(o) {
810
844
  shuttingDown = true;
811
845
  clearTimeout(saveTimer);
812
846
  clearInterval(exitTimer);
847
+ // On Windows pty.kill() returns before the tree is gone, and the host exits right after
848
+ // shutdown, so claude outlived it. One synchronous tree kill ends every session first.
849
+ const pids = [...sessions.values()].map((s) => s.pty.pid).filter(Boolean);
850
+ if (process.platform === 'win32' && pids.length) {
851
+ spawnSync('taskkill', ['/f', '/t', ...pids.flatMap((pid) => ['/pid', String(pid)])], { windowsHide: true, stdio: 'ignore' });
852
+ }
813
853
  for (const s of sessions.values()) {
814
854
  try { s.pty.kill(); } catch { /* already gone */ }
815
855
  }
@@ -819,4 +859,4 @@ function createTerminalService(o) {
819
859
  return { token, handleUpgrade, clientConfig, list, claudePids, isRunning, end, authorized, startNew, paste, restore, shutdown, unavailableReason };
820
860
  }
821
861
 
822
- module.exports = { createTerminalService, readTerminalConfig, localUnavailableReason, refuseUpgrade, clientConfigFor, ptyEnv, shellArgs, resolveShell, claudeArgsFor, parseNewSpec, findPickProcess, tokenMatches };
862
+ module.exports = { createTerminalService, readTerminalConfig, localUnavailableReason, refuseUpgrade, clientConfigFor, ptyEnv, shellArgs, claudeCommand, resolveShell, claudeArgsFor, parseNewSpec, findPickProcess, tokenMatches };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-kanban",
3
- "version": "7.1.2",
3
+ "version": "7.3.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",