claude-code-kanban 5.4.0 → 6.0.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/public/style.css CHANGED
@@ -7075,6 +7075,105 @@ select.form-input option:checked {
7075
7075
  color: var(--text-muted);
7076
7076
  align-self: center;
7077
7077
  }
7078
+
7079
+ .scratchpad-header {
7080
+ position: relative;
7081
+ }
7082
+
7083
+ .scratchpad-title-row {
7084
+ display: flex;
7085
+ align-items: center;
7086
+ gap: 10px;
7087
+ }
7088
+
7089
+ .scratchpad-emoji-btn {
7090
+ width: 32px;
7091
+ height: 32px;
7092
+ display: flex;
7093
+ align-items: center;
7094
+ justify-content: center;
7095
+ font-size: 18px;
7096
+ line-height: 1;
7097
+ color: var(--text-muted);
7098
+ background: var(--bg-elevated);
7099
+ border: 1px dashed var(--border);
7100
+ border-radius: 8px;
7101
+ cursor: pointer;
7102
+ }
7103
+
7104
+ .scratchpad-emoji-btn.set {
7105
+ border-style: solid;
7106
+ border-color: var(--accent);
7107
+ }
7108
+
7109
+ .scratchpad-emoji-btn:hover {
7110
+ border-color: var(--accent);
7111
+ }
7112
+
7113
+ .scratchpad-emoji-btn[hidden] {
7114
+ display: none;
7115
+ }
7116
+
7117
+ .scratchpad-emoji-picker {
7118
+ display: none;
7119
+ position: absolute;
7120
+ top: 40px;
7121
+ left: 0;
7122
+ z-index: 5;
7123
+ width: 300px;
7124
+ padding: 10px;
7125
+ background: var(--bg-elevated);
7126
+ border: 1px solid var(--border);
7127
+ border-radius: 10px;
7128
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
7129
+ outline: none;
7130
+ }
7131
+
7132
+ .scratchpad-emoji-picker.open {
7133
+ display: block;
7134
+ }
7135
+
7136
+ .scratchpad-emoji-grid {
7137
+ display: grid;
7138
+ gap: 2px;
7139
+ }
7140
+
7141
+ .scratchpad-emoji-grid button {
7142
+ padding: 4px 0;
7143
+ font-size: 18px;
7144
+ background: none;
7145
+ border: 0;
7146
+ border-radius: 6px;
7147
+ cursor: pointer;
7148
+ }
7149
+
7150
+ .scratchpad-emoji-grid button:hover,
7151
+ .scratchpad-emoji-grid button.kb {
7152
+ background: var(--bg-hover);
7153
+ }
7154
+
7155
+ .scratchpad-emoji-grid button.cur {
7156
+ background: var(--accent-dim);
7157
+ }
7158
+
7159
+ .scratchpad-emoji-row {
7160
+ display: flex;
7161
+ gap: 6px;
7162
+ margin-top: 8px;
7163
+ }
7164
+
7165
+ .scratchpad-emoji-row .form-input {
7166
+ flex: 1;
7167
+ min-width: 0;
7168
+ padding: 5px 8px;
7169
+ font-size: 13px;
7170
+ }
7171
+
7172
+ .scratchpad-badge.has-emoji {
7173
+ font-size: 13px;
7174
+ line-height: 18px;
7175
+ padding: 0 5px;
7176
+ }
7078
7177
  /* #endregion */
7079
7178
 
7080
7179
  /* #region STORAGE_MANAGER */
package/server.js CHANGED
@@ -43,11 +43,11 @@ const { buildDecision, decisionFileName, isDecisionFile, approvalsFrom, boardRef
43
43
  const { getClaudeDir, getArgValue, storageNamespace, isDefaultClaudeDir, encodeProjectDirName } = require('./lib/claude-dir');
44
44
  const { createTerminalService, readTerminalConfig } = require('./lib/terminal');
45
45
  const { createProcStats } = require('./lib/proc-stats');
46
- const { createDispatchRegistry, formatPreamble, formatDispatchLine, isPeerName, DISPATCH_OUTCOME } = require('./lib/dispatch');
46
+ const { createDispatchRegistry } = require('./lib/dispatch');
47
47
  const { createGroupStore, isGroupName, suggestGroupName } = require('./lib/dispatch-groups');
48
48
  const { createDispatchedStore, scanTranscripts, pruneSessionDirs, retentionMs } = require('./lib/retention');
49
49
  const { createWorktreeStore } = require('./lib/worktrees');
50
- const { readGitBranch } = require('./lib/git-branch');
50
+ const { readGitBranch, sessionGitBranch } = require('./lib/git-branch');
51
51
  const { createLinkedDocStore, linkUrl } = require('./lib/linked-docs');
52
52
  const { pickFolder } = require('./lib/folder-dialog');
53
53
  const { loadSessionCache, saveSessionCache } = require('./lib/session-cache');
@@ -343,15 +343,6 @@ const worktrees = createWorktreeStore({
343
343
  save: (data) => writeJsonAtomic(WORKTREES_FILE, data),
344
344
  });
345
345
 
346
- // Only look up the branch when cwd has diverged from the launch project — that's
347
- // the only case the JSONL value is wrong.
348
- function resolveSessionGitBranch(meta) {
349
- if (meta.cwd && meta.project && meta.cwd !== meta.project) {
350
- return getGitBranch(meta.cwd) || meta.gitBranch || null;
351
- }
352
- return meta.gitBranch || null;
353
- }
354
-
355
346
  function getSessionLogStat(meta) {
356
347
  if (!meta.jsonlPath) return { mtime: null, hasMessages: false };
357
348
  try {
@@ -1168,6 +1159,7 @@ function buildSessionObject(id, meta, overrides = {}) {
1168
1159
  const logStat = overrides._logStat || getSessionLogStat(meta);
1169
1160
  const logMtime = logStat.mtime;
1170
1161
  const logAge = logMtime ? Date.now() - logMtime : Infinity;
1162
+ const worktree = worktrees.resolve(meta.project);
1171
1163
  return {
1172
1164
  id,
1173
1165
  name: getSessionDisplayName(id, meta),
@@ -1176,8 +1168,8 @@ function buildSessionObject(id, meta, overrides = {}) {
1176
1168
  project: meta.project || null,
1177
1169
  cwd: meta.cwd || null,
1178
1170
  description: meta.description || null,
1179
- gitBranch: resolveSessionGitBranch(meta),
1180
- worktree: worktrees.resolve(meta.project),
1171
+ gitBranch: sessionGitBranch(meta, !!worktree, getGitBranch),
1172
+ worktree,
1181
1173
  customTitle: meta.customTitle || null,
1182
1174
  taskCount: 0,
1183
1175
  completed: 0,
@@ -1631,6 +1623,30 @@ app.get('/api/sessions/search', (req, res) => {
1631
1623
  res.json(hits.slice(0, SESSION_SEARCH_MAX).map((h) => h.id));
1632
1624
  });
1633
1625
 
1626
+ app.get('/api/sessions/known', (_req, res) => {
1627
+ res.setHeader('Cache-Control', 'no-store');
1628
+ try {
1629
+ const known = new Map();
1630
+ for (const [id, meta] of Object.entries(loadSessionMetadata())) {
1631
+ known.set(id, { id, project: meta.project || null, name: getSessionDisplayName(id, meta) });
1632
+ }
1633
+ const add = (id, project = null) => {
1634
+ if (!known.has(id)) known.set(id, { id, project, name: null });
1635
+ };
1636
+ for (const dir of [TASKS_DIR, AGENT_ACTIVITY_DIR]) {
1637
+ if (!existsSync(dir)) continue;
1638
+ for (const d of readdirSync(dir, { withFileTypes: true })) if (d.isDirectory()) add(d.name);
1639
+ }
1640
+ for (const map of Object.values(loadAllTaskMaps().listToSessions)) {
1641
+ for (const [id, info] of Object.entries(map)) add(id, info.project || null);
1642
+ }
1643
+ res.json([...known.values()]);
1644
+ } catch (error) {
1645
+ console.error('Error listing known sessions:', error);
1646
+ res.status(500).json({ error: 'Failed to list known sessions' });
1647
+ }
1648
+ });
1649
+
1634
1650
  function isTempPath(p) {
1635
1651
  const rel = path.relative(TEMP_ROOT, p);
1636
1652
  return !!rel && !rel.startsWith('..') && !path.isAbsolute(rel);
@@ -3276,7 +3292,9 @@ const terminal = createTerminalService({
3276
3292
  isAllowedFolder,
3277
3293
  liveSessions: () => loadLiveSessions(true),
3278
3294
  onChange: () => broadcast({ type: 'terminals-update', ids: terminal.list().map((t) => t.id) }),
3279
- onExit: (id) => dispatches.sessionExited(id),
3295
+ onExit: (id) => {
3296
+ if (dispatches.remove(id)) broadcast({ type: 'dispatch-update' });
3297
+ },
3280
3298
  });
3281
3299
 
3282
3300
  let folderDialogOpen = false;
@@ -3346,13 +3364,7 @@ const dispatched = createDispatchedStore({
3346
3364
  save: (data) => writeJsonAtomic(DISPATCHED_FILE, data),
3347
3365
  });
3348
3366
 
3349
- const dispatches = createDispatchRegistry({
3350
- onSettle: (r) => {
3351
- if (r.parent && r.report) enqueueSessionEvent(topicKey('dispatch', r.parent), formatDispatchLine(r));
3352
- if (r.session) dispatched.settle(r.session, r.status);
3353
- broadcast({ type: 'dispatch-update' });
3354
- },
3355
- });
3367
+ const dispatches = createDispatchRegistry();
3356
3368
 
3357
3369
  const dispatchGroups = createGroupStore({
3358
3370
  load: () => {
@@ -3371,8 +3383,6 @@ const linkedDocs = createLinkedDocStore({
3371
3383
  });
3372
3384
 
3373
3385
  // The board places a session from these alone, so it never has to move it later.
3374
- // `startedBy` lets it follow a starter the user put in a named group, which only the
3375
- // browser knows; it goes once the dispatch settles. `dispatched` stays for the card's marker.
3376
3386
  function withDispatchPlacement(sessions) {
3377
3387
  const groups = dispatchGroups.snapshot();
3378
3388
  return sessions.map((s) => {
@@ -3382,56 +3392,35 @@ function withDispatchPlacement(sessions) {
3382
3392
  return {
3383
3393
  ...s,
3384
3394
  dispatchGroup,
3385
- startedBy: marker?.status === 'running' ? marker.parent || undefined : undefined,
3386
- dispatched: marker ? { parent: marker.parent, outcome: DISPATCH_OUTCOME[marker.status] } : undefined,
3395
+ dispatched: marker ? { parent: marker.parent } : undefined,
3387
3396
  };
3388
3397
  });
3389
3398
  }
3390
3399
 
3391
3400
  // Starting a session needs the terminal token, as the browser does; the CLI reads it from
3392
- // TERMINAL_TOKEN_FILE. The child gets only its dispatch capability through the preamble.
3401
+ // TERMINAL_TOKEN_FILE. The started session never holds it.
3393
3402
  app.post('/api/dispatch', (req, res) => {
3394
3403
  if (!terminal.authorized(req.get('x-terminal-token'))) return res.status(401).json({ error: 'invalid terminal token' });
3395
- const { cwd, spec, name, model, worktree, parent, group, report, peer } = req.body || {};
3404
+ const { cwd, spec, name, model, worktree, parent, group, claudeArgs } = req.body || {};
3396
3405
  if (typeof spec !== 'string' || !spec.trim()) return res.status(400).json({ error: 'spec is required' });
3397
- if (peer != null && !isPeerName(peer)) return res.status(400).json({ error: 'peer must be a peer name as ListAgents prints it' });
3398
3406
  if (parent != null && !(typeof parent === 'string' && isUUID(parent))) return res.status(400).json({ error: 'invalid parent' });
3399
3407
  if (group != null && !isGroupName(group)) {
3400
3408
  return res.status(400).json({ error: `group must be kebab-case, e.g. ${suggestGroupName(group) || 'my-group'}` });
3401
3409
  }
3402
- const target = group || (parent && dispatchGroups.groupOf(parent)) || null;
3403
- const r = dispatches.create({ parent, name, spec: spec.trim(), report: report === true, peer, group: target, worktree });
3404
- const env = { CCK_DISPATCH_ID: r.id, ...(parent && { PARENT_SESSION_ID: parent }) };
3405
- const started = terminal.startNew({ cwd, name, model, worktree, prompt: formatPreamble(r) }, env);
3406
- if (started.error) {
3407
- dispatches.discard(r.id);
3408
- return res.status(started.status).json({ error: started.error });
3409
- }
3410
- dispatches.attach(r.id, { session: started.id, cwd: started.cwd });
3410
+ const started = terminal.startNew({ cwd, name, model, worktree, prompt: spec.trim(), extraArgs: claudeArgs });
3411
+ if (started.error) return res.status(started.status).json({ error: started.error });
3412
+ dispatches.add({ session: started.id, parent, cwd: started.cwd, name, group: group || null, worktree });
3411
3413
  dispatched.record(started.id, parent);
3412
3414
  // The starter stays where it is: moving it would jump it under the user.
3413
- if (target) dispatchGroups.join(target, [started.id], parent);
3415
+ if (group) dispatchGroups.join(group, [started.id]);
3414
3416
  broadcast({ type: 'dispatch-update' });
3415
- res.status(201).json({ dispatch: r.id, session: started.id, cwd: started.cwd, group: target });
3417
+ res.status(201).json({ session: started.id, cwd: started.cwd, group: group || null });
3416
3418
  });
3417
3419
 
3418
- app.post('/api/dispatch/:id/done', (req, res) => {
3419
- const { cap, outcome, summary } = req.body || {};
3420
- const err = dispatches.settle(req.params.id, cap, outcome, summary);
3421
- if (err) return res.status(err.status).json({ error: err.error });
3422
- res.status(204).end();
3423
- });
3424
-
3425
- app.get('/api/dispatch', async (req, res) => {
3420
+ app.get('/api/dispatch', (req, res) => {
3426
3421
  res.setHeader('Cache-Control', 'no-store');
3427
- const ids = typeof req.query.ids === 'string' && req.query.ids ? req.query.ids.split(',') : null;
3428
3422
  const parent = typeof req.query.parent === 'string' && req.query.parent ? req.query.parent : null;
3429
- // res, not req: a GET's request stream can close before the response is sent.
3430
- const out = await dispatches.wait({ ids, parent }, req.query.wait, (stop) => {
3431
- res.on('close', stop);
3432
- return () => res.off('close', stop);
3433
- });
3434
- if (!res.writableEnded) res.json(out);
3423
+ res.json({ running: dispatches.list({ parent }) });
3435
3424
  });
3436
3425
  // #endregion
3437
3426
 
@@ -3496,7 +3485,6 @@ const {
3496
3485
  formatTaskMoved,
3497
3486
  handleSessionEvents,
3498
3487
  hasSessionListener,
3499
- topicKey,
3500
3488
  } = require('./lib/session-events');
3501
3489
  app.get('/api/sessions/:sessionId/events', handleSessionEvents);
3502
3490
 
@@ -1,73 +1,35 @@
1
1
  # Dispatch guide
2
2
 
3
- A dispatch is one Claude Code session that cck starts for a task, in its embedded terminal. It is an ordinary session, not a child: it shows in the sidebar like any other, and the user can open its terminal at any time.
3
+ A dispatch is a plain Claude Code session that cck starts in its embedded terminal, with your spec as the first message. It is an ordinary session: it shows in the sidebar, its card links back to you, and the user can open its terminal at any time. cck only starts it.
4
4
 
5
- A dispatch **reports back** by default: start it with `--report` and `--peer`, then collect and verify the outcome (With `--report`, below). When the user's request says `--no-report`, it is **fire-and-forget**: start it with neither flag (Fire-and-forget, below). `--no-report` lives only in the user's request; `dispatch start` has no such flag.
5
+ ## Spec
6
6
 
7
- ## Write the spec
7
+ The started session sees only the spec, not this conversation, so the spec carries everything it needs.
8
8
 
9
- The started session sees only the spec, not this conversation, so every spec is self-contained. Name:
10
-
11
- - **Target:** the files, component, or environment in scope.
12
- - **Change:** the concrete result to produce.
13
- - **Constraints:** invariants and do-not-touch boundaries.
14
- - **Ownership:** what it may edit. Two dispatches edit the same files only when each runs in its own `--worktree`.
15
- - **Acceptance:** the test, output, or evidence that proves it is done.
16
-
17
- Dispatch when the task can run on its own. Do the work yourself when it is small or needs context from this conversation that you cannot write down.
9
+ cck sends nothing back. To hear from the session, the spec tells it to `SendMessage` you and names you. Your name is auto-assigned (e.g. `claude-code-hub-06`); `ListAgents` prints it as "This session is <name>".
18
10
 
19
11
  ## Start
20
12
 
21
13
  ```bash
22
- claude-code-kanban dispatch start --cwd <dir> --spec-file <spec.md> --name <name> --group <group> --peer <your-peer> --report --json
14
+ claude-code-kanban dispatch start --cwd <dir> --spec-file <spec.md> --name <name> --group <group> --json -- <claude args>
23
15
  ```
24
16
 
25
- `claude-code-kanban help dispatch start` lists every flag (model, worktree, and the rest). `project list` shows the folders `--cwd` accepts. How to choose the values:
26
-
27
- - `--peer` is your own peer name: the first line of `ListAgents` ("This session is `<name>`"). Pass it when you have the `ListAgents` tool. cck then tells the started session to ask you with `SendMessage` instead of failing on a question. See [Peer](#peer).
28
- - `--spec-file` over `--spec` for anything longer than a line: no shell quoting.
29
- - `--name` is what the user sees in the sidebar. Kebab-case, saying what the session does: `fix-login-redirect`, not `task-1`.
30
- - `--group` names the effort, in kebab-case (`auth-refactor`), and shows the new session under that sidebar group. This session stays where it is. Pass it on your first dispatch; later dispatches join the same group without it. A group goes away when its sessions end, unless the user pins a member or keeps the group.
31
- - The result holds the `dispatch` id and the `session` id.
32
-
33
- ## Fire-and-forget
17
+ - `--name` is the sidebar name and the session's peer name. Kebab-case and unique: `fix-login-redirect`.
18
+ - `--group` names the effort in kebab-case (`auth-refactor`) and shows the session under that sidebar group. Pass it on every dispatch that belongs to the effort; a dispatch without it goes to its project.
19
+ - `--spec-file` keeps a long spec out of shell quoting.
20
+ - 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
+ - After a cck restart the terminal comes back with `claude --resume <id>` alone, so the args after `--` apply to the first run only.
22
+ - The result holds the `session` id.
34
23
 
35
- The started session gets the task alone and does not know your session exists. Tell the user the session name, its group, and the dispatch id. Your part ends with that message: the user follows the dispatch in the sidebar, and asks you when they want it checked.
24
+ `claude-code-kanban help dispatch start` lists cck's flags (`--model`, `--worktree` and more); `claude --help` lists the ones you can pass after `--`.
36
25
 
37
- ## With `--report`
26
+ ## Messages and status
38
27
 
39
- The started session settles with one report, `succeeded` or `failed`, or as `exited` when its terminal ends first. Start every independent dispatch first, then collect. Two channels, use either or both:
40
-
41
- - **Inbox:** this skill armed it. Lines `[kanban board] Dispatch <id> (session <uuid>) ...` arrive on their own while you keep working.
42
- - **Wait:** block until one settles.
43
-
44
- ```bash
45
- claude-code-kanban dispatch wait [<id>...] --timeout 15m --json
46
- ```
28
+ A message from the session arrives here as a new turn. Reply with `SendMessage` to its name.
47
29
 
48
- Call it again with the ids still running (`help dispatch wait` has the output fields). A timeout is a checkpoint: the session may still be working. Look before you act:
30
+ A crashed session sends nothing. To learn when one ends, `SendMessage` it with `notify_when_idle: true`, or look:
49
31
 
50
32
  ```bash
51
- claude-code-kanban dispatch list --json
33
+ claude-code-kanban dispatch list --json # still running in cck's terminal
52
34
  claude-code-kanban session peek <session-id> --limit 20
53
35
  ```
54
-
55
- A dispatch still `running` is still working; retry only after a `failed` report or an `exited` one.
56
-
57
- The summary is the started session's own claim. Verify it (run the tests, read the diff), then give the user each dispatch's outcome, the summary, and what you checked. Done when every `--report` dispatch has settled and each summary is verified.
58
-
59
- ## Peer
60
-
61
- A dispatch is a Claude Code peer under its `--name`, so `SendMessage` reaches it and it reaches you. The peer channel carries the conversation. The report carries the record: only `dispatch done` settles a dispatch, ends `dispatch wait`, and shows in the sidebar.
62
-
63
- - **Answer questions.** A question or a finding from the dispatch arrives as a new turn. Answer it yourself, or ask the user when the decision is theirs, then send the answer back.
64
- - **Steer.** Send a short, self-contained message to the dispatch's name. It arrives between the receiver's steps, never inside a subagent or a running workflow.
65
- - **Limits.** A session in another permission mode can hold a message until its user approves it, so anything the result depends on goes in the report. A dispatch that restarts ends as `exited` and cannot report, so its result comes back as a message.
66
-
67
- ## If you are the started session
68
-
69
- Your prompt begins with `[cck dispatch <id>]` and holds your instructions: the peer to ask, and with a report the exact `dispatch done` command. Follow them. Without that line, the prompt is the task alone.
70
-
71
- - Ask the peer with `SendMessage` when you need a decision or find something that changes the task, and keep working on what does not depend on the answer.
72
- - The summary is three sentences: what changed, what you found, what remains. Use `--summary-file` if it needs quotes.
73
- - After you report, a message from the peer is a new request: answer it.