thinkpool-pair 0.6.6 → 0.6.7

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
@@ -330,6 +330,7 @@ function printLocal(evt) {
330
330
  case 'pool': return w(evt.pending ? `${A.cyan}◌ pool synthesizing…${A.rst}` : `${A.cyan}◆ pool → ${evt.text}${A.rst}`)
331
331
  case 'note': return w(`${A.dim}— ${evt.text} —${A.rst}`)
332
332
  case 'clear': return w(`${A.dim}— context cleared —${A.rst}`)
333
+ case 'usage': { const c = evt.ctx; return c ? w(`${A.dim} ctx ${c.pct}% · ${Math.round(c.used / 1000)}k/${Math.round(c.max / 1000)}k${evt.costUsd != null ? ` · $${evt.costUsd.toFixed(3)}` : ''}${A.rst}`) : undefined }
333
334
  case 'error': return w(`${A.red}✗ ${evt.message}${A.rst}`)
334
335
  case 'tool_result': {
335
336
  const c = evt.content
@@ -370,11 +371,16 @@ function openStructured({ id, model, resume, log }) {
370
371
  openStructured({ id, model, log: entry.log })
371
372
  return
372
373
  }
373
- entry.log.push(evt)
374
- if (entry.log.length > STRUCTURED_LOG_MAX) entry.log.shift()
374
+ // Chrome events (mode / usage / clear) are transient state, not transcript —
375
+ // broadcast + print them, but keep them out of the persisted/replayed log.
376
+ const chrome = evt.kind === 'mode' || evt.kind === 'usage' || evt.kind === 'clear'
377
+ if (!chrome) {
378
+ entry.log.push(evt)
379
+ if (entry.log.length > STRUCTURED_LOG_MAX) entry.log.shift()
380
+ }
375
381
  bcast('code-event', { term: id, evt })
376
382
  printLocal(evt)
377
- persist()
383
+ if (!chrome) persist()
378
384
  },
379
385
  requestPermission: (req) => new Promise((resolve) => {
380
386
  entry.pending.set(req.id, resolve)
@@ -91,6 +91,7 @@ export function startClaudeSession({ cwd, model, resume, onEvent, requestPermiss
91
91
  let q = null // the live Query — control requests (interrupt /
92
92
  // setPermissionMode) route through it once streaming.
93
93
  let mode = 'default' // mirrors Claude Code's ⇧⇥ cycle
94
+ const alwaysAllow = new Set() // tool:risk signatures the user chose "don't ask again" for
94
95
 
95
96
  const emit = (evt) => { try { onEvent?.(evt) } catch { /* never let a consumer throw into the loop */ } }
96
97
 
@@ -120,15 +121,21 @@ export function startClaudeSession({ cwd, model, resume, onEvent, requestPermiss
120
121
  return { continue: true, hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: 'The user chose "keep planning" in the ThinkPool room. Do not exit plan mode — keep refining the plan, then call ExitPlanMode again when ready.' } }
121
122
  }
122
123
  const risk = classifyRisk(toolName, toolInput)
124
+ // "Don't ask again" is keyed by tool + risk tier, so allowing medium Bash
125
+ // never silently allows a future destructive one (high always re-asks).
126
+ const sig = `${toolName}:${risk}`
123
127
  const auto =
124
128
  risk === 'low' ||
125
129
  mode === 'bypassPermissions' ||
126
- (mode === 'acceptEdits' && WRITE_TOOLS.has(toolName) && risk !== 'high')
130
+ (mode === 'acceptEdits' && WRITE_TOOLS.has(toolName) && risk !== 'high') ||
131
+ alwaysAllow.has(sig)
127
132
  let decision = 'allow'
128
133
  if (!auto) {
129
134
  try {
130
135
  decision = await requestPermission?.({ id: randomUUID(), toolName, input: toolInput, risk }) ?? 'allow'
131
136
  } catch { decision = 'deny' } // a broken permission path must fail safe (deny)
137
+ // "Allow & don't ask again" — remember the signature, then allow.
138
+ if (decision === 'always') { alwaysAllow.add(sig); decision = 'allow' }
132
139
  }
133
140
  // On deny, permissionDecisionReason IS what the model receives as the
134
141
  // tool error — make it a real instruction, not an opaque tag.
@@ -178,6 +185,17 @@ export function startClaudeSession({ cwd, model, resume, onEvent, requestPermiss
178
185
  case 'result':
179
186
  if (m.session_id) sessionId = m.session_id
180
187
  emit({ kind: 'result', subtype: m.subtype, sessionId, costUsd: m.total_cost_usd, usage: m.usage, numTurns: m.num_turns })
188
+ // Surface a usage/context meter (chrome, not a transcript line). The
189
+ // context window % comes from the control request; cost is cumulative.
190
+ ;(async () => {
191
+ let ctx = null
192
+ try {
193
+ const c = await q?.getContextUsage?.()
194
+ if (c) ctx = { used: c.totalTokens, max: c.maxTokens, pct: Math.round(c.percentage), model: c.model }
195
+ } catch { /* control req may be unavailable */ }
196
+ const u = m.usage || {}
197
+ emit({ kind: 'usage', costUsd: m.total_cost_usd ?? null, tokens: (u.input_tokens || 0) + (u.output_tokens || 0), ctx })
198
+ })()
181
199
  break
182
200
  default:
183
201
  break
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
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": {