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 +20 -1
- package/lib/show.js +15 -0
- package/lib/terminal-client.js +8 -3
- package/lib/terminal-host.js +2 -1
- package/lib/terminal.js +47 -14
- package/package.json +2 -1
- package/public/app.js +470 -45
- package/public/index.html +43 -0
- package/public/style.css +146 -8
- package/public/vendor/claude-hub-sdk.js +2 -2
- package/server.js +43 -13
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);
|
package/lib/terminal-client.js
CHANGED
|
@@ -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],
|
package/lib/terminal-host.js
CHANGED
|
@@ -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
|
|
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
|
|
509
|
-
|
|
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
|
-
|
|
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
|
-
},
|
|
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
|
|
942
|
-
// user had half-typed
|
|
943
|
-
// prompt owns the input box until it is sent.
|
|
944
|
-
function
|
|
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
|
|
947
|
-
|
|
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
|
|
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.
|
|
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": {
|