@dfosco/storyboard 0.6.0-beta.1 → 0.6.0-beta.3
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
CHANGED
|
@@ -74,7 +74,9 @@ Clients on 4.1.x likely have no `canvas` block at all. The full canvas config is
|
|
|
74
74
|
"label": "Claude Code",
|
|
75
75
|
"icon": "claude",
|
|
76
76
|
"startupCommand": "claude --agent terminal-agent --dangerously-skip-permissions",
|
|
77
|
-
"resumeCommand": "claude --resume
|
|
77
|
+
"resumeCommand": "claude --resume {id} --agent terminal-agent --dangerously-skip-permissions",
|
|
78
|
+
"sessionIdEnv": "CLAUDE_SESSION_ID",
|
|
79
|
+
"sessionStateGlob": "~/.claude/projects/*/{id}.jsonl",
|
|
78
80
|
"resizable": true,
|
|
79
81
|
"readinessSignal": "bypass permissions"
|
|
80
82
|
},
|
|
@@ -82,7 +84,9 @@ Clients on 4.1.x likely have no `canvas` block at all. The full canvas config is
|
|
|
82
84
|
"label": "Codex CLI",
|
|
83
85
|
"icon": "codex",
|
|
84
86
|
"startupCommand": "codex --full-auto",
|
|
85
|
-
"resumeCommand": "codex
|
|
87
|
+
"resumeCommand": "codex resume {id}",
|
|
88
|
+
"sessionIdEnv": "CODEX_SESSION_ID",
|
|
89
|
+
"sessionStateGlob": "~/.codex/sessions/**/rollout-*-{id}.jsonl",
|
|
86
90
|
"configFiles": [".codex/config.toml"],
|
|
87
91
|
"resizable": true
|
|
88
92
|
}
|
|
@@ -116,6 +120,10 @@ Clients on 4.1.x likely have no `canvas` block at all. The full canvas config is
|
|
|
116
120
|
| `startupCommand` | yes | Shell command to start the agent |
|
|
117
121
|
| `resumeCommand` | no | Full launch template to resume a session, with `{id}` placeholder (e.g. `copilot --resume={id} --agent terminal-agent`). Used both for auto-resume on cold restart and for the interactive "Browse existing sessions" flow. |
|
|
118
122
|
| `sessionIdEnv` | no | Env var exposed in the agent SessionStart hook payload that holds its session id (e.g. `COPILOT_AGENT_SESSION_ID`). When set, widget cold restarts auto-resume the previous session. |
|
|
123
|
+
| `sessionStateDir` | no | Directory where the agent stores per-session state, used to pre-flight `--resume` (e.g. `~/.copilot/session-state`). Pass `null` to skip the fs check (UUID-only validation). |
|
|
124
|
+
| `sessionStateGlob` | no | Glob to validate session existence for agents that store sessions in nested subdirs. Supports `<root>/*/{id}.jsonl` (Claude) and `<root>/**/<name-with-{id}>` (Codex). |
|
|
125
|
+
|
|
126
|
+
**Codex CLI: one-time hook trust.** Codex requires explicit user trust for non-managed hooks (`Non-managed command hooks must be reviewed and trusted before they run`). After the first dev-server boot, run `codex` interactively in any directory and enter `/hooks`, navigate to SessionStart, and enable the `storyboard-capture` hook. Trust persists in `~/.codex/state_*.sqlite`. Until trusted, Codex agent widgets will launch fresh on restart instead of resuming.
|
|
119
127
|
| `postStartup` | no | Text sent to the agent's stdin after it starts |
|
|
120
128
|
| `readinessSignal` | no | Substring to wait for in output before marking agent as ready |
|
|
121
129
|
| `configFiles` | no | Array of config file paths the agent requires |
|
|
@@ -18,14 +18,18 @@
|
|
|
18
18
|
* pre-validate).
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, watch as fsWatch } from 'node:fs'
|
|
21
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, watch as fsWatch, readdirSync, statSync } from 'node:fs'
|
|
22
22
|
import { join } from 'node:path'
|
|
23
23
|
import { homedir } from 'node:os'
|
|
24
|
+
import { execFileSync } from 'node:child_process'
|
|
24
25
|
|
|
25
26
|
const CAPTURE_DIR = join('.storyboard', 'agent-sessions')
|
|
26
27
|
const COPILOT_USER_HOOKS_DIR = join(homedir(), '.copilot', 'hooks')
|
|
27
28
|
const COPILOT_HOOK_FILENAME = 'storyboard-capture.json'
|
|
28
29
|
const COPILOT_SESSION_STATE_DIR = join(homedir(), '.copilot', 'session-state')
|
|
30
|
+
const CLAUDE_SETTINGS_PATH = join(homedir(), '.claude', 'settings.json')
|
|
31
|
+
const CLAUDE_HOOK_MARKER = 'storyboard-capture'
|
|
32
|
+
const CODEX_HOOKS_PATH = join(homedir(), '.codex', 'hooks.json')
|
|
29
33
|
|
|
30
34
|
/** Resolve absolute path to the per-widget capture directory under root. */
|
|
31
35
|
function captureDir(root) {
|
|
@@ -62,24 +66,11 @@ export function ensureCopilotCaptureHookInstalled() {
|
|
|
62
66
|
|
|
63
67
|
const hookPath = join(COPILOT_USER_HOOKS_DIR, COPILOT_HOOK_FILENAME)
|
|
64
68
|
|
|
65
|
-
// Single-line bash script. Reads stdin JSON, extracts sessionId via sed
|
|
66
|
-
// (no jq dependency), writes it to the widget's per-project capture file.
|
|
67
|
-
const bashScript =
|
|
68
|
-
'wid="${STORYBOARD_WIDGET_ID}"; ' +
|
|
69
|
-
'root="${STORYBOARD_PROJECT_ROOT}"; ' +
|
|
70
|
-
'[ -z "$wid" ] && exit 0; ' +
|
|
71
|
-
'[ -z "$root" ] && exit 0; ' +
|
|
72
|
-
'id=$(cat | sed -n \'s/.*"sessionId"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p\' | head -n1); ' +
|
|
73
|
-
'[ -z "$id" ] && exit 0; ' +
|
|
74
|
-
'dir="$root/.storyboard/agent-sessions"; ' +
|
|
75
|
-
'mkdir -p "$dir" 2>/dev/null; ' +
|
|
76
|
-
'printf %s "$id" > "$dir/$wid.session-id"'
|
|
77
|
-
|
|
78
69
|
const hook = {
|
|
79
70
|
version: 1,
|
|
80
71
|
hooks: {
|
|
81
72
|
sessionStart: [
|
|
82
|
-
{ type: 'command', bash:
|
|
73
|
+
{ type: 'command', bash: buildCaptureBashScript(), timeoutSec: 5 },
|
|
83
74
|
],
|
|
84
75
|
},
|
|
85
76
|
}
|
|
@@ -94,6 +85,121 @@ export function ensureCopilotCaptureHookInstalled() {
|
|
|
94
85
|
return hookPath
|
|
95
86
|
}
|
|
96
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Install (idempotently) a SessionStart hook in `~/.claude/settings.json`
|
|
90
|
+
* that captures Claude Code session ids the same way the Copilot hook does.
|
|
91
|
+
*
|
|
92
|
+
* Claude's hook payload uses `session_id` (snake_case) instead of Copilot's
|
|
93
|
+
* `sessionId` — our capture script handles both. Claude reads the hook from
|
|
94
|
+
* `~/.claude/settings.json` and merges it with project / local / managed
|
|
95
|
+
* settings, so we install user-scope and let Claude do the merging.
|
|
96
|
+
*
|
|
97
|
+
* Existing settings are preserved: we read, deep-merge our hook into the
|
|
98
|
+
* SessionStart array (replacing any prior storyboard hook by `command`
|
|
99
|
+
* marker), and write back.
|
|
100
|
+
*
|
|
101
|
+
* Safe to call on every dev-server boot.
|
|
102
|
+
*/
|
|
103
|
+
export function ensureClaudeCaptureHookInstalled() {
|
|
104
|
+
let settings = {}
|
|
105
|
+
try {
|
|
106
|
+
settings = JSON.parse(readFileSync(CLAUDE_SETTINGS_PATH, 'utf8'))
|
|
107
|
+
} catch { /* file may not exist or be invalid — start fresh */ }
|
|
108
|
+
|
|
109
|
+
if (typeof settings !== 'object' || settings === null) settings = {}
|
|
110
|
+
if (typeof settings.hooks !== 'object' || settings.hooks === null) settings.hooks = {}
|
|
111
|
+
if (!Array.isArray(settings.hooks.SessionStart)) settings.hooks.SessionStart = []
|
|
112
|
+
|
|
113
|
+
const ourHandler = {
|
|
114
|
+
type: 'command',
|
|
115
|
+
command: buildCaptureBashScript(),
|
|
116
|
+
timeout: 5,
|
|
117
|
+
}
|
|
118
|
+
// Claude wraps handlers in a matcher group. Use no matcher (matches all).
|
|
119
|
+
const ourGroup = { hooks: [ourHandler] }
|
|
120
|
+
|
|
121
|
+
// Replace any prior storyboard-capture group; identify by marker substring.
|
|
122
|
+
const next = settings.hooks.SessionStart.filter((g) => {
|
|
123
|
+
const handlers = Array.isArray(g?.hooks) ? g.hooks : []
|
|
124
|
+
return !handlers.some((h) => typeof h?.command === 'string' && h.command.includes(CLAUDE_HOOK_MARKER))
|
|
125
|
+
})
|
|
126
|
+
// Tag our handler command with the marker so we can find/replace it later.
|
|
127
|
+
ourHandler.command = `# ${CLAUDE_HOOK_MARKER}\n${ourHandler.command}`
|
|
128
|
+
next.push(ourGroup)
|
|
129
|
+
|
|
130
|
+
settings.hooks.SessionStart = next
|
|
131
|
+
|
|
132
|
+
try { mkdirSync(join(homedir(), '.claude'), { recursive: true }) } catch { /* empty */ }
|
|
133
|
+
try {
|
|
134
|
+
writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n')
|
|
135
|
+
} catch { /* best-effort */ }
|
|
136
|
+
return CLAUDE_SETTINGS_PATH
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Install (idempotently) a SessionStart hook for Codex CLI at
|
|
141
|
+
* `~/.codex/hooks.json` using the same shared capture script.
|
|
142
|
+
*
|
|
143
|
+
* Codex's hook format is JSON with PascalCase event names like Claude's
|
|
144
|
+
* (and uses `session_id` snake_case in the payload, also like Claude).
|
|
145
|
+
* We own this file end-to-end (Codex merges multiple hook sources, so
|
|
146
|
+
* other hooks the user has via config.toml or repo-level files keep
|
|
147
|
+
* working). The marker comment identifies our handler for replace.
|
|
148
|
+
*/
|
|
149
|
+
export function ensureCodexCaptureHookInstalled() {
|
|
150
|
+
let hooks = { hooks: { SessionStart: [] } }
|
|
151
|
+
try {
|
|
152
|
+
const existing = JSON.parse(readFileSync(CODEX_HOOKS_PATH, 'utf8'))
|
|
153
|
+
if (existing && typeof existing === 'object') hooks = existing
|
|
154
|
+
} catch { /* file may not exist — start fresh */ }
|
|
155
|
+
|
|
156
|
+
if (typeof hooks.hooks !== 'object' || hooks.hooks === null) hooks.hooks = {}
|
|
157
|
+
if (!Array.isArray(hooks.hooks.SessionStart)) hooks.hooks.SessionStart = []
|
|
158
|
+
|
|
159
|
+
const ourHandler = {
|
|
160
|
+
type: 'command',
|
|
161
|
+
command: `# ${CLAUDE_HOOK_MARKER}\n${buildCaptureBashScript()}`,
|
|
162
|
+
timeout: 5,
|
|
163
|
+
}
|
|
164
|
+
// Codex matchers for SessionStart: "startup", "resume", "clear" — match all
|
|
165
|
+
const ourGroup = { matcher: '*', hooks: [ourHandler] }
|
|
166
|
+
|
|
167
|
+
hooks.hooks.SessionStart = hooks.hooks.SessionStart.filter((g) => {
|
|
168
|
+
const handlers = Array.isArray(g?.hooks) ? g.hooks : []
|
|
169
|
+
return !handlers.some((h) => typeof h?.command === 'string' && h.command.includes(CLAUDE_HOOK_MARKER))
|
|
170
|
+
})
|
|
171
|
+
hooks.hooks.SessionStart.push(ourGroup)
|
|
172
|
+
|
|
173
|
+
try { mkdirSync(join(homedir(), '.codex'), { recursive: true }) } catch { /* empty */ }
|
|
174
|
+
try {
|
|
175
|
+
writeFileSync(CODEX_HOOKS_PATH, JSON.stringify(hooks, null, 2) + '\n')
|
|
176
|
+
} catch { /* best-effort */ }
|
|
177
|
+
return CODEX_HOOKS_PATH
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Shared capture bash script. Handles both Copilot (`sessionId` camelCase)
|
|
182
|
+
* and Claude (`session_id` snake_case) payload shapes. Reads
|
|
183
|
+
* STORYBOARD_WIDGET_ID + STORYBOARD_PROJECT_ROOT from env (exported by
|
|
184
|
+
* terminal-server into the agent shell). Silently no-ops if either env
|
|
185
|
+
* var is missing.
|
|
186
|
+
*/
|
|
187
|
+
function buildCaptureBashScript() {
|
|
188
|
+
return [
|
|
189
|
+
'wid="${STORYBOARD_WIDGET_ID}"',
|
|
190
|
+
'root="${STORYBOARD_PROJECT_ROOT}"',
|
|
191
|
+
'[ -z "$wid" ] && exit 0',
|
|
192
|
+
'[ -z "$root" ] && exit 0',
|
|
193
|
+
'payload=$(cat)',
|
|
194
|
+
'id=$(printf %s "$payload" | sed -n \'s/.*"sessionId"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p\' | head -n1)',
|
|
195
|
+
'[ -z "$id" ] && id=$(printf %s "$payload" | sed -n \'s/.*"session_id"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p\' | head -n1)',
|
|
196
|
+
'[ -z "$id" ] && exit 0',
|
|
197
|
+
'dir="$root/.storyboard/agent-sessions"',
|
|
198
|
+
'mkdir -p "$dir" 2>/dev/null',
|
|
199
|
+
'printf %s "$id" > "$dir/$wid.session-id"',
|
|
200
|
+
].join('; ')
|
|
201
|
+
}
|
|
202
|
+
|
|
97
203
|
/**
|
|
98
204
|
* Build a resume-aware startup command for an agent.
|
|
99
205
|
*
|
|
@@ -120,13 +226,25 @@ export function buildResumeStartupCommand({ startupCommand, sessionId, agentCfg
|
|
|
120
226
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
121
227
|
|
|
122
228
|
/**
|
|
123
|
-
* Decide whether a captured sessionId is still resumable.
|
|
124
|
-
*
|
|
125
|
-
*
|
|
229
|
+
* Decide whether a captured sessionId is still resumable.
|
|
230
|
+
*
|
|
231
|
+
* Strategies:
|
|
232
|
+
* - `agentCfg.sessionStateDir` → check if `<dir>/<id>` exists (Copilot)
|
|
233
|
+
* - `agentCfg.sessionStateGlob` → check if any
|
|
234
|
+
* `~/.claude/projects/*/<id>.jsonl` exists (Claude)
|
|
235
|
+
* - both null → trust the UUID
|
|
236
|
+
*
|
|
237
|
+
* Defaults to the Copilot session-state dir when nothing is configured.
|
|
126
238
|
*/
|
|
127
239
|
export function isResumableSessionId(sessionId, agentCfg = {}) {
|
|
128
240
|
if (!sessionId) return false
|
|
129
241
|
if (!UUID_RE.test(sessionId)) return false
|
|
242
|
+
|
|
243
|
+
// Explicit glob check (Claude-style: <dir>/*/<id>.jsonl)
|
|
244
|
+
if (agentCfg.sessionStateGlob) {
|
|
245
|
+
return matchesSessionStateGlob(sessionId, agentCfg.sessionStateGlob)
|
|
246
|
+
}
|
|
247
|
+
|
|
130
248
|
const stateDir = agentCfg.sessionStateDir === undefined
|
|
131
249
|
? COPILOT_SESSION_STATE_DIR
|
|
132
250
|
: agentCfg.sessionStateDir
|
|
@@ -134,6 +252,48 @@ export function isResumableSessionId(sessionId, agentCfg = {}) {
|
|
|
134
252
|
return existsSync(join(stateDir, sessionId))
|
|
135
253
|
}
|
|
136
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Check if `<root>/<anySubdir>/<id>.jsonl` exists, where `glob` is a
|
|
257
|
+
* shorthand string. Supports two forms:
|
|
258
|
+
* - `<root>/*` + `/{id}.jsonl` — exactly one subdir level (Claude pattern)
|
|
259
|
+
* - `<root>/**` + `/<name-with-{id}>` — recursive find (Codex pattern, where
|
|
260
|
+
* sessions are nested under year/month/day and the id is embedded in
|
|
261
|
+
* a longer filename like `rollout-<ts>-<id>.jsonl`)
|
|
262
|
+
*/
|
|
263
|
+
function matchesSessionStateGlob(sessionId, glob) {
|
|
264
|
+
const expanded = glob.replace('~', homedir()).replace(/\{id\}/g, sessionId)
|
|
265
|
+
|
|
266
|
+
// Recursive form: root/**/name
|
|
267
|
+
if (expanded.includes('/**/')) {
|
|
268
|
+
const [root, namePattern] = expanded.split('/**/')
|
|
269
|
+
if (!existsSync(root)) return false
|
|
270
|
+
try {
|
|
271
|
+
const out = execFileSync('find', [root, '-name', namePattern, '-print', '-quit'], {
|
|
272
|
+
encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'ignore'],
|
|
273
|
+
})
|
|
274
|
+
return out.trim().length > 0
|
|
275
|
+
} catch { return false }
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Single-level form: root/*/suffix
|
|
279
|
+
const parts = expanded.split('/*/')
|
|
280
|
+
if (parts.length !== 2) {
|
|
281
|
+
// Plain path with no wildcard — direct check.
|
|
282
|
+
return existsSync(expanded)
|
|
283
|
+
}
|
|
284
|
+
const [root, suffix] = parts
|
|
285
|
+
let entries = []
|
|
286
|
+
try { entries = readdirSync(root) } catch { return false }
|
|
287
|
+
for (const name of entries) {
|
|
288
|
+
const full = join(root, name)
|
|
289
|
+
try {
|
|
290
|
+
if (!statSync(full).isDirectory()) continue
|
|
291
|
+
} catch { continue }
|
|
292
|
+
if (existsSync(join(full, suffix))) return true
|
|
293
|
+
}
|
|
294
|
+
return false
|
|
295
|
+
}
|
|
296
|
+
|
|
137
297
|
/**
|
|
138
298
|
* Watch `captureFile` for the agent's session id and call `onCapture(id)`
|
|
139
299
|
* once it appears or is updated. Unlike the previous one-shot version,
|
|
@@ -39,6 +39,32 @@ describe('agent-session', () => {
|
|
|
39
39
|
mkdirSync(join(stateDir, id), { recursive: true })
|
|
40
40
|
expect(isResumableSessionId(id, { sessionStateDir: stateDir })).toBe(true)
|
|
41
41
|
})
|
|
42
|
+
|
|
43
|
+
it('uses sessionStateGlob to validate per-project session files (Claude shape)', () => {
|
|
44
|
+
const id = '11111111-2222-4333-8444-555555555555'
|
|
45
|
+
const { mkdirSync, writeFileSync } = require('node:fs')
|
|
46
|
+
const projectsDir = join(root, 'projects')
|
|
47
|
+
mkdirSync(join(projectsDir, '-Users-foo-some-project'), { recursive: true })
|
|
48
|
+
writeFileSync(join(projectsDir, '-Users-foo-some-project', `${id}.jsonl`), '')
|
|
49
|
+
expect(isResumableSessionId(id, { sessionStateGlob: `${projectsDir}/*/{id}.jsonl` })).toBe(true)
|
|
50
|
+
expect(isResumableSessionId(
|
|
51
|
+
'99999999-2222-4333-8444-555555555555',
|
|
52
|
+
{ sessionStateGlob: `${projectsDir}/*/{id}.jsonl` },
|
|
53
|
+
)).toBe(false)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('uses recursive ** in sessionStateGlob to validate nested session files (Codex shape)', () => {
|
|
57
|
+
const id = '22222222-2222-4333-8444-555555555555'
|
|
58
|
+
const { mkdirSync, writeFileSync } = require('node:fs')
|
|
59
|
+
const sessionsRoot = join(root, 'sessions')
|
|
60
|
+
mkdirSync(join(sessionsRoot, '2026', '05', '15'), { recursive: true })
|
|
61
|
+
writeFileSync(join(sessionsRoot, '2026', '05', '15', `rollout-2026-05-15T10-00-00-${id}.jsonl`), '')
|
|
62
|
+
expect(isResumableSessionId(id, { sessionStateGlob: `${sessionsRoot}/**/rollout-*-{id}.jsonl` })).toBe(true)
|
|
63
|
+
expect(isResumableSessionId(
|
|
64
|
+
'99999999-2222-4333-8444-555555555555',
|
|
65
|
+
{ sessionStateGlob: `${sessionsRoot}/**/rollout-*-{id}.jsonl` },
|
|
66
|
+
)).toBe(false)
|
|
67
|
+
})
|
|
42
68
|
})
|
|
43
69
|
|
|
44
70
|
describe('buildResumeStartupCommand', () => {
|
|
@@ -78,8 +104,7 @@ describe('agent-session', () => {
|
|
|
78
104
|
})
|
|
79
105
|
})
|
|
80
106
|
|
|
81
|
-
describe('captureFilePath / readCapturedSessionId / clearCaptureFile', () => {
|
|
82
|
-
it('writes to .storyboard/agent-sessions/<key>.session-id', () => {
|
|
107
|
+
describe('captureFilePath / readCapturedSessionId / clearCaptureFile', () => { it('writes to .storyboard/agent-sessions/<key>.session-id', () => {
|
|
83
108
|
const cap = captureFilePath(root, 'agent-foo')
|
|
84
109
|
expect(cap).toBe(join(root, '.storyboard', 'agent-sessions', 'agent-foo.session-id'))
|
|
85
110
|
})
|
|
@@ -62,6 +62,8 @@ import {
|
|
|
62
62
|
watchSessionIdFile,
|
|
63
63
|
captureFilePath,
|
|
64
64
|
ensureCopilotCaptureHookInstalled,
|
|
65
|
+
ensureClaudeCaptureHookInstalled,
|
|
66
|
+
ensureCodexCaptureHookInstalled,
|
|
65
67
|
} from './agent-session.js'
|
|
66
68
|
|
|
67
69
|
let pty
|
|
@@ -686,9 +688,13 @@ export function setupTerminalServer(httpServer, base = '/', branch = 'unknown',
|
|
|
686
688
|
initRegistry(root, { gracePeriod: termCfg.orphanGracePeriod })
|
|
687
689
|
initTerminalConfig(root)
|
|
688
690
|
|
|
689
|
-
// Install user-level Copilot CLI hook (~/.copilot/hooks/storyboard-capture.json)
|
|
690
|
-
//
|
|
691
|
+
// Install user-level Copilot CLI hook (~/.copilot/hooks/storyboard-capture.json),
|
|
692
|
+
// Claude Code hook (~/.claude/settings.json), and Codex CLI hook
|
|
693
|
+
// (~/.codex/hooks.json) that capture sessionStart payloads into per-widget
|
|
694
|
+
// files. All idempotent.
|
|
691
695
|
try { ensureCopilotCaptureHookInstalled() } catch { /* best-effort */ }
|
|
696
|
+
try { ensureClaudeCaptureHookInstalled() } catch { /* best-effort */ }
|
|
697
|
+
try { ensureCodexCaptureHookInstalled() } catch { /* best-effort */ }
|
|
692
698
|
|
|
693
699
|
// Best-effort: apply shell-config overrides if a tmux server already exists
|
|
694
700
|
// from a previous dev server run. If no server exists, this fails silently —
|
|
@@ -62,7 +62,9 @@
|
|
|
62
62
|
* @property {string} [readinessSignal] — tmux pane text that signals the agent is ready (fragile, prefer readinessFile)
|
|
63
63
|
* @property {string} [resumeCommand] — full command template to resume a session, with `{id}` placeholder (e.g. `"copilot --resume={id} --agent terminal-agent"`). Used both for auto-resume on cold restart (with `{id}` substituted) and for the interactive "Browse existing sessions" flow (binary derived, `--resume` appended).
|
|
64
64
|
* @property {boolean} [readinessFile] — use a file-based SessionStart hook for readiness (writes --settings with hook, polls for signal file)
|
|
65
|
-
* @property {string} [sessionIdEnv] — env var exposed
|
|
65
|
+
* @property {string} [sessionIdEnv] — env var exposed in the agent's SessionStart hook payload that holds its session id (e.g. "COPILOT_AGENT_SESSION_ID"). When set, the server captures the id per-widget so cold restarts can auto-resume.
|
|
66
|
+
* @property {string} [sessionStateDir] — directory where the agent stores per-session state, used to pre-flight `--resume` (e.g. "~/.copilot/session-state"). Pass `null` to skip the fs check (UUID-only validation).
|
|
67
|
+
* @property {string} [sessionStateGlob] — alternative to sessionStateDir for agents that store sessions under a per-project subdir, with `{id}` placeholder (e.g. "~/.claude/projects/*/{id}.jsonl").
|
|
66
68
|
* @property {boolean} [resizable] — override terminal resizability for this agent
|
|
67
69
|
* @property {number} [defaultWidth] — override default width
|
|
68
70
|
* @property {number} [defaultHeight] — override default height
|