thinkpool-pair 0.3.1 → 0.3.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.
Files changed (2) hide show
  1. package/bridge.mjs +35 -7
  2. package/package.json +1 -1
package/bridge.mjs CHANGED
@@ -135,7 +135,9 @@ const announce = () =>
135
135
  channel.send({ type: 'broadcast', event: 'bridge', payload: {
136
136
  v: 2, name, repo: repoLabel, branch,
137
137
  agents: installedAgents,
138
- terms: [...terms.entries()].map(([id, t]) => ({ id, cmd: t.cmd, alive: true })),
138
+ // cols/rows: the PTY's one true size web viewers render this grid and
139
+ // scale it to their own page instead of voting to reflow it.
140
+ terms: [...terms.entries()].map(([id, t]) => ({ id, cmd: t.cmd, alive: true, cols: t.term.cols, rows: t.term.rows })),
139
141
  } })
140
142
 
141
143
  // One flush timer batches every terminal's pending bytes (~35ms cadence).
@@ -149,12 +151,32 @@ const flushAll = () => {
149
151
  }
150
152
  const flushTimer = setInterval(flushAll, 35)
151
153
 
154
+ // ── shared-PTY geometry ───────────────────────────────────────────
155
+ // The room's view is the product. DEFAULT: the shared PTY is PINNED to a
156
+ // standard 120×32 — one source resolution chosen for web legibility
157
+ // (scale ≈1 on a laptop pane), regardless of the host's window. A huge
158
+ // host window made viewers' text microscopic; a tiny one broke their
159
+ // layout — both ends of the same bug (2026-06-10). The host's local
160
+ // terminal just doesn't use its extra space; below 120×32 it wraps
161
+ // oddly locally, the room stays clean.
162
+ // TP_COLS/TP_ROWS pin a different size. TP_FOLLOW=1 restores
163
+ // follow-the-host-window (floored), for solo screen-mirror use.
164
+ const FLOOR_COLS = 100, FLOOR_ROWS = 28
165
+ const FOLLOW = process.env.TP_FOLLOW === '1'
166
+ const PIN_COLS = parseInt(process.env.TP_COLS, 10) || (FOLLOW ? null : 120)
167
+ const PIN_ROWS = parseInt(process.env.TP_ROWS, 10) || (FOLLOW ? null : 32)
168
+ const attachedDims = () => ({
169
+ cols: PIN_COLS || Math.max(process.stdout.columns || FLOOR_COLS, FLOOR_COLS),
170
+ rows: PIN_ROWS || Math.max(process.stdout.rows || FLOOR_ROWS, FLOOR_ROWS),
171
+ })
172
+
152
173
  function openTerm({ id, cmd, args = [], attached = false, cols, rows }) {
153
174
  if (terms.has(id)) return
175
+ const ad = attached ? attachedDims() : null
154
176
  const term = pty.spawn(cmd, args, {
155
177
  name: 'xterm-256color',
156
- cols: cols || (attached ? (process.stdout.columns || 100) : 100),
157
- rows: rows || (attached ? (process.stdout.rows || 30) : 30),
178
+ cols: cols || (attached ? ad.cols : (PIN_COLS || 120)),
179
+ rows: rows || (attached ? ad.rows : (PIN_ROWS || 32)),
158
180
  cwd: process.cwd(), env: process.env,
159
181
  })
160
182
  const entry = { term, cmd, attached, scrollback: '', buf: '' }
@@ -192,10 +214,14 @@ process.stdin.on('data', d => {
192
214
  if (t) t.term.write(d.toString('utf8'))
193
215
  else if (d.includes(3)) shutdown() // Ctrl-C once detached/headless
194
216
  })
195
- // attached terminal follows the host's TTY size (web resize never touches it)
217
+ // attached terminal follows the host's TTY size but never below the
218
+ // floor, and never at all when pinned. Re-announce so viewers rescale.
196
219
  process.stdout.on('resize', () => {
197
220
  const t = attachedId && terms.get(attachedId)
198
- if (t) { try { t.term.resize(process.stdout.columns, process.stdout.rows) } catch { /* noop */ } }
221
+ if (!t) return
222
+ const { cols, rows } = attachedDims()
223
+ if (t.term.cols === cols && t.term.rows === rows) return
224
+ try { t.term.resize(cols, rows); announce() } catch { /* noop */ }
199
225
  })
200
226
 
201
227
  channel
@@ -205,10 +231,12 @@ channel
205
231
  if (t) t.term.write(payload.data)
206
232
  })
207
233
  .on('broadcast', { event: 'resize' }, ({ payload }) => {
208
- // headless terms are web-sized (last writer wins); the attached term is host-sized.
234
+ // headless terms are web-sized (last writer wins); the attached term is
235
+ // host-sized. Floor at 80 cols — one narrow phone viewer must not reflow
236
+ // the shared PTY into a 40-col column for everyone (2026-06-10 bug).
209
237
  if (!payload?.cols || !payload?.rows) return
210
238
  const t = terms.get(payload.term)
211
- if (t && !t.attached) { try { t.term.resize(payload.cols, payload.rows) } catch { /* noop */ } }
239
+ if (t && !t.attached) { try { t.term.resize(Math.max(payload.cols, 80), Math.max(payload.rows, 20)); announce() } catch { /* noop */ } }
212
240
  })
213
241
  .on('broadcast', { event: 'term-open' }, ({ payload }) => {
214
242
  if (!payload?.id || !payload?.cmd) return
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.3.1",
3
+ "version": "0.3.5",
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": { "thinkpool-pair": "bridge.mjs" },