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 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
@@ -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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.9",
3
+ "version": "0.7.11",
4
4
  "description": "Share a local coding-agent CLI (Claude Code, Codex, Gemini, Aider, …) into a ThinkPool Code room, live.",
5
5
  "type": "module",
6
6
  "bin": {