thinkpool-pair 0.7.42 → 0.7.43

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
@@ -151,7 +151,9 @@ if (argv[0] === 'install-service' || argv[0] === 'uninstall-service') {
151
151
  // shell export can't reach a serviced bridge — provider.json is its own source
152
152
  // of truth. Idempotent; a no-op for the default Anthropic provider. See provider.mjs.
153
153
  const { applyProviderEnv } = await import('./provider.mjs')
154
- applyProviderEnv()
154
+ // Captured so the room announce can report the active LLM provider (anthropic,
155
+ // or a custom Anthropic-compatible endpoint) to the web welcome banner.
156
+ const PROVIDER_INFO = applyProviderEnv()
155
157
 
156
158
  if (argv[0] === 'login') { const { runLogin } = await import('./account.mjs'); await runLogin(SUPABASE_URL, SUPABASE_ANON, WEB_BASE) }
157
159
  if (argv[0] === 'bind') { const { runBind } = await import('./account.mjs'); runBind(argv[1], argv[2]) }
@@ -415,6 +417,14 @@ const announce = () =>
415
417
  // cwd + version: the host's working dir + thinkpool-pair version, shown in
416
418
  // the room's welcome banner. Re-sent per announce so late joiners get them.
417
419
  cwd, version: VERSION,
420
+ // provider: the LLM endpoint this bridge drives Claude Code through —
421
+ // 'anthropic' (the default, the host's regular Claude login) or 'custom'
422
+ // (any Anthropic-compatible base url: GLM/Z.ai, OpenRouter, a proxy). Shown
423
+ // in the room banner so both drivers see whose model they're on.
424
+ // providerBaseUrl rides along only for custom so the banner can show the
425
+ // host (e.g. openrouter.ai). Older web clients ignore unknown fields.
426
+ provider: PROVIDER_INFO.provider,
427
+ providerBaseUrl: PROVIDER_INFO.baseUrl || undefined,
418
428
  // pendingUpdate: a newer thinkpool-pair is out — drives the room's "update ready"
419
429
  // chip; re-sent per announce so reloads/late-joiners see it (Slice 3).
420
430
  pendingUpdate,
@@ -702,7 +712,9 @@ function openStructured({ id, model, resume, log, commands, mode }) {
702
712
  // 'stalled' is transient turn-liveness state (would replay as a stale frozen
703
713
  // banner if persisted); 'compaction' is a real transcript milestone (the recap
704
714
  // card) and is intentionally NOT chrome, so it persists + replays.
705
- const chrome = evt.kind === 'mode' || evt.kind === 'usage' || evt.kind === 'clear' || evt.kind === 'compact' || evt.kind === 'stalled'
715
+ // 'suggestion' is Claude Code's predicted-next-prompt ghost text — ephemeral
716
+ // composer UI, broadcast to both clients but never logged/persisted/replayed.
717
+ const chrome = evt.kind === 'mode' || evt.kind === 'usage' || evt.kind === 'clear' || evt.kind === 'compact' || evt.kind === 'stalled' || evt.kind === 'suggestion'
706
718
  // compact turn finished (or errored) → clear the pulsing indicator.
707
719
  // No "done" ctl line — the SDK's own output in the transcript is the
708
720
  // signal (compact summary on success, AbortError text on abort/too-small).
@@ -125,6 +125,14 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
125
125
  let input = makeInputStream() // `let`: auto-restart swaps in a fresh stream
126
126
  let sessionId = resume || null
127
127
  let closed = false
128
+ // Cold-start measurement (2026-06-21, spec 2026-06-21-code-session-deploy-stability):
129
+ // stamp when the open begins so we can log spawn→ready (the SDK's first `init`
130
+ // system message — when the terminal becomes usable). New room terminals "take a
131
+ // long time"; settingSources:['user','project','local'] boots every host MCP
132
+ // server + SessionStart hook + plugin before init. This is the A-side baseline;
133
+ // open a terminal with TP_MCP_STRICT=1 (below) for the B-side to isolate MCP's share.
134
+ const spawnT0 = Date.now()
135
+ let readyLogged = false
128
136
  let q = null // the live Query — control requests (interrupt /
129
137
  // setPermissionMode) route through it once streaming.
130
138
  // The session is CREATED in the caller's chosen mode (not hard-coded default):
@@ -330,7 +338,22 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
330
338
  // flow during a turn. We ignore the fine-grained stream_event partials in
331
339
  // the loop; only the coarse thinking_tokens system message is surfaced.
332
340
  includePartialMessages: true,
341
+ // Claude Code's own "predicted next prompt" — the SDK emits at most one
342
+ // `prompt_suggestion` per turn, AFTER the `result` message, suppressed on the
343
+ // first turn / after API errors / in plan mode, and it piggybacks the parent's
344
+ // prompt cache (nearly free). We surface it verbatim to the room composer as a
345
+ // ghost-text chip; nothing is generated on our side. The loop keeps iterating
346
+ // past `result`, so the post-result suggestion is received. Global off switch:
347
+ // CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false.
348
+ promptSuggestions: true,
333
349
  }
350
+ // Measurement lever (default OFF — no behavior change). TP_MCP_STRICT=1 makes the
351
+ // room session skip booting the host's filesystem-configured MCP servers
352
+ // (strictMcpConfig + empty mcpServers map), isolating the MCP share of cold-start
353
+ // in the spawn→ready log below. settingSources still loads slash commands /
354
+ // CLAUDE.md / agents either way — strict only governs MCP server sourcing. Flip
355
+ // this to the default (or curate a slim allowlist) once the numbers justify it.
356
+ if (process.env.TP_MCP_STRICT === '1') { opts.strictMcpConfig = true; opts.mcpServers = {} }
334
357
  if (cwd) opts.cwd = cwd
335
358
  if (model) opts.model = model
336
359
  if (resume) opts.resume = resume
@@ -372,6 +395,14 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
372
395
  break
373
396
  }
374
397
  if (m.session_id) sessionId = m.session_id
398
+ // Cold-start timing — the init system message is when the terminal is
399
+ // usable, so spawn→here IS the latency the user waits on. One line per
400
+ // open. Compare a normal open vs one with TP_MCP_STRICT=1 to read off how
401
+ // many ms the host MCP servers cost. (resume opens also pay sanitizeSession.)
402
+ if (!readyLogged) {
403
+ readyLogged = true
404
+ process.stderr.write(`\n ◆ session ready in ${Date.now() - spawnT0}ms — MCP ${process.env.TP_MCP_STRICT === '1' ? 'OFF' : 'on'}${resume ? ', resume' : ', fresh'}.\n`)
405
+ }
375
406
  // m.slash_commands (init message) — the commands this session really
376
407
  // supports: built-ins + the host's custom .claude/commands. Surfaced
377
408
  // so the room composer's autocomplete lists what ACTUALLY exists.
@@ -432,6 +463,12 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
432
463
  emit({ kind: 'usage', costUsd: m.total_cost_usd ?? null, tokens: (u.input_tokens || 0) + (u.output_tokens || 0), ctx })
433
464
  })()
434
465
  break
466
+ case 'prompt_suggestion':
467
+ // Claude Code's own predicted next prompt (promptSuggestions opt-in).
468
+ // Arrives once per turn AFTER `result`; surfaced verbatim as ephemeral
469
+ // chrome (a ghost-text chip in the room composer), never persisted.
470
+ if (m.suggestion) emit({ kind: 'suggestion', text: m.suggestion })
471
+ break
435
472
  default:
436
473
  break
437
474
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.42",
3
+ "version": "0.7.43",
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": {