claude-code-kanban 7.21.0 → 7.22.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/cli.js +334 -19
- package/hub-app.json +6 -0
- package/lib/addon-runner.js +177 -0
- package/lib/addons.js +238 -0
- package/lib/panes.js +14 -1
- package/lib/retention.js +2 -1
- package/lib/terminal.js +5 -3
- package/lib/write-json-atomic.js +16 -0
- package/package.json +1 -1
- package/plugin/plugins/claude-code-kanban/.claude-plugin/plugin.json +1 -1
- package/plugin/plugins/claude-code-kanban/skills/addon/SKILL.md +18 -0
- package/plugin/plugins/claude-code-kanban/skills/addon/references/build.md +77 -0
- package/plugin/plugins/claude-code-kanban/skills/dispatch/SKILL.md +1 -1
- package/plugin/plugins/claude-code-kanban/skills/dispatch/references/orchestration-patterns.md +2 -2
- package/plugin/plugins/claude-code-kanban/skills/kanban/SKILL.md +1 -0
- package/public/app.js +833 -127
- package/public/index.html +6 -1
- package/public/style.css +215 -3
- package/server.js +159 -24
- package/skill-guides/dispatch.md +5 -2
package/public/index.html
CHANGED
|
@@ -379,6 +379,8 @@
|
|
|
379
379
|
</div>
|
|
380
380
|
<div id="terminal-host" class="terminal-host">
|
|
381
381
|
<section id="show-card" class="show-card" aria-label="Show overlay"></section>
|
|
382
|
+
<section id="dock-addon" class="dock-addon" aria-label="Add-on" hidden></section>
|
|
383
|
+
<nav id="dock-rail" class="dock-rail" aria-label="Dock" hidden></nav>
|
|
382
384
|
<div id="ask-modal" class="ask-modal" hidden>
|
|
383
385
|
<section class="ask-box" role="dialog" aria-modal="true" aria-labelledby="ask-title">
|
|
384
386
|
<header class="ask-head">
|
|
@@ -501,6 +503,9 @@
|
|
|
501
503
|
<button id="preview-link-btn" class="icon-btn" aria-label="Link to the session" title="Link to the session" onclick="togglePreviewSessionLink()">
|
|
502
504
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
|
|
503
505
|
</button>
|
|
506
|
+
<button class="icon-btn" aria-label="Copy path" title="Copy path" onclick="copyWithFeedback(currentPreviewPath, this)">
|
|
507
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="16" height="16"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
508
|
+
</button>
|
|
504
509
|
<button class="icon-btn" aria-label="Open in editor" title="Open in editor" onclick="openPreviewInEditor()">
|
|
505
510
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
|
506
511
|
</button>
|
|
@@ -517,7 +522,7 @@
|
|
|
517
522
|
<div id="preview-modal-body" class="rendered-md modal-zoomable"></div>
|
|
518
523
|
<aside id="preview-review-panel" class="review-panel" hidden></aside>
|
|
519
524
|
</div>
|
|
520
|
-
<div id="preview-modal-meta"></div>
|
|
525
|
+
<div id="preview-modal-meta" title="Click to copy path" onclick="copyPreviewPath(this)"></div>
|
|
521
526
|
</div>
|
|
522
527
|
</div>
|
|
523
528
|
</div>
|
package/public/style.css
CHANGED
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
--font-serif: var(--serif);
|
|
63
63
|
/* reading-surface scale for the message-detail and preview modals (Ctrl +/-/0) */
|
|
64
64
|
--modal-zoom: 1;
|
|
65
|
+
--show-zoom: 1;
|
|
65
66
|
--review-panel-w: 320px;
|
|
66
67
|
}
|
|
67
68
|
|
|
@@ -4002,6 +4003,161 @@ code.ct .ct-has[aria-expanded='false'] .ct-name::after {
|
|
|
4002
4003
|
position: relative;
|
|
4003
4004
|
}
|
|
4004
4005
|
|
|
4006
|
+
.terminal-pane {
|
|
4007
|
+
--dock-rail-w: 0px;
|
|
4008
|
+
--dock-panel-w: 0px;
|
|
4009
|
+
}
|
|
4010
|
+
|
|
4011
|
+
.terminal-pane.dock-rail-on {
|
|
4012
|
+
--dock-rail-w: 30px;
|
|
4013
|
+
}
|
|
4014
|
+
|
|
4015
|
+
.terminal-pane.show-docked {
|
|
4016
|
+
--dock-panel-w: var(--show-dock-w);
|
|
4017
|
+
}
|
|
4018
|
+
|
|
4019
|
+
.terminal-pane.dock-rail-on,
|
|
4020
|
+
.terminal-pane.show-docked {
|
|
4021
|
+
padding-top: 0;
|
|
4022
|
+
padding-right: 0;
|
|
4023
|
+
padding-bottom: 0;
|
|
4024
|
+
}
|
|
4025
|
+
|
|
4026
|
+
.terminal-pane.dock-rail-on .terminal-frame,
|
|
4027
|
+
.terminal-pane.show-docked .terminal-frame {
|
|
4028
|
+
width: calc(100% - var(--dock-panel-w) - var(--dock-rail-w));
|
|
4029
|
+
}
|
|
4030
|
+
|
|
4031
|
+
.terminal-pane.dock-rail-on .terminal-bar,
|
|
4032
|
+
.terminal-pane.show-docked .terminal-bar {
|
|
4033
|
+
top: 8px;
|
|
4034
|
+
right: calc(8px + var(--dock-panel-w) + var(--dock-rail-w));
|
|
4035
|
+
}
|
|
4036
|
+
|
|
4037
|
+
.dock-rail {
|
|
4038
|
+
position: absolute;
|
|
4039
|
+
z-index: 2;
|
|
4040
|
+
top: 0;
|
|
4041
|
+
right: 0;
|
|
4042
|
+
bottom: 0;
|
|
4043
|
+
box-sizing: border-box;
|
|
4044
|
+
width: var(--dock-rail-w);
|
|
4045
|
+
display: flex;
|
|
4046
|
+
flex-direction: column;
|
|
4047
|
+
align-items: center;
|
|
4048
|
+
gap: 6px;
|
|
4049
|
+
padding-top: 6px;
|
|
4050
|
+
border-left: 1px solid var(--border);
|
|
4051
|
+
background: var(--bg-surface);
|
|
4052
|
+
}
|
|
4053
|
+
|
|
4054
|
+
.dock-rail[hidden],
|
|
4055
|
+
.dock-addon[hidden],
|
|
4056
|
+
.dock-addon-frame[hidden] {
|
|
4057
|
+
display: none;
|
|
4058
|
+
}
|
|
4059
|
+
|
|
4060
|
+
.dock-rail-btn {
|
|
4061
|
+
position: relative;
|
|
4062
|
+
display: grid;
|
|
4063
|
+
place-items: center;
|
|
4064
|
+
width: 22px;
|
|
4065
|
+
height: 22px;
|
|
4066
|
+
padding: 0;
|
|
4067
|
+
border: 0;
|
|
4068
|
+
border-radius: 4px;
|
|
4069
|
+
background: transparent;
|
|
4070
|
+
color: var(--text-tertiary);
|
|
4071
|
+
font: 600 11px var(--font-mono);
|
|
4072
|
+
cursor: pointer;
|
|
4073
|
+
}
|
|
4074
|
+
|
|
4075
|
+
.dock-rail-btn:hover {
|
|
4076
|
+
color: var(--text-primary);
|
|
4077
|
+
background: var(--bg-hover);
|
|
4078
|
+
}
|
|
4079
|
+
|
|
4080
|
+
.dock-rail-btn.on {
|
|
4081
|
+
color: var(--accent);
|
|
4082
|
+
background: var(--bg-deep);
|
|
4083
|
+
box-shadow: inset 0 0 0 1px var(--border);
|
|
4084
|
+
}
|
|
4085
|
+
|
|
4086
|
+
.dock-rail-btn svg {
|
|
4087
|
+
width: 13px;
|
|
4088
|
+
height: 13px;
|
|
4089
|
+
}
|
|
4090
|
+
|
|
4091
|
+
.dock-rail-btn.dot::after {
|
|
4092
|
+
content: '';
|
|
4093
|
+
position: absolute;
|
|
4094
|
+
top: 1px;
|
|
4095
|
+
right: 1px;
|
|
4096
|
+
width: 6px;
|
|
4097
|
+
height: 6px;
|
|
4098
|
+
border-radius: 50%;
|
|
4099
|
+
background: var(--accent);
|
|
4100
|
+
}
|
|
4101
|
+
|
|
4102
|
+
.dock-addon {
|
|
4103
|
+
position: absolute;
|
|
4104
|
+
z-index: 2;
|
|
4105
|
+
box-sizing: border-box;
|
|
4106
|
+
display: flex;
|
|
4107
|
+
flex-direction: column;
|
|
4108
|
+
border-left: 1px solid var(--border);
|
|
4109
|
+
border-bottom: 1px solid var(--border);
|
|
4110
|
+
background: var(--bg-surface);
|
|
4111
|
+
color: var(--text-secondary);
|
|
4112
|
+
font-family: var(--font-mono);
|
|
4113
|
+
}
|
|
4114
|
+
|
|
4115
|
+
.dock-addon:focus-within,
|
|
4116
|
+
.dock-addon.focused {
|
|
4117
|
+
border-color: var(--accent);
|
|
4118
|
+
box-shadow:
|
|
4119
|
+
0 0 0 1px var(--accent),
|
|
4120
|
+
0 0 10px var(--accent-glow);
|
|
4121
|
+
}
|
|
4122
|
+
|
|
4123
|
+
.dock-addon-head:focus {
|
|
4124
|
+
outline: none;
|
|
4125
|
+
}
|
|
4126
|
+
|
|
4127
|
+
/* Stops above the prompt rows, as the expanded show card does. */
|
|
4128
|
+
.dock-addon.expanded {
|
|
4129
|
+
left: 0;
|
|
4130
|
+
bottom: calc(7.2 * var(--show-term-font, 13px));
|
|
4131
|
+
width: auto;
|
|
4132
|
+
border-bottom: 1px solid var(--border);
|
|
4133
|
+
}
|
|
4134
|
+
|
|
4135
|
+
.dock-addon.expanded .show-dock-edge {
|
|
4136
|
+
display: none;
|
|
4137
|
+
}
|
|
4138
|
+
|
|
4139
|
+
.dock-addon-body {
|
|
4140
|
+
position: relative;
|
|
4141
|
+
flex: 1;
|
|
4142
|
+
min-height: 0;
|
|
4143
|
+
}
|
|
4144
|
+
|
|
4145
|
+
.dock-addon-frame {
|
|
4146
|
+
display: block;
|
|
4147
|
+
width: 100%;
|
|
4148
|
+
height: 100%;
|
|
4149
|
+
border: 0;
|
|
4150
|
+
background: var(--bg-surface);
|
|
4151
|
+
}
|
|
4152
|
+
|
|
4153
|
+
.dock-addon-view {
|
|
4154
|
+
height: 100%;
|
|
4155
|
+
}
|
|
4156
|
+
|
|
4157
|
+
.show-card.open.under-addon {
|
|
4158
|
+
display: none;
|
|
4159
|
+
}
|
|
4160
|
+
|
|
4005
4161
|
.show-card {
|
|
4006
4162
|
--show-inset: 8px;
|
|
4007
4163
|
/* Six terminal rows (prompt, its rules, status line) at xterm's cell height of about 1.2 × font size. */
|
|
@@ -4009,7 +4165,7 @@ code.ct .ct-has[aria-expanded='false'] .ct-name::after {
|
|
|
4009
4165
|
position: absolute;
|
|
4010
4166
|
z-index: 2;
|
|
4011
4167
|
top: var(--show-inset);
|
|
4012
|
-
right: var(--show-inset);
|
|
4168
|
+
right: calc(var(--show-inset) + var(--dock-rail-w));
|
|
4013
4169
|
display: none;
|
|
4014
4170
|
flex-direction: column;
|
|
4015
4171
|
box-sizing: border-box;
|
|
@@ -4058,6 +4214,27 @@ body.light .show-card {
|
|
|
4058
4214
|
max-height: none;
|
|
4059
4215
|
}
|
|
4060
4216
|
|
|
4217
|
+
/* Off the bottom edge: the Windows 11 window frame covers the last pixel row, which hides the
|
|
4218
|
+
bottom border there. */
|
|
4219
|
+
.dock-addon,
|
|
4220
|
+
.show-card.docked {
|
|
4221
|
+
top: 0;
|
|
4222
|
+
right: var(--dock-rail-w);
|
|
4223
|
+
bottom: 2px;
|
|
4224
|
+
width: var(--show-dock-drag, var(--show-dock-w));
|
|
4225
|
+
}
|
|
4226
|
+
|
|
4227
|
+
.show-card.docked {
|
|
4228
|
+
left: auto;
|
|
4229
|
+
height: auto;
|
|
4230
|
+
min-width: 0;
|
|
4231
|
+
max-width: none;
|
|
4232
|
+
max-height: none;
|
|
4233
|
+
border-width: 0 0 1px 1px;
|
|
4234
|
+
border-radius: 0;
|
|
4235
|
+
box-shadow: none;
|
|
4236
|
+
}
|
|
4237
|
+
|
|
4061
4238
|
.show-card.collapsed {
|
|
4062
4239
|
left: auto;
|
|
4063
4240
|
bottom: auto;
|
|
@@ -4069,7 +4246,9 @@ body.light .show-card {
|
|
|
4069
4246
|
.show-card.collapsed .show-body,
|
|
4070
4247
|
.show-card.collapsed .show-review,
|
|
4071
4248
|
.show-card.collapsed .show-grip,
|
|
4072
|
-
.show-card.expanded .show-grip
|
|
4249
|
+
.show-card.expanded .show-grip,
|
|
4250
|
+
.show-card.docked .show-grip,
|
|
4251
|
+
.show-card:not(.docked) .show-dock-edge {
|
|
4073
4252
|
display: none;
|
|
4074
4253
|
}
|
|
4075
4254
|
|
|
@@ -4159,6 +4338,12 @@ body.light .show-card.focused {
|
|
|
4159
4338
|
0 0 10px var(--accent-glow);
|
|
4160
4339
|
}
|
|
4161
4340
|
|
|
4341
|
+
.dock-addon:focus-within,
|
|
4342
|
+
.dock-addon.focused,
|
|
4343
|
+
.show-card.docked.focused {
|
|
4344
|
+
border-bottom-color: var(--border);
|
|
4345
|
+
}
|
|
4346
|
+
|
|
4162
4347
|
.show-actions {
|
|
4163
4348
|
flex: none;
|
|
4164
4349
|
display: flex;
|
|
@@ -4197,6 +4382,10 @@ body.light .show-card.focused {
|
|
|
4197
4382
|
height: 14px;
|
|
4198
4383
|
}
|
|
4199
4384
|
|
|
4385
|
+
.show-btn.active {
|
|
4386
|
+
color: var(--accent);
|
|
4387
|
+
}
|
|
4388
|
+
|
|
4200
4389
|
.show-btn.remove:hover:not(:disabled) {
|
|
4201
4390
|
color: var(--text-primary);
|
|
4202
4391
|
background: var(--bg-elevated);
|
|
@@ -4271,6 +4460,7 @@ body.light .show-card.focused {
|
|
|
4271
4460
|
padding: 0.8em 1.1em 1em;
|
|
4272
4461
|
/* The same scale as an HTML card's frame (the zoom in showBridge). */
|
|
4273
4462
|
font-size: clamp(12.5px, calc(9.7px + 0.58cqi), 17.5px);
|
|
4463
|
+
zoom: var(--show-zoom, 1);
|
|
4274
4464
|
line-height: 1.6;
|
|
4275
4465
|
color: var(--text-secondary);
|
|
4276
4466
|
overflow-wrap: anywhere;
|
|
@@ -4459,6 +4649,22 @@ body.light .show-card.focused {
|
|
|
4459
4649
|
height: 9px;
|
|
4460
4650
|
}
|
|
4461
4651
|
|
|
4652
|
+
.show-dock-edge {
|
|
4653
|
+
position: absolute;
|
|
4654
|
+
top: 0;
|
|
4655
|
+
left: 0;
|
|
4656
|
+
bottom: 0;
|
|
4657
|
+
width: 5px;
|
|
4658
|
+
cursor: col-resize;
|
|
4659
|
+
touch-action: none;
|
|
4660
|
+
transition: background 0.15s;
|
|
4661
|
+
}
|
|
4662
|
+
|
|
4663
|
+
.show-dock-edge:hover,
|
|
4664
|
+
body.show-resizing .show-dock-edge {
|
|
4665
|
+
background: var(--accent-dim);
|
|
4666
|
+
}
|
|
4667
|
+
|
|
4462
4668
|
body.show-resizing {
|
|
4463
4669
|
user-select: none;
|
|
4464
4670
|
}
|
|
@@ -5915,7 +6121,7 @@ body.show-resizing iframe {
|
|
|
5915
6121
|
padding: 8px 14px;
|
|
5916
6122
|
font-size: 11px;
|
|
5917
6123
|
color: var(--text-secondary);
|
|
5918
|
-
z-index:
|
|
6124
|
+
z-index: 10020;
|
|
5919
6125
|
opacity: 0;
|
|
5920
6126
|
transform: translateY(10px);
|
|
5921
6127
|
transition:
|
|
@@ -8130,6 +8336,12 @@ body.light .msg-assistant .msg-text {
|
|
|
8130
8336
|
font-size: 11px;
|
|
8131
8337
|
color: var(--text-muted);
|
|
8132
8338
|
flex-shrink: 0;
|
|
8339
|
+
cursor: copy;
|
|
8340
|
+
align-self: flex-start;
|
|
8341
|
+
}
|
|
8342
|
+
|
|
8343
|
+
#preview-modal-meta:hover {
|
|
8344
|
+
color: var(--text-primary);
|
|
8133
8345
|
}
|
|
8134
8346
|
|
|
8135
8347
|
/* Rendered HTML pages need far more room than a markdown doc. */
|
package/server.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
const express = require('express');
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
const fs = require('node:fs').promises;
|
|
7
|
-
const { existsSync, readdirSync, readFileSync, writeFileSync, statSync, unlinkSync,
|
|
7
|
+
const { existsSync, readdirSync, readFileSync, writeFileSync, statSync, unlinkSync, mkdirSync, openSync, readSync, closeSync, realpathSync } = require('node:fs');
|
|
8
8
|
const _readline = require('node:readline');
|
|
9
9
|
const chokidar = require('chokidar');
|
|
10
10
|
const os = require('node:os');
|
|
@@ -67,6 +67,9 @@ const { readSettings } = require('./lib/claude-settings');
|
|
|
67
67
|
const { readGitBranch, sessionGitBranch } = require('./lib/git-branch');
|
|
68
68
|
const { createLinkedDocStore, linkUrl } = require('./lib/linked-docs');
|
|
69
69
|
const { createPaneStore, isOwnOrigin } = require('./lib/panes');
|
|
70
|
+
const { createAddonStore, sampleUrl } = require('./lib/addons');
|
|
71
|
+
const { writeJsonAtomic } = require('./lib/write-json-atomic');
|
|
72
|
+
const { createAddonRunner } = require('./lib/addon-runner');
|
|
70
73
|
const { probeFraming, parseOrigin } = require('./lib/frame-policy');
|
|
71
74
|
const { pickFolder } = require('./lib/folder-dialog');
|
|
72
75
|
const { loadSessionCache, saveSessionCache } = require('./lib/session-cache');
|
|
@@ -123,6 +126,7 @@ const DISPATCHED_FILE = path.join(CCK_DIR, 'dispatched.json');
|
|
|
123
126
|
const WORKTREES_FILE = path.join(CCK_DIR, 'worktrees.json');
|
|
124
127
|
const LINKED_DOCS_FILE = path.join(CCK_DIR, 'linked-docs.json');
|
|
125
128
|
const PANES_FILE = path.join(CCK_DIR, 'panes.json');
|
|
129
|
+
const ADDONS_FILE = path.join(CCK_DIR, 'addons.json');
|
|
126
130
|
const SERVER_INFO_FILE = path.join(CCK_DIR, 'server.json');
|
|
127
131
|
const TERMINAL_TOKENS_DIR = path.join(CCK_DIR, 'terminal-tokens');
|
|
128
132
|
const SESSION_CACHE_FILE = path.join(CCK_DIR, 'session-cache.json');
|
|
@@ -149,18 +153,6 @@ const SCRATCHPAD_ROOT = (() => {
|
|
|
149
153
|
// #endregion
|
|
150
154
|
|
|
151
155
|
// #region SERVER_STATE
|
|
152
|
-
function writeJsonAtomic(file, obj, mode) {
|
|
153
|
-
mkdirSync(path.dirname(file), { recursive: true });
|
|
154
|
-
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
155
|
-
try {
|
|
156
|
-
writeFileSync(tmp, JSON.stringify(obj, null, 2), { encoding: 'utf8', mode });
|
|
157
|
-
renameSync(tmp, file);
|
|
158
|
-
} catch (e) {
|
|
159
|
-
rmSync(tmp, { force: true });
|
|
160
|
-
throw e;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
156
|
// Claude Code reads task files while the board writes them, so it must never see half a file.
|
|
165
157
|
// On Windows a rename over a file another handle has open fails with EPERM or EBUSY until it closes.
|
|
166
158
|
let taskWriteSeq = 0;
|
|
@@ -3382,12 +3374,41 @@ app.post('/api/terminal/pick-folder', async (req, res) => {
|
|
|
3382
3374
|
}
|
|
3383
3375
|
});
|
|
3384
3376
|
|
|
3377
|
+
// A tab whose terminal is off, or whose token died with an earlier server, ignores
|
|
3378
|
+
// `session:launched`, so only a tab that claims the session counts as showing it.
|
|
3379
|
+
const LAUNCH_CLAIM_MS = 3000;
|
|
3380
|
+
const launchClaims = new Map();
|
|
3381
|
+
function awaitLaunchClaim(id) {
|
|
3382
|
+
const claimed = new Promise((resolve) => {
|
|
3383
|
+
launchClaims.set(id, resolve);
|
|
3384
|
+
setTimeout(resolve, LAUNCH_CLAIM_MS, false);
|
|
3385
|
+
});
|
|
3386
|
+
return claimed.finally(() => launchClaims.delete(id));
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3389
|
+
app.post('/api/terminal/launched/:id', (req, res) => {
|
|
3390
|
+
if (!terminal.authorized(req.get('x-terminal-token'))) return res.status(401).json({ error: 'invalid terminal token' });
|
|
3391
|
+
launchClaims.get(req.params.id)?.(true);
|
|
3392
|
+
res.status(204).end();
|
|
3393
|
+
});
|
|
3394
|
+
|
|
3385
3395
|
// The New session dialog's "Stay here": the PTY starts headless and the board keeps its view.
|
|
3396
|
+
// `show` is `claude-code-kanban new` from a shell outside the board: a board tab adds its card
|
|
3397
|
+
// and attaches, and with no tab to claim it the PTY is ended, because no one would see it.
|
|
3386
3398
|
app.post('/api/terminal/start', terminalRoute(async (req, res) => {
|
|
3387
3399
|
if (!terminal.authorized(req.get('x-terminal-token'))) return res.status(401).json({ error: 'invalid terminal token' });
|
|
3388
|
-
const { id, cwd, name, model, effort, worktree, prompt } = req.body || {};
|
|
3389
|
-
|
|
3400
|
+
const { id, cwd, name, model, effort, worktree, prompt, claudeArgs, show } = req.body || {};
|
|
3401
|
+
if (show && !clients.size) return res.status(409).json({ error: 'no board tab is open' });
|
|
3402
|
+
const started = await terminal.startNew({ id, cwd, name, model, effort, worktree, prompt, extraArgs: claudeArgs });
|
|
3390
3403
|
if (started.error) return res.status(started.status).json({ error: started.error });
|
|
3404
|
+
if (show) {
|
|
3405
|
+
const claimed = awaitLaunchClaim(started.id);
|
|
3406
|
+
broadcast({ type: 'session:launched', ...started });
|
|
3407
|
+
if (!(await claimed)) {
|
|
3408
|
+
await terminal.end(started.id, terminal.token);
|
|
3409
|
+
return res.status(409).json({ error: 'no open board tab can show the terminal' });
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3391
3412
|
res.status(201).json({ session: started.id, cwd: started.cwd });
|
|
3392
3413
|
}));
|
|
3393
3414
|
|
|
@@ -3500,7 +3521,7 @@ const linkedDocs = createLinkedDocStore(jsonFile(LINKED_DOCS_FILE));
|
|
|
3500
3521
|
function withDispatchMarker(sessions) {
|
|
3501
3522
|
return sessions.map((s) => {
|
|
3502
3523
|
const marker = dispatched.get(s.id);
|
|
3503
|
-
return marker ? { ...s, dispatched: { parent: marker.parent } } : s;
|
|
3524
|
+
return marker ? { ...s, dispatched: { parent: marker.parent, child: marker.child } } : s;
|
|
3504
3525
|
});
|
|
3505
3526
|
}
|
|
3506
3527
|
|
|
@@ -3508,13 +3529,15 @@ function withDispatchMarker(sessions) {
|
|
|
3508
3529
|
// TERMINAL_TOKEN_FILE. The started session never holds it.
|
|
3509
3530
|
app.post('/api/dispatch', terminalRoute(async (req, res) => {
|
|
3510
3531
|
if (!terminal.authorized(req.get('x-terminal-token'))) return res.status(401).json({ error: 'invalid terminal token' });
|
|
3511
|
-
const { cwd, spec, name, model, worktree, taskList, parent, group, claudeArgs } = req.body || {};
|
|
3532
|
+
const { cwd, spec, name, model, worktree, taskList, parent, child, group, claudeArgs } = req.body || {};
|
|
3512
3533
|
if (typeof spec !== 'string' || !spec.trim()) return res.status(400).json({ error: 'spec is required' });
|
|
3513
3534
|
if (parent != null && !(typeof parent === 'string' && isUUID(parent))) return res.status(400).json({ error: 'invalid parent' });
|
|
3514
3535
|
if (group != null && !(typeof group === 'string' && group.trim())) return res.status(400).json({ error: 'group must be a group name, path or id' });
|
|
3536
|
+
if (child && !parent) return res.status(400).json({ error: 'child needs a parent session' });
|
|
3537
|
+
if (child && group) return res.status(400).json({ error: 'pass child or group, not both' });
|
|
3515
3538
|
const started = await terminal.startNew({ cwd, name, model, worktree, taskList, prompt: spec.trim(), extraArgs: claudeArgs });
|
|
3516
3539
|
if (started.error) return res.status(started.status).json({ error: started.error });
|
|
3517
|
-
dispatched.record(started.id, { parent, cwd: started.cwd, name, group, worktree });
|
|
3540
|
+
dispatched.record(started.id, { parent, child: child === true, cwd: started.cwd, name, group, worktree });
|
|
3518
3541
|
// Joined before dispatch-update, so the board places the session from its first frame. The
|
|
3519
3542
|
// starter stays where it is: moving it would jump it under the user.
|
|
3520
3543
|
let joined = null;
|
|
@@ -4369,9 +4392,11 @@ app.get('/api/panes/:sessionId/:paneId/framing', async (req, res) => {
|
|
|
4369
4392
|
app.patch('/api/panes/:sessionId/:paneId', (req, res) => {
|
|
4370
4393
|
try {
|
|
4371
4394
|
const { sessionId, paneId } = req.params;
|
|
4372
|
-
const title = req.body
|
|
4373
|
-
if (typeof
|
|
4374
|
-
|
|
4395
|
+
const { title, dock } = req.body || {};
|
|
4396
|
+
if (typeof dock !== 'boolean' && typeof title !== 'string') {
|
|
4397
|
+
throw previewError(400, 'title must be a string, or dock a boolean', 'bad_title');
|
|
4398
|
+
}
|
|
4399
|
+
const layout = typeof dock === 'boolean' ? panes.setDock(sessionId, paneId, dock) : panes.rename(sessionId, paneId, title);
|
|
4375
4400
|
if (!layout) throw previewError(404, `No pane ${paneId} in session ${sessionId}`, 'no_pane');
|
|
4376
4401
|
res.json({ layout });
|
|
4377
4402
|
} catch (error) {
|
|
@@ -4387,6 +4412,115 @@ app.delete('/api/panes/:sessionId/:paneId', (req, res) => {
|
|
|
4387
4412
|
});
|
|
4388
4413
|
// #endregion
|
|
4389
4414
|
|
|
4415
|
+
// #region ADDONS
|
|
4416
|
+
// Web pages on the dock rail (lib/addons.js). The board fills each URL for its terminal's
|
|
4417
|
+
// session, so the routes carry the templates; every change sends one `addon:changed` and the
|
|
4418
|
+
// boards refetch.
|
|
4419
|
+
const addonRunner = createAddonRunner({ logDir: path.join(CCK_DIR, 'addons') });
|
|
4420
|
+
process.on('exit', addonRunner.shutdown);
|
|
4421
|
+
const addons = createAddonStore({
|
|
4422
|
+
...stampedJsonFile(ADDONS_FILE, writeJsonAtomic),
|
|
4423
|
+
onChange: (global) => {
|
|
4424
|
+
addonRunner.retain(global);
|
|
4425
|
+
broadcast({ type: 'addon:changed' });
|
|
4426
|
+
},
|
|
4427
|
+
isOwnOrigin: (url) => isOwnOrigin(url, { boardPort, boardHosts, hubUrl: process.env.HUB_URL }),
|
|
4428
|
+
});
|
|
4429
|
+
chokidar.watch(ADDONS_FILE, { ignoreInitial: true }).on('all', (event) => {
|
|
4430
|
+
if (event === 'add' || event === 'change') addons.reload();
|
|
4431
|
+
});
|
|
4432
|
+
|
|
4433
|
+
app.get('/api/addons', (req, res) => {
|
|
4434
|
+
const sessionId = typeof req.query.session === 'string' ? req.query.session : null;
|
|
4435
|
+
res.json(addons.list(sessionId));
|
|
4436
|
+
});
|
|
4437
|
+
|
|
4438
|
+
app.post('/api/addons', (req, res) => {
|
|
4439
|
+
try {
|
|
4440
|
+
const { scope, url, title, icon, params, run, cwd, key } = req.body || {};
|
|
4441
|
+
if (typeof scope !== 'string' || !scope) throw previewError(400, "scope must be 'global' or a session id", 'bad_scope');
|
|
4442
|
+
// A run command is a command line the board will start, so it needs what dispatch needs.
|
|
4443
|
+
if (run && !terminal.authorized(req.get('x-terminal-token'))) return res.status(401).json({ error: 'invalid terminal token' });
|
|
4444
|
+
const result = addons.add(scope, { url, title, icon, params, run, cwd, key });
|
|
4445
|
+
res.status(result.added ? 201 : 200).json(result);
|
|
4446
|
+
} catch (error) {
|
|
4447
|
+
paneRouteError(res, error, 'POST /api/addons');
|
|
4448
|
+
}
|
|
4449
|
+
});
|
|
4450
|
+
|
|
4451
|
+
app.patch('/api/addons/:id', (req, res) => {
|
|
4452
|
+
try {
|
|
4453
|
+
const { session, params } = req.body || {};
|
|
4454
|
+
if (typeof session !== 'string' || !session) throw previewError(400, 'session is required', 'bad_scope');
|
|
4455
|
+
const list = addons.setParams(session, req.params.id, params);
|
|
4456
|
+
if (!list) throw previewError(404, `No add-on ${req.params.id} in session ${session}`, 'no_addon');
|
|
4457
|
+
res.json(list);
|
|
4458
|
+
} catch (error) {
|
|
4459
|
+
paneRouteError(res, error, 'PATCH /api/addons/:id');
|
|
4460
|
+
}
|
|
4461
|
+
});
|
|
4462
|
+
|
|
4463
|
+
app.delete('/api/addons/:id', (req, res) => {
|
|
4464
|
+
const session = typeof req.query.session === 'string' ? req.query.session : null;
|
|
4465
|
+
const removed = addons.remove(session, req.params.id);
|
|
4466
|
+
if (!removed) return res.status(404).json({ error: `No add-on ${req.params.id} here`, code: 'no_addon' });
|
|
4467
|
+
res.json(removed);
|
|
4468
|
+
});
|
|
4469
|
+
|
|
4470
|
+
app.post('/api/addons/:id/start', async (req, res) => {
|
|
4471
|
+
const entry = addons.getGlobal(req.params.id);
|
|
4472
|
+
if (!entry) return res.status(404).json({ error: `No global add-on ${req.params.id}`, code: 'no_addon' });
|
|
4473
|
+
try {
|
|
4474
|
+
res.json(await addonRunner.ensure(entry));
|
|
4475
|
+
} catch (error) {
|
|
4476
|
+
res.status(500).json({ state: 'failed', error: error.message });
|
|
4477
|
+
}
|
|
4478
|
+
});
|
|
4479
|
+
|
|
4480
|
+
// The hub's app launcher rows (`launcher.items` in hub-app.json): the global add-ons,
|
|
4481
|
+
// with Restart for the ones whose server the board runs.
|
|
4482
|
+
const LAUNCHER_ITEMS = '/api/hub/launcher-items';
|
|
4483
|
+
app.get(LAUNCHER_ITEMS, async (_req, res) => {
|
|
4484
|
+
const items = await Promise.all(
|
|
4485
|
+
addons.list(null).global.map(async (a) => {
|
|
4486
|
+
let detail = a.url;
|
|
4487
|
+
try {
|
|
4488
|
+
detail = new URL(sampleUrl(a.url)).host;
|
|
4489
|
+
} catch {}
|
|
4490
|
+
return {
|
|
4491
|
+
id: a.id,
|
|
4492
|
+
name: a.title,
|
|
4493
|
+
kind: 'add-on',
|
|
4494
|
+
detail,
|
|
4495
|
+
state: a.run ? await addonRunner.status(a) : '',
|
|
4496
|
+
path: ADDONS_FILE,
|
|
4497
|
+
ops: a.run ? ['restart', 'remove'] : ['remove'],
|
|
4498
|
+
};
|
|
4499
|
+
}),
|
|
4500
|
+
);
|
|
4501
|
+
res.json({ items });
|
|
4502
|
+
});
|
|
4503
|
+
|
|
4504
|
+
app.post(`${LAUNCHER_ITEMS}/:id/restart`, async (req, res) => {
|
|
4505
|
+
const entry = addons.getGlobal(req.params.id);
|
|
4506
|
+
if (!entry?.run) return res.status(404).json({ error: `No global add-on ${req.params.id} with a run command`, code: 'no_addon' });
|
|
4507
|
+
try {
|
|
4508
|
+
const result = await addonRunner.restart(entry);
|
|
4509
|
+
if (result.state === 'failed' || result.state === 'external') return res.status(409).json(result);
|
|
4510
|
+
broadcast({ type: 'addon:restarted', id: entry.id });
|
|
4511
|
+
res.json({ state: 'running', message: `Restarted ${entry.title}` });
|
|
4512
|
+
} catch (error) {
|
|
4513
|
+
res.status(500).json({ state: 'failed', error: error.message });
|
|
4514
|
+
}
|
|
4515
|
+
});
|
|
4516
|
+
|
|
4517
|
+
app.post(`${LAUNCHER_ITEMS}/:id/remove`, (req, res) => {
|
|
4518
|
+
const removed = addons.remove(null, req.params.id);
|
|
4519
|
+
if (!removed) return res.status(404).json({ error: `No global add-on ${req.params.id}`, code: 'no_addon' });
|
|
4520
|
+
res.json({ message: `Removed ${removed.addon.title}` });
|
|
4521
|
+
});
|
|
4522
|
+
// #endregion
|
|
4523
|
+
|
|
4390
4524
|
// #region REVIEW
|
|
4391
4525
|
// API: Send review comments on something the board shows (a file preview today) to a
|
|
4392
4526
|
// session. The source is described, not interpreted, so a new kind of preview needs a
|
|
@@ -4821,7 +4955,7 @@ async function cleanupAgentActivity() {
|
|
|
4821
4955
|
} catch { /* agent-activity dir may not exist */ }
|
|
4822
4956
|
}
|
|
4823
4957
|
|
|
4824
|
-
// Dispatch markers, pane layouts, reviews, attachments, context status and worktrees: see docs/retention.md.
|
|
4958
|
+
// Dispatch markers, pane layouts, add-on sessions, reviews, attachments, context status and worktrees: see docs/retention.md.
|
|
4825
4959
|
async function runRetention() {
|
|
4826
4960
|
try {
|
|
4827
4961
|
const scan = await scanTranscripts(PROJECTS_DIR);
|
|
@@ -4829,14 +4963,15 @@ async function runRetention() {
|
|
|
4829
4963
|
const markers = dispatched.prune(opts);
|
|
4830
4964
|
const layouts = panes.prune(opts);
|
|
4831
4965
|
if (layouts) queuePaneFileWatch();
|
|
4966
|
+
const addonSessions = addons.prune(opts);
|
|
4832
4967
|
const reviews = await pruneSessionDirs(REVIEW_DIR, opts);
|
|
4833
4968
|
const attachments = await pruneSessionDirs(ATTACHMENTS_DIR, opts);
|
|
4834
4969
|
const contexts = await pruneContextStatus(CONTEXT_STATUS_DIR, opts);
|
|
4835
4970
|
const wts = worktrees.prune(scan?.dirs);
|
|
4836
4971
|
const taskMaps = await pruneTaskMaps(TASK_MAPS_DIR, opts);
|
|
4837
4972
|
const grouped = expireGroupMembers(scan?.ids);
|
|
4838
|
-
if (markers || layouts || reviews || attachments || contexts || wts || taskMaps || grouped.members) {
|
|
4839
|
-
console.log(`[retention] removed ${markers} dispatch markers, ${layouts} pane layouts, ${reviews} reviews, ${attachments} attachments, ${contexts} context status files, ${wts} worktrees, ${taskMaps} task-list mappings, ${grouped.members} group members, ${grouped.groups} groups`);
|
|
4973
|
+
if (markers || layouts || addonSessions || reviews || attachments || contexts || wts || taskMaps || grouped.members) {
|
|
4974
|
+
console.log(`[retention] removed ${markers} dispatch markers, ${layouts} pane layouts, ${addonSessions} add-on sessions, ${reviews} reviews, ${attachments} attachments, ${contexts} context status files, ${wts} worktrees, ${taskMaps} task-list mappings, ${grouped.members} group members, ${grouped.groups} groups`);
|
|
4840
4975
|
}
|
|
4841
4976
|
} catch (e) {
|
|
4842
4977
|
console.warn('[retention] failed:', e.message);
|
package/skill-guides/dispatch.md
CHANGED
|
@@ -11,11 +11,14 @@ cck sends nothing back. To hear from the session, the spec tells it to `SendMess
|
|
|
11
11
|
## Start
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
claude-code-kanban dispatch start --cwd <dir> --spec-file <spec.md> --name <name> --group <group> --json -- <claude args>
|
|
14
|
+
claude-code-kanban dispatch start --cwd <dir> --spec-file <spec.md> --name <name> (--child | --group <group>) --json -- <claude args>
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
- `--name` is the sidebar name and the session's peer name. Kebab-case and unique: `fix-login-redirect`.
|
|
18
|
-
-
|
|
18
|
+
- Every dispatch picks its **place** in the sidebar. Ask one question: will its result come back to you?
|
|
19
|
+
- Yes: `--child`. It is your part of the work (a fix, a review, research you will use). Its card shows under yours, wherever yours is.
|
|
20
|
+
- No: `--group <group>`. It is separate work the user follows on the board (a handoff, an unrelated task). `<group>` names the effort (`auth-refactor`, or a path like `auth-refactor/api`); pass the same group to each dispatch in that effort. A group it makes goes when none of its sessions shows as active; an existing group stays.
|
|
21
|
+
- If you cannot tell, ask the user in one line before you start the session.
|
|
19
22
|
- `--spec-file` keeps a long spec out of shell quoting.
|
|
20
23
|
- Everything after `--` goes to `claude` as it is: any flag in `claude --help`, e.g. `-- --permission-mode auto --add-dir ../shared`. Keep each value one shell word of plain characters (no quotes, `%` or control characters); long text belongs in the spec. cck owns the session id, name, model and worktree, so pass those with its own flags.
|
|
21
24
|
- After a cck restart the terminal comes back with `claude --resume <id>` alone, so the args after `--` apply to the first run only.
|