thinkpool-pair 0.7.9 → 0.7.11
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 +53 -0
- package/claude-session.mjs +28 -0
- package/package.json +1 -1
package/bridge.mjs
CHANGED
|
@@ -357,6 +357,59 @@ const flushAll = () => {
|
|
|
357
357
|
}
|
|
358
358
|
const flushTimer = setInterval(flushAll, 35)
|
|
359
359
|
|
|
360
|
+
// ── mockup hand-off ────────────────────────────────────────────────────
|
|
361
|
+
// render.sh (mockup-iterate) drops a manifest JSON into TP_MOCKUP_OUTBOX after
|
|
362
|
+
// a dual-viewport render; we watch that dir, upload the PNG pair + source HTML
|
|
363
|
+
// via /api/code-mockup, and push a `mockup` code-event so both people see a
|
|
364
|
+
// card in the room (inline + gallery) that opens the live sandboxed HTML.
|
|
365
|
+
// Deterministic file contract — no PTY/log scraping. The env var is exported
|
|
366
|
+
// into every agent child (PTY: env: process.env; structured: in-process Bash
|
|
367
|
+
// inherits it). Spec: docs/specs/2026-06-17-code-mockups.md
|
|
368
|
+
const MOCKUP_OUTBOX = path.join(os.tmpdir(), 'thinkpool-mockups', room)
|
|
369
|
+
try {
|
|
370
|
+
fs.mkdirSync(MOCKUP_OUTBOX, { recursive: true })
|
|
371
|
+
process.env.TP_MOCKUP_OUTBOX = MOCKUP_OUTBOX
|
|
372
|
+
} catch { /* mockups stay local if the outbox can't be made */ }
|
|
373
|
+
|
|
374
|
+
const mockupSeen = new Map() // slug → mtimeMs of the last manifest we processed
|
|
375
|
+
const handleManifest = async (file) => {
|
|
376
|
+
if (!file || !file.endsWith('.json')) return
|
|
377
|
+
const full = path.join(MOCKUP_OUTBOX, file)
|
|
378
|
+
let stat
|
|
379
|
+
try { stat = fs.statSync(full) } catch { return } // tmp/removed mid-write
|
|
380
|
+
const slug = file.replace(/\.json$/, '')
|
|
381
|
+
if (mockupSeen.get(slug) === stat.mtimeMs) return // already handled
|
|
382
|
+
mockupSeen.set(slug, stat.mtimeMs)
|
|
383
|
+
// A structured Claude session is the natural home; fall back to any terminal.
|
|
384
|
+
const term = [...sessions.keys()][0] || attachedId || [...terms.keys()][0]
|
|
385
|
+
if (!term) return // nothing to attach the card to yet
|
|
386
|
+
let m
|
|
387
|
+
try { m = JSON.parse(fs.readFileSync(full, 'utf8')) } catch { return }
|
|
388
|
+
if (!m?.slug) return
|
|
389
|
+
const readB64 = (p) => { try { return p && fs.readFileSync(p).toString('base64') } catch { return null } }
|
|
390
|
+
const readTxt = (p) => { try { return p && fs.readFileSync(p, 'utf8') } catch { return null } }
|
|
391
|
+
const cid = randomUUID()
|
|
392
|
+
try {
|
|
393
|
+
const res = await fetch(`${WEB_BASE}/api/code-mockup`, {
|
|
394
|
+
method: 'POST',
|
|
395
|
+
headers: { 'Content-Type': 'application/json' },
|
|
396
|
+
body: JSON.stringify({
|
|
397
|
+
code: room, slug: m.slug, title: m.title, cid, ts: m.ts, term,
|
|
398
|
+
desktopPng: readB64(m.desktop), mobilePng: readB64(m.mobile), html: readTxt(m.html),
|
|
399
|
+
}),
|
|
400
|
+
})
|
|
401
|
+
if (!res.ok) { process.stderr.write(`\n ◇ mockup "${m.slug}" upload failed (${res.status}).\n`); return }
|
|
402
|
+
const { paths } = await res.json()
|
|
403
|
+
bcast('code-event', { term, evt: { kind: 'mockup', __struct: true, cid, term, slug: m.slug, title: m.title || m.slug, ts: m.ts, paths } })
|
|
404
|
+
process.stderr.write(`\n ◆ mockup "${m.title || m.slug}" pushed to the room.\n`)
|
|
405
|
+
} catch (e) {
|
|
406
|
+
process.stderr.write(`\n ◇ mockup "${m.slug}" send failed: ${e?.message || e}\n`)
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
try {
|
|
410
|
+
fs.watch(MOCKUP_OUTBOX, (_evt, file) => { handleManifest(file).catch(() => {}) })
|
|
411
|
+
} catch { /* fs.watch unsupported — mockups simply won't auto-surface */ }
|
|
412
|
+
|
|
360
413
|
// ── shared-PTY geometry ───────────────────────────────────────────
|
|
361
414
|
// The attached PTY is CAPPED at 120×32, never pinned (2026-06-11).
|
|
362
415
|
// Mangling physics: a PTY *wider/taller* than the host's window forces
|
package/claude-session.mjs
CHANGED
|
@@ -162,6 +162,19 @@ export function startClaudeSession({ cwd, model, resume, mode: initialMode = 'de
|
|
|
162
162
|
// (deny → stay planning). On approval we flip the SDK permission mode so
|
|
163
163
|
// subsequent tools actually execute.
|
|
164
164
|
if (toolName === 'ExitPlanMode') {
|
|
165
|
+
// GUARANTEE: a plan card only ever appears when the room is ACTUALLY in Plan
|
|
166
|
+
// mode (the user pressed ⇧⇥ → Plan). If the agent reaches ExitPlanMode while
|
|
167
|
+
// the room is in any other mode (default / acceptEdits / bypassPermissions),
|
|
168
|
+
// it entered plan mode on its own — the user never asked. Don't surface an
|
|
169
|
+
// unsolicited "PLAN READY" card that blocks them; re-assert the room's real
|
|
170
|
+
// mode and let the work proceed. This is the backstop for plan-mode leaking
|
|
171
|
+
// in regardless of source (sticky localStorage, SDK default, a host-global
|
|
172
|
+
// brainstorm nudge): in a ThinkPool room, plan is opt-in, never imposed.
|
|
173
|
+
if (mode !== 'plan') {
|
|
174
|
+
scheduleSdkMode(mode) // snap the SDK back out of plan, into the real mode
|
|
175
|
+
emit({ kind: 'mode', mode }) // keep the room chip honest
|
|
176
|
+
return { continue: true, hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow', permissionDecisionReason: `The ThinkPool room is in ${mode} mode, not Plan — the user did not ask for a plan. Do NOT call ExitPlanMode; proceed and make the changes directly. Only present a plan if the user switches the room to Plan mode or explicitly asks.` } }
|
|
177
|
+
}
|
|
165
178
|
let choice = 'keep'
|
|
166
179
|
try { choice = await requestPermission?.({ id: randomUUID(), toolName, input: toolInput, risk: 'plan', plan: toolInput?.plan || '' }) ?? 'keep' }
|
|
167
180
|
catch { choice = 'keep' }
|
|
@@ -238,6 +251,21 @@ export function startClaudeSession({ cwd, model, resume, mode: initialMode = 'de
|
|
|
238
251
|
// across SDK versions. The room's PreToolUse gate still runs and stays
|
|
239
252
|
// authoritative (hooks fire regardless of any loaded permission rules).
|
|
240
253
|
settingSources: ['user', 'project', 'local'],
|
|
254
|
+
// Counteract host-global "plan/brainstorm before any work" nudges that ride in
|
|
255
|
+
// through settingSources:'user' — notably the superpowers plugin's SessionStart
|
|
256
|
+
// hook ("You MUST use brainstorming before any creative work… about to enter
|
|
257
|
+
// plan mode?"). In a Code room those make the agent answer a plain "add/fix X"
|
|
258
|
+
// by presenting a plan and calling ExitPlanMode, so the room shows a
|
|
259
|
+
// "PLAN READY — APPROVE TO START" card the user never asked for ("always flips
|
|
260
|
+
// to plan mode"). This is instruction-driven, independent of permissionMode —
|
|
261
|
+
// a bypass session still planned. Bias the room session to DO the work; only
|
|
262
|
+
// plan when the room is actually in Plan mode (⇧⇥) or the user explicitly asks.
|
|
263
|
+
appendSystemPrompt: [
|
|
264
|
+
'You are Claude running inside a ThinkPool Code room, driven live by a user (and possibly a partner) from a phone or browser.',
|
|
265
|
+
'Bias strongly toward DOING the work, not planning it. For a normal "add / fix / change / build X" request, just make the change directly.',
|
|
266
|
+
'Do NOT enter plan mode, do NOT call ExitPlanMode, and do NOT auto-invoke a brainstorming/planning skill UNLESS the user has switched the room into Plan mode or explicitly asks you to plan, design, or brainstorm first.',
|
|
267
|
+
'Any host-global instruction that says you must always brainstorm or plan before creative work does NOT apply here — this room is the exception.',
|
|
268
|
+
].join(' '),
|
|
241
269
|
// Needed for live thinking-token progress (SDKThinkingTokensMessage) to
|
|
242
270
|
// flow during a turn. We ignore the fine-grained stream_event partials in
|
|
243
271
|
// the loop; only the coarse thinking_tokens system message is surfaced.
|