claude-code-kanban 7.5.0 → 7.6.1
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 +5 -0
- package/lib/panes.js +16 -2
- package/lib/parsers.js +27 -2
- package/lib/retention.js +29 -6
- package/lib/session-events.js +154 -42
- package/lib/show.js +385 -0
- package/lib/terminal-client.js +14 -1
- package/lib/terminal-host.js +2 -1
- package/lib/terminal.js +27 -7
- package/package.json +1 -1
- package/plugin/plugins/claude-code-kanban/.claude-plugin/plugin.json +1 -1
- package/plugin/plugins/claude-code-kanban/hooks/doorbell.ts +66 -10
- package/plugin/plugins/claude-code-kanban/hooks/index.ts +2 -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 +2 -0
- 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 +69 -10
- package/plugin/plugins/claude-code-kanban/tests/show.test.ts +137 -0
- package/public/app.js +1149 -108
- package/public/index.html +7 -6
- package/public/style.css +770 -9
- package/public/terminal-frame.js +0 -2
- package/public/tree-highlight.js +137 -15
- package/server.js +71 -24
- package/lib/dispatch.js +0 -32
package/public/terminal-frame.js
CHANGED
package/public/tree-highlight.js
CHANGED
|
@@ -11,7 +11,8 @@ const treeHighlight = (() => {
|
|
|
11
11
|
String.raw`^([+-](?=[ \t│├└▼]|$))?([ \t]*(?:[│├└─▼↓→ \t]*[│├└─▼↓→][ \t]*)?)(${NAME})?(.*)$`,
|
|
12
12
|
'su',
|
|
13
13
|
);
|
|
14
|
-
const
|
|
14
|
+
const FRAME = new RegExp(String.raw`^( *)(~.*?|${NAME}(?:\([^()]*\))?)( \*)?(?:( {2,})(\S(?:.*\S)?))?( *)$`, 'u');
|
|
15
|
+
const LOC = /^[^\s:]+:\d+(?::\d+)?$/;
|
|
15
16
|
|
|
16
17
|
function escapeText(s) {
|
|
17
18
|
return s
|
|
@@ -36,25 +37,61 @@ const treeHighlight = (() => {
|
|
|
36
37
|
return false;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
40
|
+
// The notation of skills/show/references/explain.md: frames indented by spaces, one name or
|
|
41
|
+
// call each, an optional ` *` focus mark, a note after 2+ spaces, `~` folds, and `+`/`-` in
|
|
42
|
+
// column 1 for a change. Prose and pseudocode have single-spaced words, so they never parse.
|
|
43
|
+
function parseStack(text) {
|
|
44
|
+
const lines = text.split('\n');
|
|
45
|
+
const diff = lines.some((l) => /^[+-]/.test(l));
|
|
46
|
+
const rows = [];
|
|
47
|
+
let roots = [];
|
|
48
|
+
let stack = [];
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
if (!line.trim()) {
|
|
51
|
+
rows.push({ blank: true, text: line });
|
|
52
|
+
roots = [];
|
|
53
|
+
stack = [];
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const marker = diff ? line[0] : '';
|
|
57
|
+
if (diff && !/[ +-]/.test(marker)) return null;
|
|
58
|
+
const m = line.slice(marker.length).match(FRAME);
|
|
59
|
+
if (!m) return null;
|
|
60
|
+
const [, ind, label, star = '', gap = '', note = '', tail] = m;
|
|
61
|
+
while (stack.length && stack[stack.length - 1].indent >= ind.length) stack.pop();
|
|
62
|
+
const parent = stack[stack.length - 1] || null;
|
|
63
|
+
const sibs = parent ? parent.kids : roots;
|
|
64
|
+
const row = {
|
|
65
|
+
id: rows.length,
|
|
66
|
+
marker,
|
|
67
|
+
indent: ind.length,
|
|
68
|
+
parent,
|
|
69
|
+
sibs,
|
|
70
|
+
label,
|
|
71
|
+
fold: label.startsWith('~'),
|
|
72
|
+
star,
|
|
73
|
+
gap,
|
|
74
|
+
note,
|
|
75
|
+
tail,
|
|
76
|
+
kids: [],
|
|
77
|
+
};
|
|
78
|
+
sibs.push(row);
|
|
79
|
+
stack.push(row);
|
|
80
|
+
rows.push(row);
|
|
50
81
|
}
|
|
51
|
-
return
|
|
82
|
+
return rows;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// An untagged call tree: 3 frames at least, on 2 indent levels at least.
|
|
86
|
+
function looksLikeIndentTree(text) {
|
|
87
|
+
const frames = parseStack(text)?.filter((r) => !r.blank) || [];
|
|
88
|
+
return frames.length >= 3 && new Set(frames.map((r) => r.indent)).size >= 2;
|
|
52
89
|
}
|
|
53
90
|
|
|
54
91
|
function wantsTree(lang, text) {
|
|
55
92
|
const tag = (lang || '').trim().toLowerCase();
|
|
56
93
|
if (TREE_TAGS.has(tag)) return true;
|
|
57
|
-
return PLAIN_TAGS.has(tag) && (looksLikeTree(text) || looksLikeIndentTree(text
|
|
94
|
+
return PLAIN_TAGS.has(tag) && (looksLikeTree(text) || looksLikeIndentTree(text));
|
|
58
95
|
}
|
|
59
96
|
|
|
60
97
|
function highlightLine(line) {
|
|
@@ -68,7 +105,92 @@ const treeHighlight = (() => {
|
|
|
68
105
|
return text.split('\n').map(highlightLine).join('\n');
|
|
69
106
|
}
|
|
70
107
|
|
|
71
|
-
|
|
108
|
+
const isLast = (row) => row.sibs[row.sibs.length - 1] === row;
|
|
109
|
+
|
|
110
|
+
function guideCells(row) {
|
|
111
|
+
const g = Array(row.indent).fill('');
|
|
112
|
+
if (row.parent) {
|
|
113
|
+
for (let a = row.parent; a.parent; a = a.parent) if (!isLast(a)) g[a.parent.indent] = 'v';
|
|
114
|
+
g[row.parent.indent] = isLast(row) ? 'l' : 't';
|
|
115
|
+
if (row.parent.indent + 1 < row.indent) g[row.parent.indent + 1] = 'h';
|
|
116
|
+
}
|
|
117
|
+
return g.map((c) => (c ? `<span class="ct-g ct-${c}"> </span>` : ' ')).join('');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function renderRow(row) {
|
|
121
|
+
if (row.blank) return `<span class="ct-row">${row.text}</span>`;
|
|
122
|
+
const cls = ['ct-row'];
|
|
123
|
+
if (row.star) cls.push('ct-focus');
|
|
124
|
+
if (row.marker === '+') cls.push('ct-add');
|
|
125
|
+
if (row.marker === '-') cls.push('ct-del');
|
|
126
|
+
const above = [];
|
|
127
|
+
for (let a = row.parent; a; a = a.parent) above.push(a);
|
|
128
|
+
if (above.some((a) => !a.open)) cls.push('ct-hidden');
|
|
129
|
+
if (above.some((a) => a.fold)) cls.push('ct-lib');
|
|
130
|
+
let attrs = above.length ? ` data-ct-in="${above.map((a) => a.id).join(' ')}"` : '';
|
|
131
|
+
let label;
|
|
132
|
+
if (row.fold) {
|
|
133
|
+
cls.push('ct-fold');
|
|
134
|
+
label = `<span class="ct-tilde">~</span>${escapeText(row.label.slice(1))}`;
|
|
135
|
+
} else {
|
|
136
|
+
label = span('ct-name', row.label);
|
|
137
|
+
}
|
|
138
|
+
if (row.kids.length) {
|
|
139
|
+
cls.push('ct-has');
|
|
140
|
+
attrs += ` data-ct="${row.id}" role="button" tabindex="0" aria-expanded="${row.open}"`;
|
|
141
|
+
}
|
|
142
|
+
return (
|
|
143
|
+
`<span class="${cls.join(' ')}"${attrs}>` +
|
|
144
|
+
span('ct-m', row.marker) +
|
|
145
|
+
guideCells(row) +
|
|
146
|
+
label +
|
|
147
|
+
span('ct-star', row.star) +
|
|
148
|
+
row.gap +
|
|
149
|
+
span(LOC.test(row.note) ? 'ct-loc' : 'ct-note', row.note) +
|
|
150
|
+
row.tail +
|
|
151
|
+
'</span>'
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Rows are blocks, so the newlines between them sit in hidden spans: the text stays
|
|
156
|
+
// byte-identical and the rows draw no blank lines.
|
|
157
|
+
function renderTree(text) {
|
|
158
|
+
const rows = parseStack(text);
|
|
159
|
+
if (!rows) return `<pre><code class="hljs language-tree">${highlightTree(text)}</code></pre>`;
|
|
160
|
+
for (let i = rows.length - 1; i >= 0; i--) {
|
|
161
|
+
const r = rows[i];
|
|
162
|
+
if (r.parent && (r.star || r.marker === '+' || r.marker === '-' || r.holdsPoint)) r.parent.holdsPoint = true;
|
|
163
|
+
}
|
|
164
|
+
for (const r of rows) r.open = !r.fold || !!r.holdsPoint;
|
|
165
|
+
return `<pre><code class="hljs ct">${rows.map(renderRow).join('<span class="ct-nl">\n</span>')}</code></pre>`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function toggleFold(fold) {
|
|
169
|
+
fold.setAttribute('aria-expanded', String(fold.getAttribute('aria-expanded') !== 'true'));
|
|
170
|
+
const code = fold.closest('code');
|
|
171
|
+
const closed = new Set();
|
|
172
|
+
for (const f of code.querySelectorAll('.ct-has[aria-expanded="false"]')) closed.add(f.dataset.ct);
|
|
173
|
+
for (const r of code.querySelectorAll('[data-ct-in]')) {
|
|
174
|
+
r.classList.toggle(
|
|
175
|
+
'ct-hidden',
|
|
176
|
+
r.dataset.ctIn.split(' ').some((id) => closed.has(id)),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// A note in the LOC form → { path, line }, else null.
|
|
182
|
+
function parseLoc(text) {
|
|
183
|
+
if (!LOC.test(text)) return null;
|
|
184
|
+
const [path, line] = text.split(':');
|
|
185
|
+
return { path, line: Math.max(1, Number(line)) };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// A trailing newline does not start a line.
|
|
189
|
+
function lineCount(text) {
|
|
190
|
+
return Math.max(1, text.split('\n').length - (text.endsWith('\n') ? 1 : 0));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { wantsTree, highlightTree, renderTree, toggleFold, parseLoc, lineCount };
|
|
72
194
|
})();
|
|
73
195
|
|
|
74
196
|
if (typeof module === 'object' && module.exports) module.exports = treeHighlight;
|
package/server.js
CHANGED
|
@@ -28,6 +28,7 @@ const {
|
|
|
28
28
|
modelDisplayName,
|
|
29
29
|
buildSessionDigest,
|
|
30
30
|
readCompactSummaries,
|
|
31
|
+
fillCompactSummaries,
|
|
31
32
|
readArtifactLinks,
|
|
32
33
|
readScratchpadCreations,
|
|
33
34
|
extractPromptFromTranscript,
|
|
@@ -48,9 +49,9 @@ const { buildDecision, decisionFileName, isDecisionFile, approvalsFrom, boardRef
|
|
|
48
49
|
const { getClaudeDir, getArgValue, storageNamespace, isDefaultClaudeDir } = require('./lib/claude-dir');
|
|
49
50
|
const { readTerminalConfig } = require('./lib/terminal');
|
|
50
51
|
const { createTerminalClient } = require('./lib/terminal-client');
|
|
52
|
+
const { createShowStore, mountShowRoutes, showBodyParser, SHOW_PATH } = require('./lib/show');
|
|
51
53
|
const { readLiveSessions, isPidAlive, isSessionLive } = require('./lib/live-sessions');
|
|
52
54
|
const { createProcStats } = require('./lib/proc-stats');
|
|
53
|
-
const { createDispatchRegistry } = require('./lib/dispatch');
|
|
54
55
|
const { createGroupStore, isGroupName, suggestGroupName } = require('./lib/dispatch-groups');
|
|
55
56
|
const { createDispatchedStore, scanTranscripts, pruneSessionDirs, pruneContextStatus, pruneTaskMaps, retentionMs } = require('./lib/retention');
|
|
56
57
|
const { freshRateLimits } = require('./lib/rate-limits');
|
|
@@ -660,6 +661,7 @@ app.param('name', (_req, res, next, val) => {
|
|
|
660
661
|
next();
|
|
661
662
|
});
|
|
662
663
|
|
|
664
|
+
app.use(SHOW_PATH, showBodyParser());
|
|
663
665
|
// Parse JSON bodies. A review of 50 comments at the field caps is about 450 kB.
|
|
664
666
|
app.use(express.json({ limit: '1mb' }));
|
|
665
667
|
// #endregion
|
|
@@ -3045,7 +3047,10 @@ async function sendSessionMessages(req, res) {
|
|
|
3045
3047
|
hasMore = messages.length > limit;
|
|
3046
3048
|
if (hasMore) messages = messages.slice(-limit);
|
|
3047
3049
|
}
|
|
3048
|
-
const
|
|
3050
|
+
const compactedMsgs = messages.filter(m => m.systemLabel === 'Compacted');
|
|
3051
|
+
const compactPromise = compactedMsgs.some(m => !m.compactSummary)
|
|
3052
|
+
? cachedByMtime(compactSummaryCache, jsonlPath, jsonlPath, () => readCompactSummaries(jsonlPath), [])
|
|
3053
|
+
: null;
|
|
3049
3054
|
const agentMessages = messages.filter(m => m.tool === 'Agent' && m.toolUseId);
|
|
3050
3055
|
if (agentMessages.length) {
|
|
3051
3056
|
const progressMap = await getProgressMap(jsonlPath);
|
|
@@ -3099,16 +3104,7 @@ async function sendSessionMessages(req, res) {
|
|
|
3099
3104
|
}
|
|
3100
3105
|
}
|
|
3101
3106
|
}
|
|
3102
|
-
|
|
3103
|
-
// Match compaction messages to summaries by chronological order
|
|
3104
|
-
const compactedMsgs = messages
|
|
3105
|
-
.filter(m => m.systemLabel === 'Compacted')
|
|
3106
|
-
.sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
|
|
3107
|
-
for (let i = 0; i < compactedMsgs.length; i++) {
|
|
3108
|
-
if (i < compactSummaries.length) {
|
|
3109
|
-
compactedMsgs[i].compactSummary = compactSummaries[i].summary;
|
|
3110
|
-
}
|
|
3111
|
-
}
|
|
3107
|
+
if (compactPromise) fillCompactSummaries(compactedMsgs, await compactPromise);
|
|
3112
3108
|
// The client keeps needing toolUseId when it builds a follow-up URL from it:
|
|
3113
3109
|
// lazy-fetching a truncated tool result, or fetching each tool-result image.
|
|
3114
3110
|
const clientNeedsToolUseId = (msg) => msg.toolResultTruncated || msg.toolResultImageCount;
|
|
@@ -3321,6 +3317,16 @@ function resolveSessionFolder(id) {
|
|
|
3321
3317
|
return meta ? meta.project || meta.cwd || null : null;
|
|
3322
3318
|
}
|
|
3323
3319
|
|
|
3320
|
+
// The host saves its terminals; cck adds the show map (terminal id → session id) beside them.
|
|
3321
|
+
const terminalsFile = jsonFile(TERMINALS_FILE);
|
|
3322
|
+
let savedTerminals = terminalsFile.load() || {};
|
|
3323
|
+
|
|
3324
|
+
function saveTerminals() {
|
|
3325
|
+
const kept = new Set(Object.values(savedTerminals.terminalIds || {}));
|
|
3326
|
+
const showSessions = show.prune((id) => kept.has(id) || terminal.hasTerminal(id));
|
|
3327
|
+
terminalsFile.save({ ...savedTerminals, showSessions });
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3324
3330
|
const terminal = createTerminalClient({
|
|
3325
3331
|
config: readTerminalConfig({ getArgValue }),
|
|
3326
3332
|
net,
|
|
@@ -3328,14 +3334,18 @@ const terminal = createTerminalClient({
|
|
|
3328
3334
|
isDefaultDir: isDefaultClaudeDir(CLAUDE_DIR),
|
|
3329
3335
|
sessionsDir: SESSIONS_DIR,
|
|
3330
3336
|
token: process.env.CCK_TERMINAL_TOKEN,
|
|
3331
|
-
|
|
3337
|
+
load: terminalsFile.load,
|
|
3338
|
+
save: (data) => {
|
|
3339
|
+
savedTerminals = data;
|
|
3340
|
+
saveTerminals();
|
|
3341
|
+
},
|
|
3332
3342
|
// The project, not the last cwd: `claude --resume` finds a session under the
|
|
3333
3343
|
// project dir it started in, and cwd drifts into subdirectories.
|
|
3334
3344
|
resolveCwd: resolveSessionFolder,
|
|
3335
3345
|
isAllowedFolder,
|
|
3336
3346
|
onChange: () => broadcast({ type: 'terminals-update', ids: terminal.ids() }),
|
|
3337
3347
|
onExit: (id) => {
|
|
3338
|
-
if (
|
|
3348
|
+
if (dispatched.get(id)) broadcast({ type: 'dispatch-update' });
|
|
3339
3349
|
},
|
|
3340
3350
|
});
|
|
3341
3351
|
|
|
@@ -3396,6 +3406,25 @@ app.delete('/api/terminals/:id', terminalRoute(async (req, res) => {
|
|
|
3396
3406
|
res.status(204).end();
|
|
3397
3407
|
}));
|
|
3398
3408
|
|
|
3409
|
+
const show = createShowStore({
|
|
3410
|
+
load: () => savedTerminals.showSessions,
|
|
3411
|
+
onChange: saveTerminals,
|
|
3412
|
+
onPosted: (e) => broadcast({ type: 'show:posted', ...e }),
|
|
3413
|
+
startedSession: terminal.sessionOfTerminal,
|
|
3414
|
+
resolveDir: (id) => {
|
|
3415
|
+
const meta = sessionMetaFor(id);
|
|
3416
|
+
return meta ? getScratchpadDir(id, meta) : null;
|
|
3417
|
+
},
|
|
3418
|
+
});
|
|
3419
|
+
mountShowRoutes(app, {
|
|
3420
|
+
store: show,
|
|
3421
|
+
authorized: (token) => terminal.authorized(token),
|
|
3422
|
+
hasTerminal: (id) => terminal.hasTerminal(id),
|
|
3423
|
+
readPreviewFile,
|
|
3424
|
+
broadcast,
|
|
3425
|
+
onRemoved: (sessionId, paths) => panes.removeShow(sessionId, paths),
|
|
3426
|
+
});
|
|
3427
|
+
|
|
3399
3428
|
// Served from node_modules, never a CDN: any script on this page can use the token.
|
|
3400
3429
|
const XTERM_FILES = {
|
|
3401
3430
|
'xterm.js': '@xterm/xterm/lib/xterm.js',
|
|
@@ -3417,8 +3446,6 @@ app.get('/vendor/xterm/:file', (req, res) => {
|
|
|
3417
3446
|
// #region DISPATCH
|
|
3418
3447
|
const dispatched = createDispatchedStore(jsonFile(DISPATCHED_FILE));
|
|
3419
3448
|
|
|
3420
|
-
const dispatches = createDispatchRegistry();
|
|
3421
|
-
|
|
3422
3449
|
const dispatchGroups = createGroupStore({
|
|
3423
3450
|
...jsonFile(DISPATCH_GROUPS_FILE),
|
|
3424
3451
|
isAlive: (id) => terminal.isRunning(id) || isSessionLive(loadLiveSessions(), id),
|
|
@@ -3454,8 +3481,7 @@ app.post('/api/dispatch', terminalRoute(async (req, res) => {
|
|
|
3454
3481
|
}
|
|
3455
3482
|
const started = await terminal.startNew({ cwd, name, model, worktree, taskList, prompt: spec.trim(), extraArgs: claudeArgs });
|
|
3456
3483
|
if (started.error) return res.status(started.status).json({ error: started.error });
|
|
3457
|
-
|
|
3458
|
-
dispatched.record(started.id, parent);
|
|
3484
|
+
dispatched.record(started.id, { parent, cwd: started.cwd, name, group, worktree });
|
|
3459
3485
|
// The starter stays where it is: moving it would jump it under the user.
|
|
3460
3486
|
if (group) dispatchGroups.join(group, [started.id]);
|
|
3461
3487
|
broadcast({ type: 'dispatch-update' });
|
|
@@ -3465,7 +3491,7 @@ app.post('/api/dispatch', terminalRoute(async (req, res) => {
|
|
|
3465
3491
|
app.get('/api/dispatch', (req, res) => {
|
|
3466
3492
|
res.setHeader('Cache-Control', 'no-store');
|
|
3467
3493
|
const parent = typeof req.query.parent === 'string' && req.query.parent ? req.query.parent : null;
|
|
3468
|
-
res.json({ running:
|
|
3494
|
+
res.json({ running: dispatched.running(terminal.isRunning, { parent }) });
|
|
3469
3495
|
});
|
|
3470
3496
|
// #endregion
|
|
3471
3497
|
|
|
@@ -3524,7 +3550,8 @@ const {
|
|
|
3524
3550
|
formatReviewSubmitted,
|
|
3525
3551
|
formatTaskMoved,
|
|
3526
3552
|
handleSessionEvents,
|
|
3527
|
-
|
|
3553
|
+
hasDoorbell,
|
|
3554
|
+
configureSessionEvents,
|
|
3528
3555
|
} = require('./lib/session-events');
|
|
3529
3556
|
app.get('/api/sessions/:sessionId/events', handleSessionEvents);
|
|
3530
3557
|
|
|
@@ -4092,11 +4119,11 @@ app.get('/api/panes/:sessionId', (req, res) => {
|
|
|
4092
4119
|
|
|
4093
4120
|
app.post('/api/panes/:sessionId', async (req, res) => {
|
|
4094
4121
|
try {
|
|
4095
|
-
const { target, base, title, kind } = req.body || {};
|
|
4122
|
+
const { target, base, title, kind, show } = req.body || {};
|
|
4096
4123
|
if (typeof target !== 'string' || !target) throw previewError(400, 'target is required', 'bad_target');
|
|
4097
4124
|
// A message target is a session log message id, which the board resolves when it shows the pane.
|
|
4098
4125
|
const resolved = kind === 'message' ? { kind, target } : await resolvePaneTarget(target, base);
|
|
4099
|
-
res.json(panes.add(req.params.sessionId, { ...resolved, title }));
|
|
4126
|
+
res.json(panes.add(req.params.sessionId, { ...resolved, title, show: show === true }));
|
|
4100
4127
|
} catch (error) {
|
|
4101
4128
|
paneRouteError(res, error, 'POST /api/panes');
|
|
4102
4129
|
}
|
|
@@ -4159,6 +4186,23 @@ app.delete('/api/panes/:sessionId/:paneId', (req, res) => {
|
|
|
4159
4186
|
// carries one line, and the file is what each route points at.
|
|
4160
4187
|
const REVIEW_DIR = path.join(CCK_DIR, 'reviews');
|
|
4161
4188
|
const REVIEW_KIND_RE = /^[a-z]{1,20}$/;
|
|
4189
|
+
|
|
4190
|
+
// `<ts>.pending` holds the doorbell line of a review the mod has not acked yet.
|
|
4191
|
+
configureSessionEvents({
|
|
4192
|
+
onDelivered: (entry) => fs.unlink(entry.marker).catch(() => {}),
|
|
4193
|
+
restore: async (sessionId) => {
|
|
4194
|
+
if (!isUUID(sessionId)) return [];
|
|
4195
|
+
const dir = path.join(REVIEW_DIR, sessionId);
|
|
4196
|
+
const names = (await fs.readdir(dir).catch(() => [])).filter((n) => n.endsWith('.pending')).sort();
|
|
4197
|
+
const lines = [];
|
|
4198
|
+
for (const name of names) {
|
|
4199
|
+
const marker = path.join(dir, name);
|
|
4200
|
+
const text = await fs.readFile(marker, 'utf8').catch(() => '');
|
|
4201
|
+
if (text) lines.push({ text, marker });
|
|
4202
|
+
}
|
|
4203
|
+
return lines;
|
|
4204
|
+
},
|
|
4205
|
+
});
|
|
4162
4206
|
const REVIEW_MAX_COMMENTS = 50;
|
|
4163
4207
|
const REVIEW_MAX_CHARS = 4000;
|
|
4164
4208
|
|
|
@@ -4227,8 +4271,11 @@ app.post('/api/sessions/:sessionId/review', async (req, res) => {
|
|
|
4227
4271
|
await fs.writeFile(file, markdown);
|
|
4228
4272
|
|
|
4229
4273
|
let delivered = null;
|
|
4230
|
-
if (boardEventsEnabled() &&
|
|
4231
|
-
|
|
4274
|
+
if (boardEventsEnabled() && hasDoorbell(sessionId)) {
|
|
4275
|
+
const line = formatReviewSubmitted(src.items.length, src.label, file);
|
|
4276
|
+
const marker = file.replace(/\.md$/, '.pending');
|
|
4277
|
+
await fs.writeFile(marker, line);
|
|
4278
|
+
enqueueSessionEvent(sessionId, line, { marker });
|
|
4232
4279
|
delivered = 'doorbell';
|
|
4233
4280
|
} else if (
|
|
4234
4281
|
terminal.authorized(req.get('x-terminal-token')) &&
|
package/lib/dispatch.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
// Dispatch: a Claude Code session one session started in cck's terminal. In memory on
|
|
2
|
-
// purpose: an entry lives as long as its PTY, which dies with the server.
|
|
3
|
-
|
|
4
|
-
function createDispatchRegistry({ now = Date.now } = {}) {
|
|
5
|
-
const entries = new Map();
|
|
6
|
-
|
|
7
|
-
function add({ session, parent, cwd, name, group, worktree }) {
|
|
8
|
-
entries.set(session, {
|
|
9
|
-
session,
|
|
10
|
-
parent: parent || null,
|
|
11
|
-
cwd,
|
|
12
|
-
name: name || null,
|
|
13
|
-
group: group || null,
|
|
14
|
-
worktree: worktree || null,
|
|
15
|
-
startedAt: now(),
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function list({ parent } = {}) {
|
|
20
|
-
const out = [...entries.values()];
|
|
21
|
-
return parent ? out.filter((e) => e.parent === parent) : out;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return {
|
|
25
|
-
add,
|
|
26
|
-
list,
|
|
27
|
-
has: (session) => entries.has(session),
|
|
28
|
-
remove: (session) => entries.delete(session),
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
module.exports = { createDispatchRegistry };
|