claude-code-kanban 7.22.0 → 7.24.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
@@ -7,7 +7,26 @@
7
7
  "publishes": ["session.changed"],
8
8
  "actions": {
9
9
  "handles": {
10
- "session.open": { "params": { "session": "string" }, "mode": "message" }
10
+ "session.open": { "params": { "session": "string" }, "mode": "message" },
11
+ "session.find": { "params": { "name": "string?" }, "mode": "message" },
12
+ "session.paste": { "params": { "text": "string", "submit": "string?", "session": "string?" }, "mode": "message" },
13
+ "task.add": { "params": { "subject": "string?", "description": "string?" }, "mode": "message" },
14
+ "task.live": { "params": { "subject": "string?", "description": "string?", "pending": "string?", "final": "string?", "submit": "string?", "cancel": "string?" }, "mode": "message" },
15
+ "session.live": { "params": { "text": "string", "final": "string?", "submit": "string?", "cancel": "string?", "session": "string?" }, "mode": "message" },
16
+ "session.new": {
17
+ "params": {
18
+ "cwd": "string?",
19
+ "name": "string?",
20
+ "prompt": "string?",
21
+ "worktree": "string?",
22
+ "model": "string?",
23
+ "effort": "string?",
24
+ "args": "string?",
25
+ "edit": "string?",
26
+ "stay": "string?"
27
+ },
28
+ "mode": "message"
29
+ }
11
30
  }
12
31
  },
13
32
  "settings": [
package/lib/show.js CHANGED
@@ -173,6 +173,16 @@ function createShowStore(o) {
173
173
  for (const ext of Object.values(EXT)) fs.rmSync(path.join(dir, `${id}.${ext}`), { force: true });
174
174
  }
175
175
 
176
+ // The kind of `file` when it is the post's own claimed file, else null: posting that path as a
177
+ // file post would have removeContent delete it.
178
+ function ownContentKind(dir, id, file) {
179
+ const m = CONTENT_FILE_RE.exec(path.basename(file));
180
+ if (!m || m[1].toLowerCase() !== id.toLowerCase()) return null;
181
+ const real = (p) => { try { return fs.realpathSync.native(p); } catch { return path.resolve(p); } };
182
+ if (real(path.dirname(file)).toLowerCase() !== real(dir).toLowerCase()) return null;
183
+ return Object.keys(EXT).find((k) => EXT[k] === m[2].toLowerCase());
184
+ }
185
+
176
186
  // A session resumed in a new terminal gets a new terminal id, so a terminal with no posts yet
177
187
  // takes the posts of the session it was started with.
178
188
  function sessionOf(terminalId) {
@@ -203,6 +213,11 @@ function createShowStore(o) {
203
213
  let at = key == null ? -1 : posts.findIndex((p) => p.key === key);
204
214
  const old = at >= 0 ? posts[at] : null;
205
215
  const id = old ? old.id : crypto.randomUUID();
216
+ const ownKind = old && file != null ? ownContentKind(dir, id, file) : null;
217
+ if (ownKind) {
218
+ kind = ownKind;
219
+ file = null;
220
+ }
206
221
  const target = file == null ? contentPath(dir, { id, kind }) : null;
207
222
  if (target && old && !old.file) {
208
223
  const from = contentPath(dir, old);
@@ -191,9 +191,14 @@ function createTerminalClient(o) {
191
191
  return call('end', id, providedToken);
192
192
  }
193
193
 
194
- async function paste(id, text) {
194
+ async function paste(id, text, submit = false) {
195
195
  if (!isRunning(id)) return false;
196
- return call('paste', id, text).catch(() => false);
196
+ return call('paste', id, text, submit).catch(() => false);
197
+ }
198
+
199
+ async function type(id, from, text, submit = false) {
200
+ if (!isRunning(id)) return false;
201
+ return call('type', id, from, text, submit).catch(() => false);
197
202
  }
198
203
 
199
204
  function restore() {
@@ -230,7 +235,7 @@ function createTerminalClient(o) {
230
235
  else spawn();
231
236
 
232
237
  return {
233
- token, handleUpgrade, clientConfig, unavailableReason, authorized, isRunning, hasTerminal, sessionOfTerminal, startNew, end, paste, restore,
238
+ token, handleUpgrade, clientConfig, unavailableReason, authorized, isRunning, hasTerminal, sessionOfTerminal, startNew, end, paste, type, restore,
234
239
  setServerUrl, sessions, stats, shutdown, started,
235
240
  ids: () => [...ids],
236
241
  awaitingTrust: () => [...awaitingTrust],
@@ -69,7 +69,8 @@ function init(msg) {
69
69
  const CALLS = {
70
70
  startNew: (msg, extraEnv) => service.startNew(msg, extraEnv),
71
71
  end: (id, token) => service.end(id, token),
72
- paste: (id, text) => service.paste(id, text),
72
+ paste: (id, text, submit) => service.paste(id, text, submit),
73
+ type: (id, from, text, submit) => service.type(id, from, text, submit),
73
74
  restore: () => service.restore(),
74
75
  sessions: () => service.list(),
75
76
  claudePids: () => service.claudePids(),
package/lib/terminal.js CHANGED
@@ -49,11 +49,14 @@ const OWNED_FLAGS = new Set([
49
49
  const MAX_PROMPT = 32 * 1024;
50
50
  const PROMPT_QUIET_MS = 400;
51
51
  const SUBMIT_POLL_MS = 1000;
52
+ const SUBMIT_AFTER_PASTE_MS = 200;
52
53
  const SUBMIT_CHECKS = 5;
53
54
  const SUBMIT_RETRIES = 2;
54
55
  // The answer lines of Claude Code's folder-trust dialog, current and earlier wording. They tell the
55
- // board the dialog is up; the looser onTrustScreen only holds the prompt back, where a miss is worse.
56
+ // board the dialog is up; the looser onDialogScreen only holds the prompt back, where a miss is worse.
56
57
  const TRUST_DIALOG = /Yes, I trust this folder|Do you trust the files in this folder\?/;
58
+ // The footer of Claude Code's choice dialogs: folder trust, a new .mcp.json server and the rest.
59
+ const DIALOG_SCREEN = /Enter to confirm/;
57
60
  // Watermarks from https://xtermjs.org/docs/guides/flowcontrol/ — pause the PTY while a
58
61
  // client lags, so a runaway process cannot grow the socket buffer without bound.
59
62
  const HIGH_WATER = 128 * 1024;
@@ -376,6 +379,7 @@ function createTerminalService(o) {
376
379
  const restoreGapMs = o.restoreGapMs ?? RESTORE_GAP_MS;
377
380
  const exitPollMs = o.exitPollMs ?? CLAUDE_EXIT_POLL_MS;
378
381
  const submitPollMs = o.submitPollMs ?? SUBMIT_POLL_MS;
382
+ const promptQuietMs = o.promptQuietMs ?? PROMPT_QUIET_MS;
379
383
  const pidAlive = o.isPidAlive || isPidAlive;
380
384
  const exitTimer = setInterval(checkExits, exitPollMs);
381
385
  exitTimer.unref();
@@ -505,8 +509,8 @@ function createTerminalService(o) {
505
509
  }
506
510
 
507
511
  // The folder and session names show on claude's main screen, so a "trust" in them is not the dialog.
508
- function onTrustScreen(s) {
509
- let text = screenText(s.term);
512
+ function onDialogScreen(s, text = screenText(s.term)) {
513
+ if (DIALOG_SCREEN.test(text)) return true;
510
514
  for (const own of [path.basename(s.cwd), s.name]) if (own) text = text.split(own).join('');
511
515
  return /\btrust\b/i.test(text);
512
516
  }
@@ -536,12 +540,13 @@ function createTerminalService(o) {
536
540
  if (!armed) return;
537
541
  timer = setTimeout(() => {
538
542
  if (s.exited) return;
539
- if (submit && onTrustScreen(s)) return setAwaitingTrust(s, TRUST_DIALOG.test(screenText(s.term)));
543
+ const shown = submit && screenText(s.term);
544
+ if (submit && onDialogScreen(s, shown)) return setAwaitingTrust(s, TRUST_DIALOG.test(shown));
540
545
  setAwaitingTrust(s, false);
541
546
  s.onOutput = null;
542
547
  s.pty.write(text);
543
548
  if (submit) setTimeout(() => submitPrompt(s), 100);
544
- }, PROMPT_QUIET_MS);
549
+ }, promptQuietMs);
545
550
  };
546
551
  }
547
552
 
@@ -938,16 +943,44 @@ function createTerminalService(o) {
938
943
  return r.error ? r : { id: r.session.id, cwd: r.session.cwd, name: r.session.name, prompted: r.session.prompted };
939
944
  }
940
945
 
941
- // Pastes into claude's input box and stops there: Enter would also submit whatever the
942
- // user had half-typed, and over a permission dialog it would answer it. A pending queued
943
- // prompt owns the input box until it is sent.
944
- function paste(id, text) {
946
+ // Pastes into claude's input box and stops there unless the caller asks for submit: Enter
947
+ // also submits whatever the user had half-typed. A dialog screen refuses the paste, so the
948
+ // Enter cannot answer one. A pending queued prompt owns the input box until it is sent.
949
+ function inputBox(id) {
945
950
  const s = sessions.get(id);
946
- if (!s || s.ended || s.exited || s.claudeExitedAt || s.onOutput || s.mode === 'shell') return false;
947
- if (onTrustScreen(s)) return false;
951
+ if (!s || s.ended || s.exited || s.claudeExitedAt || s.onOutput || s.mode === 'shell') return null;
952
+ // Not onDialogScreen: its loose "trust" test is for the first screen, and a conversation says trust.
953
+ const shown = screenText(s.term);
954
+ if (DIALOG_SCREEN.test(shown) || TRUST_DIALOG.test(shown)) return null;
948
955
  // Until the user runs the typed command, the input box is the shell's command line.
949
- if (s.edit && !s.claudePid) return false;
956
+ if (s.edit && !s.claudePid) return null;
957
+ return s;
958
+ }
959
+
960
+ function paste(id, text, submit = false) {
961
+ const s = inputBox(id);
962
+ if (!s) return false;
950
963
  s.pty.write(bracketed(text));
964
+ // claude takes a paste and an Enter in the same read as a line break in the input box.
965
+ if (submit) setTimeout(() => !s.exited && s.pty.write('\r'), SUBMIT_AFTER_PASTE_MS);
966
+ return true;
967
+ }
968
+
969
+ // Live dictation: the caller typed `from` before and now wants `text`. Backspaces remove what
970
+ // differs and keys type the rest, so the input box follows the words as they come.
971
+ // Typed keys, not a paste: claude shows a long paste as one "[Pasted text]" chip that a backspace removes whole.
972
+ function type(id, from, text, submit = false) {
973
+ const s = inputBox(id);
974
+ if (!s) return false;
975
+ // A line break would be Enter.
976
+ const keysOf = (t) => Array.from(t.replace(/\s*[\r\n]+\s*/g, ' '));
977
+ const was = keysOf(from);
978
+ const now = keysOf(text);
979
+ let same = 0;
980
+ while (same < was.length && same < now.length && was[same] === now[same]) same++;
981
+ const keys = '\x7f'.repeat(was.length - same) + now.slice(same).join('');
982
+ if (keys) s.pty.write(keys);
983
+ if (submit) setTimeout(() => !s.exited && s.pty.write('\r'), keys ? SUBMIT_AFTER_PASTE_MS : 0);
951
984
  return true;
952
985
  }
953
986
 
@@ -967,7 +1000,7 @@ function createTerminalService(o) {
967
1000
  sessions.clear();
968
1001
  }
969
1002
 
970
- return { token, handleUpgrade, clientConfig, list, claudePids, isRunning, end, authorized, startNew, paste, restore, shutdown, unavailableReason };
1003
+ return { token, handleUpgrade, clientConfig, list, claudePids, isRunning, end, authorized, startNew, paste, type, restore, shutdown, unavailableReason };
971
1004
  }
972
1005
 
973
- module.exports = { MODELS,createTerminalService, readTerminalConfig, localUnavailableReason, refuseUpgrade, clientConfigFor, ptyEnv, shellArgs, claudeCommand, resolveShell, claudeArgsFor, parseNewSpec, findPickProcess, tokenMatches };
1006
+ module.exports = { MODELS,createTerminalService, DIALOG_SCREEN, screenText, 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.22.0",
3
+ "version": "7.24.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",
@@ -13,6 +13,7 @@
13
13
  "test": "node --test test/*.test.js",
14
14
  "test:swarm": "npm --prefix extensions/swarm/plugin/plugins/swarm test",
15
15
  "validate:schemas": "node test/validate-live-schemas.js",
16
+ "test:live-dialogs": "node test/live-startup-dialogs.js",
16
17
  "prepare": "husky"
17
18
  },
18
19
  "repository": {