thinkpool-pair 0.6.3 → 0.6.5
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/bridge.mjs +26 -12
- package/claude-session.mjs +31 -0
- package/package.json +1 -1
package/bridge.mjs
CHANGED
|
@@ -327,7 +327,7 @@ function openStructured({ id, model, resume }) {
|
|
|
327
327
|
},
|
|
328
328
|
requestPermission: (req) => new Promise((resolve) => {
|
|
329
329
|
entry.pending.set(req.id, resolve)
|
|
330
|
-
bcast('code-perm-req', { term: id, id: req.id, toolName: req.toolName, input: req.input, risk: req.risk })
|
|
330
|
+
bcast('code-perm-req', { term: id, id: req.id, toolName: req.toolName, input: req.input, risk: req.risk, plan: req.plan })
|
|
331
331
|
}),
|
|
332
332
|
})
|
|
333
333
|
announce()
|
|
@@ -444,22 +444,36 @@ channel
|
|
|
444
444
|
})
|
|
445
445
|
.on('broadcast', { event: 'code-turn' }, ({ payload }) => {
|
|
446
446
|
const s = payload?.term && sessions.get(payload.term)
|
|
447
|
-
if (s
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
if (
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
447
|
+
if (!s || payload.text == null) return
|
|
448
|
+
const text = String(payload.text)
|
|
449
|
+
// Echo the turn so BOTH readers (and late joiners, via the log) show who
|
|
450
|
+
// said what — UNLESS silent (an @pool synthesis, which renders its own line).
|
|
451
|
+
const echoYou = () => {
|
|
452
|
+
if (payload.silent) return
|
|
453
|
+
const evt = { kind: 'you', text, cid: payload.cid, by: payload.by }
|
|
454
|
+
s.log.push(evt); if (s.log.length > STRUCTURED_LOG_MAX) s.log.shift()
|
|
455
|
+
bcast('code-event', { term: payload.term, evt })
|
|
456
|
+
}
|
|
457
|
+
// Most slash commands (/compact, /clear, …) are processed by the SDK when
|
|
458
|
+
// sent as a turn. Two need special handling:
|
|
459
|
+
// • /model <name> is disabled headless → route to the setModel control.
|
|
460
|
+
// • /clear also wipes the on-screen transcript (parity with the CLI).
|
|
461
|
+
const mm = text.match(/^\/model\b\s*(\S+)?/)
|
|
462
|
+
if (mm) { echoYou(); if (mm[1]) s.session.setModel(mm[1]); else s.session.listModels(); return }
|
|
463
|
+
if (/^\/clear\s*$/.test(text)) {
|
|
464
|
+
echoYou(); s.session.sendTurn(text)
|
|
465
|
+
s.log = []; bcast('code-event', { term: payload.term, evt: { kind: 'clear' } })
|
|
466
|
+
return
|
|
457
467
|
}
|
|
468
|
+
s.session.sendTurn(text)
|
|
469
|
+
echoYou()
|
|
458
470
|
})
|
|
459
471
|
.on('broadcast', { event: 'code-perm' }, ({ payload }) => {
|
|
460
472
|
const s = payload?.term && sessions.get(payload.term)
|
|
461
473
|
const resolve = s && payload.id && s.pending.get(payload.id)
|
|
462
|
-
|
|
474
|
+
// Pass the raw decision through — normal tools use allow/deny, plan cards
|
|
475
|
+
// use run/accept/keep (claude-session interprets). Default deny on missing.
|
|
476
|
+
if (resolve) { s.pending.delete(payload.id); resolve(payload.decision || 'deny') }
|
|
463
477
|
})
|
|
464
478
|
.on('broadcast', { event: 'code-abort' }, ({ payload }) => {
|
|
465
479
|
const s = payload?.term && sessions.get(payload.term)
|
package/claude-session.mjs
CHANGED
|
@@ -101,6 +101,24 @@ export function startClaudeSession({ cwd, model, resume, onEvent, requestPermiss
|
|
|
101
101
|
const preTool = async (hookInput) => {
|
|
102
102
|
const toolName = hookInput.tool_name
|
|
103
103
|
const toolInput = hookInput.tool_input
|
|
104
|
+
// ── Plan approval — ExitPlanMode is how the agent presents its plan in
|
|
105
|
+
// plan mode. Render a dedicated plan card (not the generic perm card) with
|
|
106
|
+
// three outcomes: run (exit → default), accept (exit → acceptEdits), keep
|
|
107
|
+
// (deny → stay planning). On approval we flip the SDK permission mode so
|
|
108
|
+
// subsequent tools actually execute.
|
|
109
|
+
if (toolName === 'ExitPlanMode') {
|
|
110
|
+
let choice = 'keep'
|
|
111
|
+
try { choice = await requestPermission?.({ id: randomUUID(), toolName, input: toolInput, risk: 'plan', plan: toolInput?.plan || '' }) ?? 'keep' }
|
|
112
|
+
catch { choice = 'keep' }
|
|
113
|
+
if (choice === 'run' || choice === 'accept') {
|
|
114
|
+
const next = choice === 'accept' ? 'acceptEdits' : 'default'
|
|
115
|
+
mode = next
|
|
116
|
+
try { await q?.setPermissionMode?.(next) } catch { /* only valid mid-stream */ }
|
|
117
|
+
emit({ kind: 'mode', mode: next })
|
|
118
|
+
return { continue: true, hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow', permissionDecisionReason: `Plan approved in the ThinkPool room — proceed (${next} mode).` } }
|
|
119
|
+
}
|
|
120
|
+
return { continue: true, hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: 'The user chose "keep planning" in the ThinkPool room. Do not exit plan mode — keep refining the plan, then call ExitPlanMode again when ready.' } }
|
|
121
|
+
}
|
|
104
122
|
const risk = classifyRisk(toolName, toolInput)
|
|
105
123
|
const auto =
|
|
106
124
|
risk === 'low' ||
|
|
@@ -181,6 +199,19 @@ export function startClaudeSession({ cwd, model, resume, onEvent, requestPermiss
|
|
|
181
199
|
try { await q?.setPermissionMode?.(m) } catch { /* only valid mid-stream */ }
|
|
182
200
|
emit({ kind: 'mode', mode })
|
|
183
201
|
},
|
|
202
|
+
// /model is disabled in headless SDK — switch via the setModel control
|
|
203
|
+
// instead, and echo a note line so both drivers see the change.
|
|
204
|
+
async setModel(m) {
|
|
205
|
+
try { await q?.setModel?.(m) } catch { /* may reject unknown model */ }
|
|
206
|
+
emit({ kind: 'note', text: `model → ${m}` })
|
|
207
|
+
},
|
|
208
|
+
async listModels() {
|
|
209
|
+
try {
|
|
210
|
+
const ms = await q?.supportedModels?.()
|
|
211
|
+
const names = (ms || []).map((x) => x.model || x.id || x.name).filter(Boolean)
|
|
212
|
+
emit({ kind: 'note', text: names.length ? `models: ${names.join(', ')}` : 'no model list available' })
|
|
213
|
+
} catch { emit({ kind: 'note', text: 'usage: /model <name>' }) }
|
|
214
|
+
},
|
|
184
215
|
// Graceful interrupt (Esc / Stop) — stops the current turn but keeps the
|
|
185
216
|
// session alive for the next one. ac.abort() is teardown only (end()).
|
|
186
217
|
async abort() { try { await q?.interrupt?.() } catch { /* noop */ } },
|