claude-code-kanban 7.4.0 → 7.6.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/README.md +5 -3
- package/cli.js +95 -29
- package/hub-app.json +5 -0
- package/lib/panes.js +41 -6
- package/lib/parsers.js +42 -3
- package/lib/retention.js +54 -6
- package/lib/scratchpad-dir.js +19 -2
- package/lib/session-cache.js +1 -1
- package/lib/session-events.js +174 -47
- package/lib/show.js +385 -0
- package/lib/task-dir.js +26 -0
- package/lib/terminal-client.js +14 -1
- package/lib/terminal-host.js +2 -1
- package/lib/terminal.js +28 -8
- package/package.json +1 -1
- package/plugin/plugins/claude-code-kanban/.claude-plugin/plugin.json +2 -5
- package/plugin/plugins/claude-code-kanban/hooks/doorbell.ts +136 -0
- package/plugin/plugins/claude-code-kanban/hooks/index.ts +4 -0
- package/plugin/plugins/claude-code-kanban/hooks/show.ts +120 -0
- package/plugin/plugins/claude-code-kanban/skills/dispatch/SKILL.md +2 -0
- package/plugin/plugins/claude-code-kanban/skills/kanban/SKILL.md +3 -3
- package/plugin/plugins/claude-code-kanban/skills/show/SKILL.md +45 -0
- package/plugin/plugins/claude-code-kanban/skills/show/references/NOTICE.md +27 -0
- package/plugin/plugins/claude-code-kanban/skills/show/references/chart.md +58 -0
- package/plugin/plugins/claude-code-kanban/skills/show/references/diagram/architecture.md +18 -0
- package/plugin/plugins/claude-code-kanban/skills/show/references/diagram/er.md +10 -0
- package/plugin/plugins/claude-code-kanban/skills/show/references/diagram/flow.md +13 -0
- package/plugin/plugins/claude-code-kanban/skills/show/references/diagram/layers.md +10 -0
- package/plugin/plugins/claude-code-kanban/skills/show/references/diagram/sequence.md +19 -0
- package/plugin/plugins/claude-code-kanban/skills/show/references/diagram/state.md +12 -0
- package/plugin/plugins/claude-code-kanban/skills/show/references/diagram/swimlane.md +10 -0
- package/plugin/plugins/claude-code-kanban/skills/show/references/diagram/timeline.md +9 -0
- package/plugin/plugins/claude-code-kanban/skills/show/references/diagram/tree.md +10 -0
- package/plugin/plugins/claude-code-kanban/skills/show/references/diagram.md +87 -0
- package/plugin/plugins/claude-code-kanban/skills/show/references/explain.md +98 -0
- package/plugin/plugins/claude-code-kanban/skills/show/references/mockup.md +30 -0
- package/plugin/plugins/claude-code-kanban/tests/doorbell.test.ts +133 -0
- package/plugin/plugins/claude-code-kanban/tests/show.test.ts +137 -0
- package/public/app.js +2151 -1108
- package/public/index.html +19 -36
- package/public/style.css +1047 -125
- package/public/terminal-frame.js +0 -2
- package/public/tree-highlight.js +137 -15
- package/server.js +237 -178
- package/lib/dispatch.js +0 -32
- package/plugin/plugins/claude-code-kanban/monitors.json +0 -8
- package/plugin/plugins/claude-code-kanban/scripts/postman.js +0 -82
- package/plugin/plugins/claude-code-kanban/skills/follow/SKILL.md +0 -47
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ Run `claude` in any project. You do not configure anything per project. Claude C
|
|
|
82
82
|
- **Embedded terminal.** Run a real Claude Code process for any session next to its board (<kbd>Ctrl</kbd>+<kbd>`</kbd>). <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>R</kbd> resumes a past session and <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>S</kbd> swaps to the previous one. The terminal is off by default when the board runs alone. Start it with `--enable-terminal` and open the `#t=<token>` link the server prints. [Embedded terminal](https://nikiforovall.blog/claude-code-kanban/guides/embedded-terminal/)
|
|
83
83
|
- **New session.** <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>N</kbd> opens a dialog to pick a folder, a name, a model, an optional git worktree and a first prompt. Needs the terminal.
|
|
84
84
|
- **Dispatch.** Hand a written task to a new session with `claude-code-kanban dispatch start`, or ask Claude to do it with the `dispatch` skill. Args after `--` go to `claude` as they are, and the started session reports back with `SendMessage`. Needs the terminal. [Dispatch tasks to other sessions](https://nikiforovall.blog/claude-code-kanban/guides/dispatch/)
|
|
85
|
-
- **Steer with card moves.**
|
|
85
|
+
- **Steer with card moves.** Drag a session's cards. Claude starts, parks or stops the task. Turn it off with `boardEvents.enabled: false` in `.cck/config.json`. [Claude Code plugin skills](https://nikiforovall.blog/claude-code-kanban/guides/plugin-skills/)
|
|
86
86
|
- **Review comments.** Select text in a previewed file or the plan, add comments and send them to the session in one step. [Review comments](https://nikiforovall.blog/claude-code-kanban/guides/review-comments/)
|
|
87
87
|
|
|
88
88
|
<picture>
|
|
@@ -98,7 +98,7 @@ Run `claude` in any project. You do not configure anything per project. Claude C
|
|
|
98
98
|
### Organize
|
|
99
99
|
|
|
100
100
|
- **Session groups.** Drag sessions and projects from different folders into one named group in the sidebar. [Session groups](https://nikiforovall.blog/claude-code-kanban/guides/session-groups/)
|
|
101
|
-
- **Filters, search and pins.** Filter by project and activity, search across sessions and tasks, pin a session or make it sticky. <kbd>Shift</kbd>+<kbd>P</kbd> opens the session picker.
|
|
101
|
+
- **Filters, search and pins.** Filter by project and activity, search across sessions and tasks, pin a session or make it sticky. <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>P</kbd> opens the session picker, and <kbd>Shift</kbd>+<kbd>P</kbd> the project picker.
|
|
102
102
|
- **Scratchpad and linked documents.** Keep a note per session, project or group (<kbd>N</kbd>), and link files or web URLs to a session to open them in one click.
|
|
103
103
|
- **Themes.** 17 color themes, each in light and dark. <kbd>T</kbd> switches the mode.
|
|
104
104
|
- **Keyboard first.** Arrow keys move through tasks, and <kbd>Tab</kbd> moves between the sidebar and the board. Press <kbd>?</kbd> for the full list. [Keyboard shortcuts](https://nikiforovall.blog/claude-code-kanban/reference/keyboard-shortcuts/)
|
|
@@ -111,7 +111,8 @@ With no subcommand, `claude-code-kanban` starts the server. Subcommands talk to
|
|
|
111
111
|
- `task list` and `project list` to read tasks and projects.
|
|
112
112
|
- `doc link|unlink|list|preview` to link a file or URL to a session, or show it in the preview.
|
|
113
113
|
- `pane add|rm|list` to add live panes to a session's view.
|
|
114
|
-
- `dispatch start|list` to start sessions with a task in the board's terminal.
|
|
114
|
+
- `dispatch start|list|end` to start sessions with a task in the board's terminal, and end them.
|
|
115
|
+
- `skills get <name>` to print the guide for a skill bundled with this version.
|
|
115
116
|
|
|
116
117
|
Run `claude-code-kanban --help` or see the [CLI reference](https://nikiforovall.blog/claude-code-kanban/reference/cli/).
|
|
117
118
|
|
|
@@ -126,6 +127,7 @@ EDITOR="code -w" claude-code-kanban # Command for Open in editor (default
|
|
|
126
127
|
|
|
127
128
|
- The server listens on `127.0.0.1` only and has no authentication. To reach it from another machine, use `--host` and `--allowed-hosts`, and do it only on a network you trust.
|
|
128
129
|
- UI approvals are on by default. Turn them off or tune them in `<config-dir>/.cck/config.json`.
|
|
130
|
+
- Board events (card moves and reviews sent to the session as prompts) are on by default. Turn them off with `"boardEvents": {"enabled": false}` in the same file.
|
|
129
131
|
- Terminal settings, such as the shell, font size and scrollback, go in the `CCK_TERMINAL` JSON variable.
|
|
130
132
|
|
|
131
133
|
See [Configuration](https://nikiforovall.blog/claude-code-kanban/reference/configuration/) for every setting, and [Troubleshooting](https://nikiforovall.blog/claude-code-kanban/troubleshooting/) for common problems.
|
package/cli.js
CHANGED
|
@@ -224,7 +224,7 @@ const COMMANDS = {
|
|
|
224
224
|
},
|
|
225
225
|
},
|
|
226
226
|
dispatch: {
|
|
227
|
-
summary: 'Start
|
|
227
|
+
summary: 'Start, list and end Claude Code sessions for tasks in cck\'s terminal',
|
|
228
228
|
verbs: {
|
|
229
229
|
start: {
|
|
230
230
|
summary: 'Start claude in cck\'s terminal with a task; prints the session id',
|
|
@@ -257,6 +257,18 @@ const COMMANDS = {
|
|
|
257
257
|
},
|
|
258
258
|
run: runDispatchListCli,
|
|
259
259
|
},
|
|
260
|
+
end: {
|
|
261
|
+
summary: 'End the terminal of a session this session started',
|
|
262
|
+
usage: 'claude-code-kanban dispatch end (<session id or prefix> | --name <n>) [--json]',
|
|
263
|
+
flags: {
|
|
264
|
+
'<session id or prefix>': 'A session from `dispatch list`',
|
|
265
|
+
'--name <n>': 'The session\'s --name from dispatch start',
|
|
266
|
+
'--json': 'Output JSON',
|
|
267
|
+
},
|
|
268
|
+
notes: 'Needs the terminal token, like dispatch start. The worktree stays.',
|
|
269
|
+
examples: ['claude-code-kanban dispatch end --name api-worker'],
|
|
270
|
+
run: runDispatchEndCli,
|
|
271
|
+
},
|
|
260
272
|
},
|
|
261
273
|
},
|
|
262
274
|
skills: {
|
|
@@ -333,6 +345,20 @@ function resolveCliCommand(argv) {
|
|
|
333
345
|
return { kind: 'leaf', name: `${noun} ${verb}`, entry: entry.verbs[verb], args: argv.slice(4) };
|
|
334
346
|
}
|
|
335
347
|
|
|
348
|
+
// The website's reference/cli.md has the same table; change both.
|
|
349
|
+
const SERVER_FLAGS = [
|
|
350
|
+
['--port <n>', 'PORT', 'Port (default 3541)'],
|
|
351
|
+
['--open', '', 'Open the board in the browser'],
|
|
352
|
+
['--dir <path>', 'CLAUDE_CONFIG_DIR', 'Config dir (default ~/.claude); also for --install'],
|
|
353
|
+
['--enable-terminal', '', 'Turn on the embedded terminal'],
|
|
354
|
+
['--terminal-shell <shell>', 'CCK_TERMINAL_SHELL', 'gitbash, a program on PATH, or a path'],
|
|
355
|
+
['--host <addr>', 'HOST', 'Listen address (default 127.0.0.1)'],
|
|
356
|
+
['--allowed-hosts <list>', 'ALLOWED_HOSTS', 'Extra Host header values, comma-separated'],
|
|
357
|
+
['--<app>-url <url>', '<APP>_URL', 'Link to app: marketplace, cost, memory'],
|
|
358
|
+
['--install, --uninstall', '', 'Install or remove the plugin'],
|
|
359
|
+
['--yes', '', 'With --install: no prompt'],
|
|
360
|
+
];
|
|
361
|
+
|
|
336
362
|
function printTopHelp() {
|
|
337
363
|
console.log('Usage: claude-code-kanban <command> [args] [--flags]\n');
|
|
338
364
|
console.log('Commands:');
|
|
@@ -344,13 +370,11 @@ function printTopHelp() {
|
|
|
344
370
|
console.log('\nFlags:');
|
|
345
371
|
console.log(' --help, -h Show help (top-level, noun-level, or leaf-level)');
|
|
346
372
|
console.log(' --version, -v Print version and exit');
|
|
347
|
-
console.log('\nServer
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
console.log(
|
|
351
|
-
console.log('
|
|
352
|
-
console.log(' --yes With --install: install without a prompt');
|
|
353
|
-
console.log('\nEnvironment:');
|
|
373
|
+
console.log('\nServer (no command; a flag wins over its env var):');
|
|
374
|
+
const flagPad = Math.max(...SERVER_FLAGS.map(([flag]) => flag.length)) + 2;
|
|
375
|
+
const envPad = Math.max(...SERVER_FLAGS.map(([, env]) => env.length)) + 2;
|
|
376
|
+
for (const [flag, env, desc] of SERVER_FLAGS) console.log(` ${flag.padEnd(flagPad)}${env.padEnd(envPad)}${desc}`);
|
|
377
|
+
console.log('\nCommand env:');
|
|
354
378
|
console.log(' CCK_URL Server base URL, e.g. http://127.0.0.1:4795 (wins over PORT)');
|
|
355
379
|
console.log(' PORT Server port (default: the one this config dir\'s server reports, else 3541)');
|
|
356
380
|
console.log(' CLAUDE_CONFIG_DIR Claude config dir whose board to use');
|
|
@@ -492,7 +516,7 @@ function reportCliError(e) {
|
|
|
492
516
|
}
|
|
493
517
|
|
|
494
518
|
async function runPreviewCli(args) {
|
|
495
|
-
const filePathArg = args
|
|
519
|
+
const [filePathArg] = positionals(args, ['--session']);
|
|
496
520
|
if (!filePathArg) {
|
|
497
521
|
printLeafHelp(COMMANDS.doc.verbs.preview);
|
|
498
522
|
return 1;
|
|
@@ -620,9 +644,8 @@ async function runPaneListCli(args, entry) {
|
|
|
620
644
|
|
|
621
645
|
async function printLinkedDocs(sessionId, asJson) {
|
|
622
646
|
try {
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
const paths = (await res.json())[sessionId] || [];
|
|
647
|
+
const links = await cliGetJson(`/api/document/links?session=${encodeURIComponent(sessionId)}`, 'Linked docs');
|
|
648
|
+
const paths = links[sessionId] || [];
|
|
626
649
|
if (asJson) console.log(JSON.stringify(paths, null, 2));
|
|
627
650
|
else if (!paths.length) console.log(`No linked docs for session ${sessionId.slice(0, 8)}.`);
|
|
628
651
|
else for (const p of paths) console.log(p);
|
|
@@ -655,9 +678,11 @@ async function fetchSessionsList(limit, pinnedIds = [], project = null) {
|
|
|
655
678
|
const q = limit === null ? 'all' : String(limit);
|
|
656
679
|
const pinnedQ = pinnedIds.length ? `&pinned=${pinnedIds.join(',')}` : '';
|
|
657
680
|
const projectQ = project ? `&project=${encodeURIComponent(project)}` : '';
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
681
|
+
return cliGetJson(`/api/sessions?limit=${q}${pinnedQ}${projectQ}`, 'Session list');
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function fetchSessionsByIds(ids, label) {
|
|
685
|
+
return cliGetJson(`/api/sessions?limit=1&include=${ids.map(encodeURIComponent).join(',')}`, label);
|
|
661
686
|
}
|
|
662
687
|
|
|
663
688
|
async function fetchPinsMap() {
|
|
@@ -823,7 +848,7 @@ async function runSessionViewCli(args) {
|
|
|
823
848
|
if (!resolved) return 1;
|
|
824
849
|
let list;
|
|
825
850
|
try {
|
|
826
|
-
list = await
|
|
851
|
+
list = await fetchSessionsByIds([resolved.id], 'Session view');
|
|
827
852
|
} catch (e) { reportCliError(e); return 1; }
|
|
828
853
|
const s = list.find(x => x.id === resolved.id);
|
|
829
854
|
if (!s) {
|
|
@@ -900,7 +925,7 @@ async function runSessionSearchCli(args) {
|
|
|
900
925
|
if (!parsed.ok) return usageError(entry, parsed.error);
|
|
901
926
|
try {
|
|
902
927
|
const ids = (await cliGetJson(`/api/sessions/search?q=${encodeURIComponent(text)}`, 'Search')).slice(0, parsed.limit);
|
|
903
|
-
const list = ids.length ? await
|
|
928
|
+
const list = ids.length ? await fetchSessionsByIds(ids, 'Search') : [];
|
|
904
929
|
const byId = new Map(list.map(s => [s.id, s]));
|
|
905
930
|
const rows = ids.map(id => byId.get(id) || { id });
|
|
906
931
|
if (args.includes('--json')) {
|
|
@@ -1054,20 +1079,26 @@ function textArg(args, name) {
|
|
|
1054
1079
|
return file ? fs.readFileSync(cliPath(file), 'utf8') : getArgValue(args, name);
|
|
1055
1080
|
}
|
|
1056
1081
|
|
|
1057
|
-
|
|
1058
|
-
const sep = argv.indexOf('--');
|
|
1059
|
-
const args = sep === -1 ? argv : argv.slice(0, sep);
|
|
1060
|
-
const claudeArgs = sep === -1 ? [] : argv.slice(sep + 1);
|
|
1082
|
+
function readTerminalToken() {
|
|
1061
1083
|
const port = cliTargetPort();
|
|
1062
1084
|
if (port === null) {
|
|
1063
1085
|
console.error(unreachable());
|
|
1064
|
-
return
|
|
1086
|
+
return null;
|
|
1065
1087
|
}
|
|
1066
1088
|
const token = port && readCckJson(`terminal-tokens/${port}.json`)?.token;
|
|
1067
1089
|
if (!token) {
|
|
1068
1090
|
console.error(`No terminal token for ${displayPath(getClaudeDir())} at ${cliBaseUrl()}. The cck server must be running with the terminal enabled.`);
|
|
1069
|
-
return
|
|
1091
|
+
return null;
|
|
1070
1092
|
}
|
|
1093
|
+
return token;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
async function runDispatchStartCli(argv) {
|
|
1097
|
+
const sep = argv.indexOf('--');
|
|
1098
|
+
const args = sep === -1 ? argv : argv.slice(0, sep);
|
|
1099
|
+
const claudeArgs = sep === -1 ? [] : argv.slice(sep + 1);
|
|
1100
|
+
const token = readTerminalToken();
|
|
1101
|
+
if (!token) return 1;
|
|
1071
1102
|
let spec;
|
|
1072
1103
|
try { spec = textArg(args, 'spec'); } catch (e) { console.error(e.message); return 1; }
|
|
1073
1104
|
if (!spec) {
|
|
@@ -1107,15 +1138,50 @@ async function runDispatchStartCli(argv) {
|
|
|
1107
1138
|
} catch (e) { reportCliError(e); return 1; }
|
|
1108
1139
|
}
|
|
1109
1140
|
|
|
1110
|
-
async function
|
|
1141
|
+
async function fetchDispatches(all) {
|
|
1111
1142
|
const q = new URLSearchParams();
|
|
1112
|
-
if (!
|
|
1143
|
+
if (!all && process.env.CLAUDE_CODE_SESSION_ID) q.set('parent', process.env.CLAUDE_CODE_SESSION_ID);
|
|
1144
|
+
return (await cliGetJson(`/api/dispatch?${q}`, 'Dispatch list')).running.sort((a, b) => b.startedAt - a.startedAt);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
const dispatchLine = (r) => `${r.session}${r.name ? ` ${r.name}` : ''}${r.group ? ` [${r.group}]` : ''}`;
|
|
1148
|
+
|
|
1149
|
+
async function runDispatchListCli(args) {
|
|
1113
1150
|
try {
|
|
1114
|
-
const
|
|
1115
|
-
const rows = (await res.json()).running.sort((a, b) => b.startedAt - a.startedAt);
|
|
1151
|
+
const rows = await fetchDispatches(args.includes('--all'));
|
|
1116
1152
|
if (args.includes('--json')) console.log(JSON.stringify(rows, null, 2));
|
|
1117
1153
|
else if (!rows.length) console.log('No dispatches.');
|
|
1118
|
-
else for (const r of rows) console.log(
|
|
1154
|
+
else for (const r of rows) console.log(dispatchLine(r));
|
|
1155
|
+
return 0;
|
|
1156
|
+
} catch (e) { reportCliError(e); return 1; }
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
async function runDispatchEndCli(args) {
|
|
1160
|
+
const entry = COMMANDS.dispatch.verbs.end;
|
|
1161
|
+
const name = getArgValue(args, 'name');
|
|
1162
|
+
const id = positionals(args, ['--name'])[0];
|
|
1163
|
+
if (!id && !name) {
|
|
1164
|
+
printLeafHelp(entry);
|
|
1165
|
+
return 1;
|
|
1166
|
+
}
|
|
1167
|
+
if (id && name) return usageError(entry, 'Give one of <session id or prefix> or --name.');
|
|
1168
|
+
const token = readTerminalToken();
|
|
1169
|
+
if (!token) return 1;
|
|
1170
|
+
try {
|
|
1171
|
+
const matches = (await fetchDispatches(false)).filter(r => name ? r.name === name : r.session.startsWith(id));
|
|
1172
|
+
if (!matches.length) {
|
|
1173
|
+
console.error(`No running dispatch matches "${name || id}". Run "claude-code-kanban dispatch list".`);
|
|
1174
|
+
return 1;
|
|
1175
|
+
}
|
|
1176
|
+
if (matches.length > 1) {
|
|
1177
|
+
console.error(`"${name || id}" matches ${matches.length} dispatches:`);
|
|
1178
|
+
for (const r of matches) console.error(` ${dispatchLine(r)}`);
|
|
1179
|
+
return 1;
|
|
1180
|
+
}
|
|
1181
|
+
const { session } = matches[0];
|
|
1182
|
+
if (!await cliSendJson('DELETE', `/api/terminals/${session}`, undefined, 'Dispatch end', { 'x-terminal-token': token })) return 1;
|
|
1183
|
+
if (args.includes('--json')) console.log(JSON.stringify({ session }, null, 2));
|
|
1184
|
+
else console.log(`Ended session ${session}`);
|
|
1119
1185
|
return 0;
|
|
1120
1186
|
} catch (e) { reportCliError(e); return 1; }
|
|
1121
1187
|
}
|
|
@@ -1131,4 +1197,4 @@ async function runSkillsGetCli(args) {
|
|
|
1131
1197
|
return 0;
|
|
1132
1198
|
}
|
|
1133
1199
|
|
|
1134
|
-
module.exports = { runCli, COMMANDS };
|
|
1200
|
+
module.exports = { runCli, COMMANDS, SERVER_FLAGS };
|
package/hub-app.json
CHANGED
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
"run": { "entry": "server.js", "defaultPort": 3541 },
|
|
7
7
|
"publishes": ["session.changed"],
|
|
8
8
|
"keys": { "keeps": ["ctrl+alt+n", "ctrl+alt+r", "ctrl+alt+s"] },
|
|
9
|
+
"plugin": {
|
|
10
|
+
"id": "claude-code-kanban@claude-code-kanban",
|
|
11
|
+
"path": "plugin/plugins/claude-code-kanban",
|
|
12
|
+
"install": "claude-code-kanban --install --yes"
|
|
13
|
+
},
|
|
9
14
|
"provides": {
|
|
10
15
|
"projects": { "path": "/api/projects" },
|
|
11
16
|
"terminal": { "liveWork": "/api/terminals" }
|
package/lib/panes.js
CHANGED
|
@@ -11,7 +11,10 @@ const { pruneMap } = require('./retention');
|
|
|
11
11
|
|
|
12
12
|
const MAX_PANES = 50;
|
|
13
13
|
const MAX_TITLE = 100;
|
|
14
|
-
const
|
|
14
|
+
const FILE_KINDS = new Set(['html', 'markdown', 'text', 'image']);
|
|
15
|
+
const KINDS = new Set([...FILE_KINDS, 'url', 'message']);
|
|
16
|
+
const MAX_MESSAGE_TARGET = 300;
|
|
17
|
+
const isFileKind = (kind) => FILE_KINDS.has(kind);
|
|
15
18
|
|
|
16
19
|
const portOf = (u) => Number(u.port || (u.protocol === 'https:' ? 443 : 80));
|
|
17
20
|
|
|
@@ -39,9 +42,10 @@ function isOwnOrigin(url, { boardPort, boardHosts = [], hubUrl }) {
|
|
|
39
42
|
|
|
40
43
|
const emptyLayout = () => ({ rev: 0, nextId: 1, panes: [], updatedAt: null });
|
|
41
44
|
|
|
42
|
-
const targetKey = (kind, target) => (kind
|
|
45
|
+
const targetKey = (kind, target) => (isFileKind(kind) ? `file:${canonicalPath(target)}` : `${kind}:${target}`);
|
|
43
46
|
|
|
44
47
|
function defaultTitle(kind, target) {
|
|
48
|
+
if (kind === 'message') return 'Message';
|
|
45
49
|
if (kind !== 'url') return path.basename(target);
|
|
46
50
|
try {
|
|
47
51
|
return new URL(target).host;
|
|
@@ -50,6 +54,8 @@ function defaultTitle(kind, target) {
|
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
|
|
57
|
+
const paneTitle = (kind, target, title) => oneLine(title, MAX_TITLE) || oneLine(defaultTitle(kind, target), MAX_TITLE);
|
|
58
|
+
|
|
53
59
|
const idNumber = (id) => Number(id.slice(1)) || 0;
|
|
54
60
|
|
|
55
61
|
function cleanLayout(raw) {
|
|
@@ -118,8 +124,12 @@ function createPaneStore({ load, save, onChange = () => {}, now = Date.now }) {
|
|
|
118
124
|
}
|
|
119
125
|
|
|
120
126
|
// The same target added again returns its pane where it is, so a repeated `pane add` is harmless.
|
|
121
|
-
|
|
127
|
+
// `show` marks a show card's file, which the board renders with the card's theme.
|
|
128
|
+
function add(sessionId, { kind, target, title, show }) {
|
|
122
129
|
if (!KINDS.has(kind)) throw httpError(400, `Unknown pane kind: ${kind}`, 'bad_kind');
|
|
130
|
+
if (kind === 'message' && target.length > MAX_MESSAGE_TARGET) {
|
|
131
|
+
throw httpError(400, `A message target holds at most ${MAX_MESSAGE_TARGET} characters`, 'bad_target');
|
|
132
|
+
}
|
|
123
133
|
reload();
|
|
124
134
|
const layout = bySession.get(sessionId) || emptyLayout();
|
|
125
135
|
const key = targetKey(kind, target);
|
|
@@ -130,7 +140,8 @@ function createPaneStore({ load, save, onChange = () => {}, now = Date.now }) {
|
|
|
130
140
|
id: `p${layout.nextId++}`,
|
|
131
141
|
kind,
|
|
132
142
|
target,
|
|
133
|
-
title:
|
|
143
|
+
title: paneTitle(kind, target, title),
|
|
144
|
+
...(show && { show: true }),
|
|
134
145
|
addedAt: now(),
|
|
135
146
|
};
|
|
136
147
|
layout.panes.push(pane);
|
|
@@ -148,6 +159,30 @@ function createPaneStore({ load, save, onChange = () => {}, now = Date.now }) {
|
|
|
148
159
|
return write(sessionId, layout);
|
|
149
160
|
}
|
|
150
161
|
|
|
162
|
+
// Drops the show panes of these files; returns the new layout, or null when none matched.
|
|
163
|
+
function removeShow(sessionId, files) {
|
|
164
|
+
reload();
|
|
165
|
+
const layout = bySession.get(sessionId);
|
|
166
|
+
if (!layout) return null;
|
|
167
|
+
const keys = new Set(files.map((f) => targetKey('html', f)));
|
|
168
|
+
const kept = layout.panes.filter((p) => !(p.show && keys.has(targetKey(p.kind, p.target))));
|
|
169
|
+
if (kept.length === layout.panes.length) return null;
|
|
170
|
+
layout.panes = kept;
|
|
171
|
+
return write(sessionId, layout);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// An empty title gives the pane its default title back. Returns null when the session has no such pane.
|
|
175
|
+
function rename(sessionId, paneId, title) {
|
|
176
|
+
reload();
|
|
177
|
+
const layout = bySession.get(sessionId);
|
|
178
|
+
const pane = layout?.panes.find((p) => p.id === paneId);
|
|
179
|
+
if (!pane) return null;
|
|
180
|
+
const next = paneTitle(pane.kind, pane.target, title);
|
|
181
|
+
if (next === pane.title) return get(sessionId);
|
|
182
|
+
pane.title = next;
|
|
183
|
+
return write(sessionId, layout);
|
|
184
|
+
}
|
|
185
|
+
|
|
151
186
|
// `order` is every pane id of the session, in the new tab order.
|
|
152
187
|
function reorder(sessionId, order) {
|
|
153
188
|
reload();
|
|
@@ -171,12 +206,12 @@ function createPaneStore({ load, save, onChange = () => {}, now = Date.now }) {
|
|
|
171
206
|
function fileTargets() {
|
|
172
207
|
const out = new Set();
|
|
173
208
|
for (const layout of bySession.values()) {
|
|
174
|
-
for (const p of layout.panes) if (p.kind
|
|
209
|
+
for (const p of layout.panes) if (isFileKind(p.kind)) out.add(p.target);
|
|
175
210
|
}
|
|
176
211
|
return out;
|
|
177
212
|
}
|
|
178
213
|
|
|
179
|
-
return { get, add, remove, reorder, prune, reload, fileTargets };
|
|
214
|
+
return { get, add, remove, removeShow, rename, reorder, prune, reload, fileTargets };
|
|
180
215
|
}
|
|
181
216
|
|
|
182
217
|
module.exports = { createPaneStore, isOwnOrigin, portOf, MAX_PANES, MAX_TITLE };
|
package/lib/parsers.js
CHANGED
|
@@ -383,6 +383,11 @@ const modeOf = (data) => {
|
|
|
383
383
|
return (data.type === 'user' || data.type === 'permission-mode') && data.permissionMode || null;
|
|
384
384
|
};
|
|
385
385
|
|
|
386
|
+
// Claude Code writes its environment, scratchpad dir included, at each process start, so a resume
|
|
387
|
+
// in a worktree adds a second one: the last wins.
|
|
388
|
+
const scratchpadOf = (data) =>
|
|
389
|
+
(data.type === 'attachment' && data.attachment?.type === 'environment' && data.attachment.snapshot?.scratchpadDirectory) || null;
|
|
390
|
+
|
|
386
391
|
const isMainReply = (data) =>
|
|
387
392
|
data.type === 'assistant' && !data.isSidechain && data.message?.usage && data.message.model !== '<synthetic>';
|
|
388
393
|
|
|
@@ -426,7 +431,7 @@ const withoutUsage = (reply) => reply && { ...reply, usage: null };
|
|
|
426
431
|
// change or truncation (size < scannedUpTo).
|
|
427
432
|
|
|
428
433
|
function readSessionInfoFromJsonl(jsonlPath) {
|
|
429
|
-
const result = { slug: null, projectPath: null, cwd: null, gitBranch: null, customTitle: null, agentName: null, logicalParentUuid: null, compactBoundaryUuid: null, permissionMode: null, cacheTtl: null, lastReply: null };
|
|
434
|
+
const result = { slug: null, projectPath: null, cwd: null, gitBranch: null, customTitle: null, agentName: null, logicalParentUuid: null, compactBoundaryUuid: null, permissionMode: null, cacheTtl: null, lastReply: null, scratchpadDir: null };
|
|
430
435
|
let stat;
|
|
431
436
|
let fd;
|
|
432
437
|
try {
|
|
@@ -449,6 +454,7 @@ function readSessionInfoFromJsonl(jsonlPath) {
|
|
|
449
454
|
permissionMode: cached.permissionMode || null,
|
|
450
455
|
cacheTtl: cached.cacheTtl || null,
|
|
451
456
|
lastReply: cached.lastReply || null,
|
|
457
|
+
scratchpadDir: cached.scratchpadDir || null,
|
|
452
458
|
...pickTitles(readTitles(jsonlPath, stat))
|
|
453
459
|
};
|
|
454
460
|
}
|
|
@@ -463,6 +469,7 @@ function readSessionInfoFromJsonl(jsonlPath) {
|
|
|
463
469
|
result.permissionMode = cached.permissionMode || null;
|
|
464
470
|
result.cacheTtl = cached.cacheTtl || null;
|
|
465
471
|
result.lastReply = cached.lastReply || null;
|
|
472
|
+
result.scratchpadDir = cached.scratchpadDir || null;
|
|
466
473
|
}
|
|
467
474
|
|
|
468
475
|
let lastCwdSeen = result.cwd;
|
|
@@ -480,6 +487,8 @@ function readSessionInfoFromJsonl(jsonlPath) {
|
|
|
480
487
|
if (mode) result.permissionMode = mode;
|
|
481
488
|
const ttl = cacheTtlOf(data);
|
|
482
489
|
if (ttl) result.cacheTtl = ttl;
|
|
490
|
+
const scratchpad = scratchpadOf(data);
|
|
491
|
+
if (scratchpad) result.scratchpadDir = scratchpad;
|
|
483
492
|
if (isMainUser(data)) lastUserTs = data.timestamp;
|
|
484
493
|
else if (isMainReply(data)) result.lastReply = replyOf(data, lastUserTs);
|
|
485
494
|
if (data.subtype === 'compact_boundary') result.lastReply = withoutUsage(result.lastReply);
|
|
@@ -556,6 +565,7 @@ function readSessionInfoFromJsonl(jsonlPath) {
|
|
|
556
565
|
let latestTailCwd = null;
|
|
557
566
|
let latestTailMode = null;
|
|
558
567
|
let latestTailTtl = null;
|
|
568
|
+
let latestTailScratchpad = null;
|
|
559
569
|
let tailReply = null;
|
|
560
570
|
let tailRequestTs = null;
|
|
561
571
|
let compactedAfterReply = false;
|
|
@@ -583,6 +593,7 @@ function readSessionInfoFromJsonl(jsonlPath) {
|
|
|
583
593
|
try {
|
|
584
594
|
const data = JSON.parse(lines[i]);
|
|
585
595
|
if (!latestTailTtl) latestTailTtl = cacheTtlOf(data);
|
|
596
|
+
if (!latestTailScratchpad) latestTailScratchpad = scratchpadOf(data);
|
|
586
597
|
if (!result.slug && data.slug) result.slug = data.slug;
|
|
587
598
|
if (!result.projectPath && data.cwd) result.projectPath = data.cwd;
|
|
588
599
|
if (!result.gitBranch && data.gitBranch) result.gitBranch = data.gitBranch;
|
|
@@ -601,6 +612,8 @@ function readSessionInfoFromJsonl(jsonlPath) {
|
|
|
601
612
|
if (latestTailCwd) lastCwdSeen = latestTailCwd;
|
|
602
613
|
if (latestTailMode) result.permissionMode = latestTailMode;
|
|
603
614
|
if (latestTailTtl) result.cacheTtl = latestTailTtl;
|
|
615
|
+
// A record in the unread middle would be newer than the head's, so the head's cannot be trusted.
|
|
616
|
+
if (latestTailScratchpad || !reachedHead) result.scratchpadDir = latestTailScratchpad;
|
|
604
617
|
if (latestTailUserTs || !reachedHead) lastUserTs = latestTailUserTs;
|
|
605
618
|
if (tailReply) {
|
|
606
619
|
const reply = replyOf(tailReply, tailRequestTs);
|
|
@@ -635,6 +648,7 @@ function readSessionInfoFromJsonl(jsonlPath) {
|
|
|
635
648
|
permissionMode: result.permissionMode,
|
|
636
649
|
cacheTtl: result.cacheTtl,
|
|
637
650
|
lastReply: result.lastReply,
|
|
651
|
+
scratchpadDir: result.scratchpadDir,
|
|
638
652
|
lastUserTs
|
|
639
653
|
});
|
|
640
654
|
if (sessionInfoCache.size > SESSION_INFO_CACHE_MAX) {
|
|
@@ -1620,7 +1634,7 @@ async function readCompactSummaries(jsonlPath) {
|
|
|
1620
1634
|
if (!text) continue;
|
|
1621
1635
|
// Strip the "This session is being continued..." preamble if present.
|
|
1622
1636
|
text = text.replace(/^This session is being continued[^\n]*\n+(The summary below[^\n]*\n+)?/i, '').trim();
|
|
1623
|
-
if (text) results.push({ timestamp: obj.timestamp, summary: text });
|
|
1637
|
+
if (text) results.push({ timestamp: obj.timestamp, summary: text, source: 'inline' });
|
|
1624
1638
|
} catch (_) {}
|
|
1625
1639
|
}
|
|
1626
1640
|
} catch (_) {}
|
|
@@ -1651,7 +1665,7 @@ async function readCompactSummaries(jsonlPath) {
|
|
|
1651
1665
|
for (const b of blocks) {
|
|
1652
1666
|
if (b.type !== 'text' || !b.text) continue;
|
|
1653
1667
|
const match = b.text.match(/<summary>([\s\S]*?)(?:<\/summary>|$)/);
|
|
1654
|
-
if (match) { results.push({ timestamp: lastTs, summary: match[1].trim() }); found = true; break; }
|
|
1668
|
+
if (match) { results.push({ timestamp: lastTs, summary: match[1].trim(), source: 'subagent' }); found = true; break; }
|
|
1655
1669
|
}
|
|
1656
1670
|
if (found) break;
|
|
1657
1671
|
} catch (_) {}
|
|
@@ -1661,6 +1675,30 @@ async function readCompactSummaries(jsonlPath) {
|
|
|
1661
1675
|
return results.sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
|
|
1662
1676
|
}
|
|
1663
1677
|
|
|
1678
|
+
// A subagent summary's timestamp is the compact agent's last record, written just
|
|
1679
|
+
// before the parent's isCompactSummary record; compactions are minutes apart.
|
|
1680
|
+
const COMPACT_SUMMARY_MATCH_MS = 2 * 60 * 1000;
|
|
1681
|
+
|
|
1682
|
+
function fillCompactSummaries(chips, summaries) {
|
|
1683
|
+
const candidates = summaries.filter(s => s.source === 'subagent' && s.timestamp);
|
|
1684
|
+
const used = new Set();
|
|
1685
|
+
for (const chip of chips) {
|
|
1686
|
+
if (chip.compactSummary || !chip.timestamp) continue;
|
|
1687
|
+
const at = Date.parse(chip.timestamp);
|
|
1688
|
+
let best = null;
|
|
1689
|
+
let bestGap = COMPACT_SUMMARY_MATCH_MS;
|
|
1690
|
+
for (const s of candidates) {
|
|
1691
|
+
if (used.has(s)) continue;
|
|
1692
|
+
const gap = Math.abs(Date.parse(s.timestamp) - at);
|
|
1693
|
+
if (gap <= bestGap) { best = s; bestGap = gap; }
|
|
1694
|
+
}
|
|
1695
|
+
if (best) {
|
|
1696
|
+
used.add(best);
|
|
1697
|
+
chip.compactSummary = best.summary;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1664
1702
|
async function findTerminatedTeammates(jsonlPath) {
|
|
1665
1703
|
return (await buildSessionDigest(jsonlPath)).terminated;
|
|
1666
1704
|
}
|
|
@@ -2014,6 +2052,7 @@ module.exports = {
|
|
|
2014
2052
|
buildAgentProgressMap,
|
|
2015
2053
|
buildSessionDigest,
|
|
2016
2054
|
readCompactSummaries,
|
|
2055
|
+
fillCompactSummaries,
|
|
2017
2056
|
readArtifactLinks,
|
|
2018
2057
|
readScratchpadCreations,
|
|
2019
2058
|
findTerminatedTeammates,
|
package/lib/retention.js
CHANGED
|
@@ -38,11 +38,24 @@ function pruneMap(map, atOf, { known, maxAgeMs, now }) {
|
|
|
38
38
|
return removed;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
const textOrNull = (v) => (typeof v === 'string' && v ? v : null);
|
|
42
|
+
|
|
43
|
+
function dispatchedEntry(e) {
|
|
44
|
+
return {
|
|
45
|
+
parent: textOrNull(e.parent),
|
|
46
|
+
at: e.at,
|
|
47
|
+
name: textOrNull(e.name),
|
|
48
|
+
group: textOrNull(e.group),
|
|
49
|
+
cwd: textOrNull(e.cwd),
|
|
50
|
+
worktree: e.worktree === true ? true : textOrNull(e.worktree),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
41
54
|
/**
|
|
42
55
|
* Sessions started through `dispatch start`, kept after their terminal ends so the card
|
|
43
|
-
* keeps its marker.
|
|
56
|
+
* keeps its marker, and across a restart so `dispatch list` finds a session cck resumes.
|
|
44
57
|
* @param {object} o
|
|
45
|
-
* @param {() => object|null} o.load returns `{version: 1, sessions: {[id]: {parent, at}}}` or null
|
|
58
|
+
* @param {() => object|null} o.load returns `{version: 1, sessions: {[id]: {parent, at, name, group, cwd, worktree}}}` or null
|
|
46
59
|
* @param {(data: object) => void} o.save
|
|
47
60
|
*/
|
|
48
61
|
function createDispatchedStore({ load, save, now = Date.now }) {
|
|
@@ -52,15 +65,15 @@ function createDispatchedStore({ load, save, now = Date.now }) {
|
|
|
52
65
|
if (saved && typeof saved === 'object') {
|
|
53
66
|
for (const [id, e] of Object.entries(saved)) {
|
|
54
67
|
if (!e || !Number.isFinite(e.at)) continue;
|
|
55
|
-
entries.set(id,
|
|
68
|
+
entries.set(id, dispatchedEntry(e));
|
|
56
69
|
}
|
|
57
70
|
}
|
|
58
71
|
|
|
59
72
|
const persist = () => save({ version: 1, sessions: Object.fromEntries(entries) });
|
|
60
73
|
|
|
61
|
-
function record(id,
|
|
74
|
+
function record(id, info = {}) {
|
|
62
75
|
entries.delete(id);
|
|
63
|
-
entries.set(id, {
|
|
76
|
+
entries.set(id, dispatchedEntry({ ...info, at: now() }));
|
|
64
77
|
if (entries.size > MAX_DISPATCHED) entries.delete(entries.keys().next().value);
|
|
65
78
|
persist();
|
|
66
79
|
}
|
|
@@ -72,7 +85,17 @@ function createDispatchedStore({ load, save, now = Date.now }) {
|
|
|
72
85
|
return removed;
|
|
73
86
|
}
|
|
74
87
|
|
|
75
|
-
|
|
88
|
+
// Derived, not kept: cck resumes every terminal on restart, and a list kept in memory
|
|
89
|
+
// would lose those sessions.
|
|
90
|
+
function running(isRunning, { parent } = {}) {
|
|
91
|
+
const out = [];
|
|
92
|
+
for (const [session, { at, ...m }] of entries) {
|
|
93
|
+
if (isRunning(session) && (!parent || m.parent === parent)) out.push({ session, ...m, startedAt: at });
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { record, prune, running, get: (id) => entries.get(id) || null, entries: () => entries.entries() };
|
|
76
99
|
}
|
|
77
100
|
|
|
78
101
|
// Names only, no stat or parse: the sweep needs which transcripts exist, not what they hold.
|
|
@@ -163,12 +186,37 @@ async function pruneContextStatus(dir, { known, maxAgeMs, now = Date.now(), max
|
|
|
163
186
|
return removed;
|
|
164
187
|
}
|
|
165
188
|
|
|
189
|
+
// `<dir>/<task list id>.json` maps session id → `{project, updatedAt}`, written by the plugin mod.
|
|
190
|
+
// A file with no sessions left is deleted.
|
|
191
|
+
async function pruneTaskMaps(dir, { known, maxAgeMs, now = Date.now() }) {
|
|
192
|
+
let names;
|
|
193
|
+
try {
|
|
194
|
+
names = await fs.readdir(dir);
|
|
195
|
+
} catch {
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
198
|
+
let removed = 0;
|
|
199
|
+
for (const f of names.filter((n) => n.endsWith('.json'))) {
|
|
200
|
+
const file = path.join(dir, f);
|
|
201
|
+
try {
|
|
202
|
+
const map = new Map(Object.entries(JSON.parse(await fs.readFile(file, 'utf8'))));
|
|
203
|
+
const n = pruneMap(map, (e) => Date.parse(e?.updatedAt), { known, maxAgeMs, now });
|
|
204
|
+
if (!n) continue;
|
|
205
|
+
removed += n;
|
|
206
|
+
if (map.size) await fs.writeFile(file, JSON.stringify(Object.fromEntries(map)));
|
|
207
|
+
else await fs.rm(file, { force: true });
|
|
208
|
+
} catch {}
|
|
209
|
+
}
|
|
210
|
+
return removed;
|
|
211
|
+
}
|
|
212
|
+
|
|
166
213
|
module.exports = {
|
|
167
214
|
pruneMap,
|
|
168
215
|
createDispatchedStore,
|
|
169
216
|
scanTranscripts,
|
|
170
217
|
pruneSessionDirs,
|
|
171
218
|
pruneContextStatus,
|
|
219
|
+
pruneTaskMaps,
|
|
172
220
|
retentionMs,
|
|
173
221
|
GRACE_MS,
|
|
174
222
|
MAX_DISPATCHED,
|
package/lib/scratchpad-dir.js
CHANGED
|
@@ -4,6 +4,15 @@ const path = require('node:path');
|
|
|
4
4
|
const { existsSync, opendirSync } = require('node:fs');
|
|
5
5
|
const { encodeProjectDirName } = require('./claude-dir');
|
|
6
6
|
|
|
7
|
+
// Claude Code's rule, from code.claude.com/docs/en/env-vars (CLAUDE_CODE_TMPDIR) and its 2.1.294
|
|
8
|
+
// macOS, Linux and Windows builds. `overrides` are
|
|
9
|
+
// candidate CLAUDE_CODE_TMPDIR values, first wins; a relative one is skipped.
|
|
10
|
+
function scratchpadRoot({ platform, overrides, tmpdir, uid }) {
|
|
11
|
+
const p = platform === 'win32' ? path.win32 : path.posix;
|
|
12
|
+
const base = overrides.find((v) => typeof v === 'string' && p.isAbsolute(v)) || (platform === 'darwin' ? '/tmp' : tmpdir);
|
|
13
|
+
return p.join(base, platform === 'win32' || uid == null ? 'claude' : `claude-${uid}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
function isEmptyDir(dir) {
|
|
8
17
|
let handle;
|
|
9
18
|
try {
|
|
@@ -16,11 +25,19 @@ function isEmptyDir(dir) {
|
|
|
16
25
|
}
|
|
17
26
|
}
|
|
18
27
|
|
|
19
|
-
function createScratchpadDirResolver({ root, resolveWorktree, exists = existsSync, isEmpty = isEmptyDir }) {
|
|
28
|
+
function createScratchpadDirResolver({ root, resolveWorktree, toLong = (p) => p, exists = existsSync, isEmpty = isEmptyDir }) {
|
|
20
29
|
// A by-project dir that holds an entry always wins, so that answer is kept and never probed again.
|
|
21
30
|
const settled = new Map();
|
|
31
|
+
// Long form per root, not per session: the recorded dir is <root>/<project>/<id>/scratchpad.
|
|
32
|
+
const longRoots = new Map();
|
|
22
33
|
|
|
23
34
|
return function getScratchpadDir(id, meta) {
|
|
35
|
+
const recorded = meta.scratchpadDir;
|
|
36
|
+
if (recorded) {
|
|
37
|
+
const root = path.dirname(path.dirname(path.dirname(recorded)));
|
|
38
|
+
if (!longRoots.has(root)) longRoots.set(root, toLong(root));
|
|
39
|
+
return longRoots.get(root) + recorded.slice(root.length);
|
|
40
|
+
}
|
|
24
41
|
if (!meta.jsonlPath) return null;
|
|
25
42
|
const byProject = path.join(root, path.basename(path.dirname(meta.jsonlPath)), id, 'scratchpad');
|
|
26
43
|
if (settled.get(id) === byProject) return byProject;
|
|
@@ -38,4 +55,4 @@ function createScratchpadDirResolver({ root, resolveWorktree, exists = existsSyn
|
|
|
38
55
|
};
|
|
39
56
|
}
|
|
40
57
|
|
|
41
|
-
module.exports = { createScratchpadDirResolver };
|
|
58
|
+
module.exports = { createScratchpadDirResolver, scratchpadRoot };
|
package/lib/session-cache.js
CHANGED
|
@@ -4,7 +4,7 @@ const { exportParentVerdicts, importParentVerdicts, parentVerdictsDirty } = requ
|
|
|
4
4
|
const { exportTaskCounts, importTaskCounts, taskCountsDirty } = require('./task-counts');
|
|
5
5
|
|
|
6
6
|
// Bump when an entry's shape or meaning changes: a file with another version is ignored.
|
|
7
|
-
const VERSION =
|
|
7
|
+
const VERSION = 7;
|
|
8
8
|
// A cache this large is not one cck wrote for a normal set of transcripts. Rebuild it cold.
|
|
9
9
|
const MAX_BYTES = 8 * 1024 * 1024;
|
|
10
10
|
|