thinkpool-pair 0.6.22 → 0.6.24

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.
Files changed (2) hide show
  1. package/bridge.mjs +25 -7
  2. package/package.json +1 -1
package/bridge.mjs CHANGED
@@ -184,9 +184,10 @@ const forceContinue = ownArgs.includes('--continue')
184
184
  const forceFresh = ownArgs.includes('--fresh')
185
185
 
186
186
  // One yes/no on the bridge's stderr (same channel as the agent picker).
187
- const askYesNo = (q) => new Promise((resolve) => {
187
+ // defaultYes=true bare Enter means yes; false → Enter means no (opt-in).
188
+ const askYesNo = (q, defaultYes = true) => new Promise((resolve) => {
188
189
  const rl = readline.createInterface({ input: process.stdin, output: process.stderr })
189
- rl.question(q, (ans) => { rl.close(); resolve(!/^n/i.test(String(ans).trim())) })
190
+ rl.question(q, (ans) => { rl.close(); const a = String(ans).trim(); resolve(defaultYes ? !/^n/i.test(a) : /^y/i.test(a)) })
190
191
  })
191
192
 
192
193
  let attachedCmd = null, attachedArgs = [], continuing = false
@@ -227,10 +228,12 @@ if (process.stdin.isTTY && !headless && process.env.THINKPOOL_PAIR_AUTOUPDATE !=
227
228
  let declined = false
228
229
  try { declined = fs.existsSync(declineFile) } catch { /* noop */ }
229
230
  if (!declined) {
230
- const yes = await askYesNo(`\n Keep this bridge running with auto-updates (survives reboot, no terminal to babysit)?\n Install it as a background service? [Y/n] `)
231
+ // Default NO a bare Enter keeps it foreground (the expected "I ran it in
232
+ // my terminal" behaviour); installing a background daemon is deliberate opt-in.
233
+ const yes = await askYesNo(`\n Run as a background service instead? It survives reboot + auto-updates,\n but runs detached — you'd stop it with 'uninstall-service', not Ctrl-C.\n Install background service? [y/N] `, false)
231
234
  if (yes) {
232
235
  svc.installService(room, attachedCmd ? [attachedCmd, ...attachedArgs] : [])
233
- process.stderr.write(` ◆ the background service is handling room ${room} now — you can close this terminal.\n\n`)
236
+ process.stderr.write(`\n ◆ the background service is handling room ${room} now — you can close this terminal.\n ◆ STOP IT with: npx thinkpool-pair@latest uninstall-service ${room}\n (closing the terminal does NOT stop it — it's detached under launchd/systemd.)\n\n`)
234
237
  process.exit(0) // hand off to the supervised service (no double bridge)
235
238
  }
236
239
  try { fs.mkdirSync(path.dirname(declineFile), { recursive: true }); fs.writeFileSync(declineFile, new Date().toISOString()) } catch { /* noop */ }
@@ -536,6 +539,16 @@ channel
536
539
  const t = terms.get(payload.term)
537
540
  if (t && !t.attached) { try { t.term.resize(Math.max(payload.cols, 80), Math.max(payload.rows, 20)); announce() } catch { /* noop */ } }
538
541
  })
542
+ .on('broadcast', { event: 'session-deleted' }, async () => {
543
+ // The room was deleted from the dashboard. Close the bridge — and if we're a
544
+ // background service, uninstall ourselves first so the supervisor doesn't
545
+ // restart us into a room that no longer exists.
546
+ process.stderr.write('\n ◆ this room was deleted — closing the bridge.\n')
547
+ if (process.env.THINKPOOL_PAIR_AUTOUPDATE === '1') {
548
+ try { const svc = await import('./service.mjs'); svc.uninstallService(room) } catch { /* noop */ }
549
+ }
550
+ shutdown()
551
+ })
539
552
  .on('broadcast', { event: 'term-open' }, ({ payload }) => {
540
553
  if (!payload?.id || !payload?.cmd) return
541
554
  // Multi-bridge rooms: a targeted open is for ONE machine. Untargeted
@@ -781,7 +794,7 @@ if (process.env.THINKPOOL_PAIR_AUTOUPDATE === '1' && VERSION) {
781
794
  }, 60000).unref()
782
795
  }
783
796
 
784
- function shutdown() {
797
+ async function shutdown() {
785
798
  if (shuttingDown) return
786
799
  shuttingDown = true
787
800
  clearInterval(flushTimer)
@@ -789,11 +802,16 @@ function shutdown() {
789
802
  const bye = Buffer.from('\r\n[ shared session ended ]\r\n', 'utf8').toString('base64')
790
803
  for (const id of terms.keys()) bcast('pty-out', { term: id, b64: bye })
791
804
  } catch { /* noop */ }
792
- try { supabase.removeChannel(channel) } catch { /* noop */ }
805
+ // Leave presence EXPLICITLY before exiting so the web room flips to dormant
806
+ // immediately. Previously we removeChannel()'d and process.exit()'d on the next
807
+ // line — the leave frame never flushed, so the web only noticed via the realtime
808
+ // heartbeat timeout (tens of seconds later) and looked stuck "live".
809
+ try { await channel.untrack() } catch { /* noop */ }
810
+ try { await supabase.removeChannel(channel) } catch { /* noop */ }
793
811
  for (const t of terms.values()) { try { t.term.kill() } catch { /* noop */ } }
794
812
  for (const s of sessions.values()) { try { s.session.end() } catch { /* noop */ } }
795
813
  detachLocal()
796
- process.exit(0)
814
+ setTimeout(() => process.exit(0), 250) // small grace for the leave/close frames to flush over the socket
797
815
  }
798
816
  process.on('SIGINT', shutdown)
799
817
  process.on('SIGTERM', shutdown)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.6.22",
3
+ "version": "0.6.24",
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": {