claude-code-kanban 7.16.0 → 7.17.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/package.json +2 -1
- package/public/app.js +81 -22
- package/public/style.css +11 -2
- package/public/terminal-frame.js +52 -1
- package/public/terminal.html +1 -0
- package/public/vendor/claude-hub-sdk.js +1 -1
- package/server.js +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-kanban",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.17.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",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"@xterm/addon-fit": "0.11.0",
|
|
40
40
|
"@xterm/addon-serialize": "0.14.0",
|
|
41
41
|
"@xterm/addon-unicode11": "0.9.0",
|
|
42
|
+
"@xterm/addon-web-links": "0.12.0",
|
|
42
43
|
"@xterm/addon-webgl": "0.19.0",
|
|
43
44
|
"@xterm/headless": "6.0.0",
|
|
44
45
|
"@xterm/xterm": "6.0.0",
|
package/public/app.js
CHANGED
|
@@ -7503,7 +7503,13 @@ function _findOrphanedKeys(known) {
|
|
|
7503
7503
|
const orphaned = [];
|
|
7504
7504
|
for (const id of pinnedSessionIds) if (!known.has(id)) orphaned.push(`__pinned__${id}`);
|
|
7505
7505
|
for (const id of stickySessionIds) if (!known.has(id)) orphaned.push(`__sticky__${id}`);
|
|
7506
|
-
const sessionKeyPrefixes = [
|
|
7506
|
+
const sessionKeyPrefixes = [
|
|
7507
|
+
PINNED_MESSAGES_PREFIX,
|
|
7508
|
+
PAD_EMOJI_PREFIX,
|
|
7509
|
+
PREVIEW_STORAGE_PREFIX,
|
|
7510
|
+
PAD_LINKED_PREFIX,
|
|
7511
|
+
DROPPED_ARTIFACTS_PREFIX,
|
|
7512
|
+
];
|
|
7507
7513
|
for (const key of store.keys()) {
|
|
7508
7514
|
const pad = _parsePadKey(key);
|
|
7509
7515
|
if (pad?.kind === 'group') {
|
|
@@ -9173,7 +9179,7 @@ function bindPreviewRelativeLinks(bodyEl, baseOf = () => currentPreviewPath) {
|
|
|
9173
9179
|
if (isUrlRef(href)) return;
|
|
9174
9180
|
const cleanHref = href.replace(/#.*$/, '');
|
|
9175
9181
|
e.preventDefault();
|
|
9176
|
-
openPreviewByPath(cleanHref,
|
|
9182
|
+
openPreviewByPath(cleanHref, baseOf(a), openFileInEditor);
|
|
9177
9183
|
});
|
|
9178
9184
|
bodyEl.dataset.relLinkBound = '1';
|
|
9179
9185
|
}
|
|
@@ -9675,8 +9681,34 @@ function artifactLabel(a) {
|
|
|
9675
9681
|
return a.path ? pathBasename(a.path).replace(/\.html?$/i, '') : 'Artifact';
|
|
9676
9682
|
}
|
|
9677
9683
|
|
|
9684
|
+
// The list comes from the transcript on every read, so a dropped artifact is kept out by URL.
|
|
9685
|
+
const DROPPED_ARTIFACTS_PREFIX = 'artifacts-dropped-';
|
|
9686
|
+
|
|
9687
|
+
function droppedArtifacts(sessionId) {
|
|
9688
|
+
return new Set(readStoredList(DROPPED_ARTIFACTS_PREFIX + sessionId));
|
|
9689
|
+
}
|
|
9690
|
+
|
|
9691
|
+
function setArtifactDropped(sessionId, url, dropped) {
|
|
9692
|
+
const urls = droppedArtifacts(sessionId);
|
|
9693
|
+
if (dropped) urls.add(url);
|
|
9694
|
+
else urls.delete(url);
|
|
9695
|
+
if (urls.size) store.writeJson(DROPPED_ARTIFACTS_PREFIX + sessionId, [...urls]);
|
|
9696
|
+
else store.removeItem(DROPPED_ARTIFACTS_PREFIX + sessionId);
|
|
9697
|
+
artifactsSection.repaint(sessionId);
|
|
9698
|
+
}
|
|
9699
|
+
|
|
9700
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in HTML
|
|
9701
|
+
function dropArtifact(sessionId, url) {
|
|
9702
|
+
setArtifactDropped(sessionId, url, true);
|
|
9703
|
+
showToast('Artifact dropped from the list', 'info', {
|
|
9704
|
+
label: 'Undo',
|
|
9705
|
+
onClick: () => setArtifactDropped(sessionId, url, false),
|
|
9706
|
+
});
|
|
9707
|
+
}
|
|
9708
|
+
|
|
9678
9709
|
function artifactsInnerHtml(sessionId) {
|
|
9679
|
-
const
|
|
9710
|
+
const dropped = droppedArtifacts(sessionId);
|
|
9711
|
+
const list = artifactsSection.get(sessionId).filter((a) => !dropped.has(a.url));
|
|
9680
9712
|
if (!list.length) return '';
|
|
9681
9713
|
const items = list
|
|
9682
9714
|
.map(
|
|
@@ -9684,6 +9716,7 @@ function artifactsInnerHtml(sessionId) {
|
|
|
9684
9716
|
<a class="artifact-link" href="${escapeHtml(a.url)}" target="_blank" rel="noopener" title="${escapeHtml(a.url)}">${escapeHtml(artifactLabel(a))}</a>
|
|
9685
9717
|
<span class="row-actions artifact-actions">
|
|
9686
9718
|
<button type="button" onclick="copyWithFeedback('${escAttrJs(a.url)}', this)" title="Copy link" aria-label="Copy artifact link">${ICON_COPY}</button>
|
|
9719
|
+
<button type="button" class="artifact-remove" onclick="dropArtifact('${escAttrJs(sessionId)}','${escAttrJs(a.url)}')" title="Drop from the list" aria-label="Drop ${escapeHtml(artifactLabel(a))} from the list">×</button>
|
|
9687
9720
|
</span>
|
|
9688
9721
|
</li>`,
|
|
9689
9722
|
)
|
|
@@ -11055,9 +11088,14 @@ function toRelativeIfUnder(filePath, baseDir) {
|
|
|
11055
11088
|
return fp.slice(bd.length + 1);
|
|
11056
11089
|
}
|
|
11057
11090
|
|
|
11091
|
+
// DOMPurify's default list plus `file:` and drive paths (`C:/x` reads as a scheme), so a link to a
|
|
11092
|
+
// local file survives and opens in the preview.
|
|
11093
|
+
const MARKDOWN_URI =
|
|
11094
|
+
/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix|file):|[a-z]:[\\/]|[^a-z]|[a-z+.-]+(?:[^a-z+.\-:]|$))/i;
|
|
11095
|
+
|
|
11058
11096
|
function renderMarkdown(text) {
|
|
11059
11097
|
if (typeof DOMPurify !== 'undefined' && typeof marked !== 'undefined') {
|
|
11060
|
-
return DOMPurify.sanitize(marked.parse(text));
|
|
11098
|
+
return DOMPurify.sanitize(marked.parse(text), { ALLOWED_URI_REGEXP: MARKDOWN_URI });
|
|
11061
11099
|
}
|
|
11062
11100
|
return `<pre style="white-space:pre-wrap;margin:0;">${escapeHtml(text)}</pre>`;
|
|
11063
11101
|
}
|
|
@@ -15377,6 +15415,12 @@ function terminalInitOptions(themeOptions) {
|
|
|
15377
15415
|
};
|
|
15378
15416
|
}
|
|
15379
15417
|
|
|
15418
|
+
function openTerminalLink(href) {
|
|
15419
|
+
if (typeof href === 'string' && isLocalHref(href)) return openLocalHref(href);
|
|
15420
|
+
const url = linkUrl(href);
|
|
15421
|
+
if (url) hub.openExternal(url);
|
|
15422
|
+
}
|
|
15423
|
+
|
|
15380
15424
|
function onTerminalFrameMessage(t, m) {
|
|
15381
15425
|
const socket = termState.socket;
|
|
15382
15426
|
const current = socket && m.socketId === socket.id;
|
|
@@ -15396,6 +15440,7 @@ function onTerminalFrameMessage(t, m) {
|
|
|
15396
15440
|
reconnectTerminal();
|
|
15397
15441
|
}
|
|
15398
15442
|
} else if (t === 'key') replayKey(m, document.getElementById('terminal-key-proxy'));
|
|
15443
|
+
else if (t === 'link') openTerminalLink(m.href);
|
|
15399
15444
|
// Chrome does not always fire focusin on the iframe element when its content takes focus.
|
|
15400
15445
|
else if (t === 'focused') terminalHadFocus = terminalPaneFocused();
|
|
15401
15446
|
else if (t === 'opening') {
|
|
@@ -18092,13 +18137,6 @@ function publishSession() {
|
|
|
18092
18137
|
);
|
|
18093
18138
|
}
|
|
18094
18139
|
|
|
18095
|
-
// The help modal stops click propagation, so the document handler below never sees this link.
|
|
18096
|
-
// The service worker can pair a cached page that lacks the link with this script.
|
|
18097
|
-
document.getElementById('help-docs')?.addEventListener('click', (e) => {
|
|
18098
|
-
e.preventDefault();
|
|
18099
|
-
hub.openExternal(e.currentTarget.href);
|
|
18100
|
-
});
|
|
18101
|
-
|
|
18102
18140
|
function webUrl(href) {
|
|
18103
18141
|
let url;
|
|
18104
18142
|
try {
|
|
@@ -18109,17 +18147,38 @@ function webUrl(href) {
|
|
|
18109
18147
|
return url.protocol === 'http:' || url.protocol === 'https:' ? url : null;
|
|
18110
18148
|
}
|
|
18111
18149
|
|
|
18112
|
-
|
|
18113
|
-
|
|
18114
|
-
|
|
18115
|
-
|
|
18116
|
-
|
|
18117
|
-
|
|
18118
|
-
|
|
18119
|
-
|
|
18120
|
-
|
|
18121
|
-
|
|
18122
|
-
|
|
18150
|
+
function isLocalHref(href) {
|
|
18151
|
+
return /^file:/i.test(href) || (isAbsolutePath(href) && !href.startsWith('//'));
|
|
18152
|
+
}
|
|
18153
|
+
|
|
18154
|
+
function openLocalHref(href) {
|
|
18155
|
+
openPreviewByPath(href.replace(/#.*$/, ''), undefined, openFileInEditor);
|
|
18156
|
+
}
|
|
18157
|
+
|
|
18158
|
+
// Capture, because the modals stop a click's propagation, and stopped here so a link in a message
|
|
18159
|
+
// preview does not also open the row. Under the hub the SDK has already opened a web link
|
|
18160
|
+
// outside (defaultPrevented), and leaves a link to a hub app alone.
|
|
18161
|
+
document.addEventListener(
|
|
18162
|
+
'click',
|
|
18163
|
+
(e) => {
|
|
18164
|
+
if (e.button !== 0) return;
|
|
18165
|
+
const href = e.target.closest?.('a[href]')?.getAttribute('href');
|
|
18166
|
+
if (!href) return;
|
|
18167
|
+
const url = webUrl(href);
|
|
18168
|
+
const external = url && url.origin !== window.location.origin;
|
|
18169
|
+
if (e.defaultPrevented) {
|
|
18170
|
+
if (external) e.stopPropagation();
|
|
18171
|
+
return;
|
|
18172
|
+
}
|
|
18173
|
+
const local = isLocalHref(href);
|
|
18174
|
+
if (!local && (!external || hub.inHub)) return;
|
|
18175
|
+
e.preventDefault();
|
|
18176
|
+
e.stopPropagation();
|
|
18177
|
+
if (local) openLocalHref(href);
|
|
18178
|
+
else hub.openExternal(url.href);
|
|
18179
|
+
},
|
|
18180
|
+
true,
|
|
18181
|
+
);
|
|
18123
18182
|
|
|
18124
18183
|
(function initHubTheme() {
|
|
18125
18184
|
const getTheme = () => (document.body.classList.contains('light') ? 'light' : 'dark');
|
package/public/style.css
CHANGED
|
@@ -7030,13 +7030,15 @@ body.show-resizing iframe {
|
|
|
7030
7030
|
}
|
|
7031
7031
|
|
|
7032
7032
|
/* The × is a glyph, not an svg, so .row-actions button svg does not size it. */
|
|
7033
|
-
.linked-doc-remove
|
|
7033
|
+
.linked-doc-remove,
|
|
7034
|
+
.artifact-remove {
|
|
7034
7035
|
font-family: var(--mono);
|
|
7035
7036
|
font-size: 15px;
|
|
7036
7037
|
line-height: 1;
|
|
7037
7038
|
}
|
|
7038
7039
|
|
|
7039
|
-
.linked-doc-actions .linked-doc-remove:hover
|
|
7040
|
+
.linked-doc-actions .linked-doc-remove:hover,
|
|
7041
|
+
.artifact-actions .artifact-remove:hover {
|
|
7040
7042
|
background: var(--bg-elevated);
|
|
7041
7043
|
border-color: var(--border);
|
|
7042
7044
|
color: var(--text-primary);
|
|
@@ -8134,6 +8136,13 @@ body.light .msg-assistant .msg-text {
|
|
|
8134
8136
|
padding: 8px 16px;
|
|
8135
8137
|
white-space: nowrap;
|
|
8136
8138
|
}
|
|
8139
|
+
|
|
8140
|
+
/* The modal focuses a button itself, and after a mouse click Chrome does not match
|
|
8141
|
+
:focus-visible for that, so Enter would act on a button with no ring. */
|
|
8142
|
+
.confirm-buttons .btn:focus {
|
|
8143
|
+
outline: 2px solid var(--accent);
|
|
8144
|
+
outline-offset: 2px;
|
|
8145
|
+
}
|
|
8137
8146
|
/* #endregion */
|
|
8138
8147
|
|
|
8139
8148
|
/* Reference card, not reading material: it stays out of the modal zoom, uses the app's
|
package/public/terminal-frame.js
CHANGED
|
@@ -48,6 +48,39 @@
|
|
|
48
48
|
return lines.join('\n');
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
const PATH_ROOT = String.raw`(?:[A-Za-z]:[\\/]|\/(?=[\w.@+-]+\/)|\\\\(?=[\w.$-]+\\))`;
|
|
52
|
+
// A quoted path may hold spaces; a bare one ends at the first.
|
|
53
|
+
const PATH_RE = new RegExp(
|
|
54
|
+
[
|
|
55
|
+
String.raw`"(${PATH_ROOT}[^"<>|*?]*[^\s"<>|*?])"`,
|
|
56
|
+
String.raw`'(${PATH_ROOT}[^'<>|*?]*[^\s'<>|*?])'`,
|
|
57
|
+
String.raw`(?<![\w/\\:.~-])${PATH_ROOT}[^\s"'<>|*?${'`'}]*[^\s"'<>|*?${'`'}.,;:!)\]}]`,
|
|
58
|
+
].join('|'),
|
|
59
|
+
'g',
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
function pathsIn(text) {
|
|
63
|
+
return Array.from(text.matchAll(PATH_RE), (m) => {
|
|
64
|
+
const quoted = m[1] ?? m[2];
|
|
65
|
+
const found = quoted ?? m[0];
|
|
66
|
+
return { index: m.index + (quoted ? 1 : 0), text: found, path: found.replace(/(?::\d+){1,2}$/, '') };
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Cell x of each character, because a wide character spans two cells and translateToString counts it once.
|
|
71
|
+
function lineCells(line, cols, reuse) {
|
|
72
|
+
let text = '';
|
|
73
|
+
const xs = [];
|
|
74
|
+
for (let x = 0; x < cols; x++) {
|
|
75
|
+
const cell = line.getCell(x, reuse);
|
|
76
|
+
if (!cell || cell.getWidth() === 0) continue;
|
|
77
|
+
const chars = cell.getChars() || ' ';
|
|
78
|
+
for (let i = 0; i < chars.length; i++) xs.push(x);
|
|
79
|
+
text += chars;
|
|
80
|
+
}
|
|
81
|
+
return { text, xs };
|
|
82
|
+
}
|
|
83
|
+
|
|
51
84
|
function oscColor(hex) {
|
|
52
85
|
const m = /^#([0-9a-f]{6})$/i.exec(hex || '');
|
|
53
86
|
if (!m) return null;
|
|
@@ -147,6 +180,8 @@
|
|
|
147
180
|
scrollback: o.scrollback,
|
|
148
181
|
cursorBlink: true,
|
|
149
182
|
allowProposedApi: true,
|
|
183
|
+
// xterm's default asks with confirm() and opens a window, which the installed hub window blocks.
|
|
184
|
+
linkHandler: { activate: (_e, uri) => up('link', { href: uri }), allowNonHttpProtocols: true },
|
|
150
185
|
...o.themeOptions,
|
|
151
186
|
});
|
|
152
187
|
state.term = term;
|
|
@@ -154,6 +189,22 @@
|
|
|
154
189
|
term.loadAddon(state.fit);
|
|
155
190
|
term.loadAddon(new win.Unicode11Addon.Unicode11Addon());
|
|
156
191
|
term.unicode.activeVersion = '11';
|
|
192
|
+
// Link providers scan a line only when the pointer moves onto it, never on output.
|
|
193
|
+
term.loadAddon(new win.WebLinksAddon.WebLinksAddon((_e, uri) => up('link', { href: uri })));
|
|
194
|
+
const cellReuse = term.buffer.active.getNullCell();
|
|
195
|
+
term.registerLinkProvider({
|
|
196
|
+
provideLinks(y, done) {
|
|
197
|
+
const line = term.buffer.active.getLine(y - 1);
|
|
198
|
+
if (!line || !/[\\/]/.test(line.translateToString(true))) return done(undefined);
|
|
199
|
+
const { text, xs } = lineCells(line, term.cols, cellReuse);
|
|
200
|
+
const links = pathsIn(text).map((p) => ({
|
|
201
|
+
text: p.text,
|
|
202
|
+
range: { start: { x: xs[p.index] + 1, y }, end: { x: xs[p.index + p.text.length - 1] + 1, y } },
|
|
203
|
+
activate: () => up('link', { href: p.path }),
|
|
204
|
+
}));
|
|
205
|
+
done(links.length ? links : undefined);
|
|
206
|
+
},
|
|
207
|
+
});
|
|
157
208
|
term.open(host);
|
|
158
209
|
try {
|
|
159
210
|
const gl = new win.WebglAddon.WebglAddon();
|
|
@@ -346,7 +397,7 @@
|
|
|
346
397
|
}
|
|
347
398
|
|
|
348
399
|
if (typeof module === 'object' && module.exports) {
|
|
349
|
-
module.exports = { createTerminalFrame, claimSig, isClaimed, handsBack, parentOriginOf, tailOf };
|
|
400
|
+
module.exports = { createTerminalFrame, claimSig, isClaimed, handsBack, parentOriginOf, tailOf, pathsIn };
|
|
350
401
|
} else {
|
|
351
402
|
createTerminalFrame(root);
|
|
352
403
|
}
|
package/public/terminal.html
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
<script src="/vendor/xterm/addon-fit.js"></script>
|
|
23
23
|
<script src="/vendor/xterm/addon-webgl.js"></script>
|
|
24
24
|
<script src="/vendor/xterm/addon-unicode11.js"></script>
|
|
25
|
+
<script src="/vendor/xterm/addon-web-links.js"></script>
|
|
25
26
|
<script src="/terminal-frame.js"></script>
|
|
26
27
|
</body>
|
|
27
28
|
</html>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// claude-hub-sdk 1.
|
|
1
|
+
// claude-hub-sdk 1.6.0 (sha256 71a58022516d). Copied by npm run sdk:sync in claude-code-hub. Do not edit.
|
|
2
2
|
// Claude Code Hub SDK: the one rule that names a key press, shared by the hub page, client.js and
|
|
3
3
|
// stub.js. The hub serves it to its page as /sdk/keys.js; mount() and sdk:sync put it in front of
|
|
4
4
|
// client.js and stub.js, so an app still loads one file.
|
package/server.js
CHANGED
|
@@ -3468,6 +3468,7 @@ const XTERM_FILES = {
|
|
|
3468
3468
|
'addon-fit.js': '@xterm/addon-fit/lib/addon-fit.js',
|
|
3469
3469
|
'addon-webgl.js': '@xterm/addon-webgl/lib/addon-webgl.js',
|
|
3470
3470
|
'addon-unicode11.js': '@xterm/addon-unicode11/lib/addon-unicode11.js',
|
|
3471
|
+
'addon-web-links.js': '@xterm/addon-web-links/lib/addon-web-links.js',
|
|
3471
3472
|
};
|
|
3472
3473
|
app.get('/vendor/xterm/:file', (req, res) => {
|
|
3473
3474
|
const rel = Object.hasOwn(XTERM_FILES, req.params.file) ? XTERM_FILES[req.params.file] : null;
|