claude-code-kanban 7.16.0 → 7.18.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 +65 -40
- package/hub-app.json +23 -0
- package/package.json +2 -1
- package/plugin/plugins/claude-code-kanban/.claude-plugin/plugin.json +1 -1
- package/plugin/plugins/claude-code-kanban/hooks/activity.ts +45 -6
- package/plugin/plugins/claude-code-kanban/hooks/show.ts +93 -53
- package/plugin/plugins/claude-code-kanban/skills/visualize/SKILL.md +1 -0
- package/plugin/plugins/claude-code-kanban/tests/activity.test.ts +79 -13
- package/plugin/plugins/claude-code-kanban/tests/doorbell.test.ts +1 -0
- package/plugin/plugins/claude-code-kanban/tests/show.test.ts +63 -28
- package/public/app.js +178 -60
- package/public/style.css +116 -2
- package/public/terminal-frame.js +52 -1
- package/public/terminal.html +1 -0
- package/public/vendor/claude-hub-sdk.js +3 -1
- package/server.js +1 -0
package/public/style.css
CHANGED
|
@@ -834,6 +834,111 @@ body::before {
|
|
|
834
834
|
padding: 0 14px 12px;
|
|
835
835
|
}
|
|
836
836
|
|
|
837
|
+
.sessions-list.zen {
|
|
838
|
+
--zen-dot: var(--border);
|
|
839
|
+
display: flex;
|
|
840
|
+
flex-direction: column;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
.sessions-list.zen[data-zen-state='working'] {
|
|
844
|
+
--zen-dot: color-mix(in srgb, var(--success) 45%, var(--border));
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
.sessions-list.zen[data-zen-state='waiting'] {
|
|
848
|
+
--zen-dot: color-mix(in srgb, var(--warning) 55%, var(--border));
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/* The spacers fill the free space. Centered, they split it 1:2, so a growing panel moves the card up by a third of the growth, not a half. With no free space they are 0 and the list scrolls from the top. */
|
|
852
|
+
.sessions-list.zen::after,
|
|
853
|
+
.sessions-list.zen.zen-centered::before {
|
|
854
|
+
content: '';
|
|
855
|
+
flex: 1 0 0;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/* Dots unless the hub's zenPattern setting names another pattern. Line patterns use a lighter color, because a line has more ink than a dot. */
|
|
859
|
+
.sessions-list.zen {
|
|
860
|
+
--zen-line: color-mix(in srgb, var(--zen-dot) 50%, transparent);
|
|
861
|
+
--zen-fill: radial-gradient(circle, var(--zen-dot) 1px, transparent 1.5px) 0 0 / 20px 20px;
|
|
862
|
+
--zen-glow-y: 0;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
.sessions-list.zen[data-zen-pattern='diagonal'] {
|
|
866
|
+
--zen-fill: repeating-linear-gradient(-45deg, var(--zen-line) 0 1px, transparent 1px 14px);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
.sessions-list.zen[data-zen-pattern='rings'] {
|
|
870
|
+
--zen-fill: radial-gradient(circle, transparent 2.5px, var(--zen-line) 3px 3.5px, transparent 4px) 0 0 / 28px 28px;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
.sessions-list.zen[data-zen-pattern='grid'] {
|
|
874
|
+
--zen-fill:
|
|
875
|
+
linear-gradient(var(--zen-line) 1px, transparent 1px) 0 0 / 24px 24px,
|
|
876
|
+
linear-gradient(90deg, var(--zen-line) 1px, transparent 1px) 0 0 / 24px 24px;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/* The pattern glows out from the card. The glow has a fixed height, so it looks the same in a short space and a tall one. */
|
|
880
|
+
.sessions-list.zen:not([data-zen-pattern='none'])::after,
|
|
881
|
+
.sessions-list.zen.zen-centered:not([data-zen-pattern='none'])::before {
|
|
882
|
+
background: var(--zen-fill);
|
|
883
|
+
mask-image: radial-gradient(110% 260px at 50% var(--zen-glow-y), rgb(0 0 0 / 0.9), transparent);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
.sessions-list.zen.zen-centered::before {
|
|
887
|
+
--zen-glow-y: 100%;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
.sessions-list.zen.zen-centered::after {
|
|
891
|
+
flex-grow: 2;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
.sessions-list.zen > * {
|
|
895
|
+
flex-shrink: 0;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/* After the ::after spacer, so the agents sit at the bottom of the free space. */
|
|
899
|
+
.zen-agents {
|
|
900
|
+
order: 1;
|
|
901
|
+
display: flex;
|
|
902
|
+
flex-direction: column;
|
|
903
|
+
gap: 2px;
|
|
904
|
+
margin-top: 6px;
|
|
905
|
+
}
|
|
906
|
+
.zen-agents:empty {
|
|
907
|
+
display: none;
|
|
908
|
+
}
|
|
909
|
+
.zen-agent {
|
|
910
|
+
display: grid;
|
|
911
|
+
grid-template-columns: 5px 1fr auto;
|
|
912
|
+
align-items: center;
|
|
913
|
+
gap: 8px;
|
|
914
|
+
padding: 5px 8px;
|
|
915
|
+
background: none;
|
|
916
|
+
border: none;
|
|
917
|
+
border-radius: 6px;
|
|
918
|
+
font: inherit;
|
|
919
|
+
font-size: 12px;
|
|
920
|
+
text-align: left;
|
|
921
|
+
cursor: pointer;
|
|
922
|
+
}
|
|
923
|
+
.zen-agent:hover {
|
|
924
|
+
background: var(--bg-hover);
|
|
925
|
+
}
|
|
926
|
+
.zen-agent-text {
|
|
927
|
+
min-width: 0;
|
|
928
|
+
}
|
|
929
|
+
.zen-agent-task {
|
|
930
|
+
display: block;
|
|
931
|
+
overflow: hidden;
|
|
932
|
+
text-overflow: ellipsis;
|
|
933
|
+
white-space: nowrap;
|
|
934
|
+
color: var(--text-primary);
|
|
935
|
+
}
|
|
936
|
+
.zen-agent-time {
|
|
937
|
+
font-size: 10.5px;
|
|
938
|
+
font-variant-numeric: tabular-nums;
|
|
939
|
+
color: var(--text-tertiary);
|
|
940
|
+
}
|
|
941
|
+
|
|
837
942
|
.session-item {
|
|
838
943
|
position: relative;
|
|
839
944
|
display: block;
|
|
@@ -7030,13 +7135,15 @@ body.show-resizing iframe {
|
|
|
7030
7135
|
}
|
|
7031
7136
|
|
|
7032
7137
|
/* The × is a glyph, not an svg, so .row-actions button svg does not size it. */
|
|
7033
|
-
.linked-doc-remove
|
|
7138
|
+
.linked-doc-remove,
|
|
7139
|
+
.artifact-remove {
|
|
7034
7140
|
font-family: var(--mono);
|
|
7035
7141
|
font-size: 15px;
|
|
7036
7142
|
line-height: 1;
|
|
7037
7143
|
}
|
|
7038
7144
|
|
|
7039
|
-
.linked-doc-actions .linked-doc-remove:hover
|
|
7145
|
+
.linked-doc-actions .linked-doc-remove:hover,
|
|
7146
|
+
.artifact-actions .artifact-remove:hover {
|
|
7040
7147
|
background: var(--bg-elevated);
|
|
7041
7148
|
border-color: var(--border);
|
|
7042
7149
|
color: var(--text-primary);
|
|
@@ -8134,6 +8241,13 @@ body.light .msg-assistant .msg-text {
|
|
|
8134
8241
|
padding: 8px 16px;
|
|
8135
8242
|
white-space: nowrap;
|
|
8136
8243
|
}
|
|
8244
|
+
|
|
8245
|
+
/* The modal focuses a button itself, and after a mouse click Chrome does not match
|
|
8246
|
+
:focus-visible for that, so Enter would act on a button with no ring. */
|
|
8247
|
+
.confirm-buttons .btn:focus {
|
|
8248
|
+
outline: 2px solid var(--accent);
|
|
8249
|
+
outline-offset: 2px;
|
|
8250
|
+
}
|
|
8137
8251
|
/* #endregion */
|
|
8138
8252
|
|
|
8139
8253
|
/* 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.7.0 (sha256 904e53d87ec4). 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.
|
|
@@ -60,6 +60,8 @@ if (typeof module === 'object' && module.exports) module.exports = ClaudeHubKeys
|
|
|
60
60
|
status: 'standalone',
|
|
61
61
|
themes: [],
|
|
62
62
|
inHub: false,
|
|
63
|
+
settings: null,
|
|
64
|
+
onSettings: () => off,
|
|
63
65
|
onStatus: () => off,
|
|
64
66
|
onActive: () => off,
|
|
65
67
|
onThemes: () => off,
|
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;
|