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 +1 -0
- package/lib/panes.js +9 -1
- package/lib/terminal.js +54 -14
- package/package.json +1 -1
- package/public/app.js +773 -88
- package/public/index.html +18 -8
- package/public/style.css +300 -10
- package/public/terminal-frame.js +6 -4
- package/public/vendor/claude-hub-sdk.js +40 -6
- package/server.js +41 -2
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
|
-
|
|
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 =
|
|
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
|
-
|
|
200
|
-
|
|
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
|
-
|
|
446
|
-
|
|
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(
|
|
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
|
|
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 (
|
|
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 };
|