thinkpool-pair 0.6.14 → 0.6.16

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/claude-session.mjs +36 -7
  2. package/package.json +1 -1
@@ -54,6 +54,21 @@ export function isSafeDocWrite(toolName, input) {
54
54
  return SAFE_DOC_RE.test(p)
55
55
  }
56
56
 
57
+ // The full auto-allow decision (exported for the contract test, so the policy
58
+ // can't drift away from docs/specs/2026-06-15-paired-permission-safe-doc-writes.md).
59
+ // Mirrors the PreToolUse policy: reads always; bypass mode always; acceptEdits for
60
+ // non-high writes; mandated safe-doc writes always; per tool:risk "always allow".
61
+ export function autoAllow({ toolName, input, mode = 'default', alwaysAllow = new Set() }) {
62
+ const risk = classifyRisk(toolName, input)
63
+ return (
64
+ risk === 'low' ||
65
+ mode === 'bypassPermissions' ||
66
+ (mode === 'acceptEdits' && WRITE_TOOLS.has(toolName) && risk !== 'high') ||
67
+ isSafeDocWrite(toolName, input) ||
68
+ alwaysAllow.has(`${toolName}:${risk}`)
69
+ )
70
+ }
71
+
57
72
  // ── input stream — a generator we keep open and feed turns into ──
58
73
  function makeInputStream() {
59
74
  const queue = []
@@ -108,6 +123,7 @@ export function startClaudeSession({ cwd, model, resume, onEvent, requestPermiss
108
123
  let mode = 'default' // mirrors Claude Code's ⇧⇥ cycle
109
124
  const alwaysAllow = new Set() // tool:risk signatures the user chose "don't ask again" for
110
125
  const toolStart = new Map() // tool_use id → start time, for the duration badge
126
+ let effort = null // active reasoning effort for the turn (from the hook input)
111
127
 
112
128
  const emit = (evt) => { try { onEvent?.(evt) } catch { /* never let a consumer throw into the loop */ } }
113
129
 
@@ -118,6 +134,11 @@ export function startClaudeSession({ cwd, model, resume, onEvent, requestPermiss
118
134
  const preTool = async (hookInput) => {
119
135
  const toolName = hookInput.tool_name
120
136
  const toolInput = hookInput.tool_input
137
+ // The hook input carries the turn's active reasoning effort (post any
138
+ // model downgrade) — the real signal for "thinking with <X> effort".
139
+ // Surface it to the room when it changes; absent on models without effort.
140
+ const lvl = hookInput.effort?.level
141
+ if (lvl && lvl !== effort) { effort = lvl; emit({ kind: 'effort', level: effort }) }
121
142
  // ── Plan approval — ExitPlanMode is how the agent presents its plan in
122
143
  // plan mode. Render a dedicated plan card (not the generic perm card) with
123
144
  // three outcomes: run (exit → default), accept (exit → acceptEdits), keep
@@ -162,12 +183,7 @@ export function startClaudeSession({ cwd, model, resume, onEvent, requestPermiss
162
183
  // never silently allows a future destructive one (high always re-asks).
163
184
  const sig = `${toolName}:${risk}`
164
185
  const safeDoc = isSafeDocWrite(toolName, toolInput)
165
- const auto =
166
- risk === 'low' ||
167
- mode === 'bypassPermissions' ||
168
- (mode === 'acceptEdits' && WRITE_TOOLS.has(toolName) && risk !== 'high') ||
169
- safeDoc ||
170
- alwaysAllow.has(sig)
186
+ const auto = autoAllow({ toolName, input: toolInput, mode, alwaysAllow })
171
187
  let decision = 'allow'
172
188
  if (!auto) {
173
189
  try {
@@ -186,7 +202,9 @@ export function startClaudeSession({ cwd, model, resume, onEvent, requestPermiss
186
202
  permissionDecision: denied ? 'deny' : 'allow',
187
203
  permissionDecisionReason: denied
188
204
  ? 'Denied by the user in the ThinkPool room. Do not retry this tool — ask what to do instead.'
189
- : auto ? `Auto-approved (${mode}).` : 'Approved in the ThinkPool room.',
205
+ : auto
206
+ ? (safeDoc ? 'Auto-approved (mandated doc write — .claude/SESSIONS/ or CLAUDE.md).' : `Auto-approved (${mode}).`)
207
+ : 'Approved in the ThinkPool room.',
190
208
  },
191
209
  }
192
210
  }
@@ -195,6 +213,10 @@ export function startClaudeSession({ cwd, model, resume, onEvent, requestPermiss
195
213
  abortController: ac,
196
214
  permissionMode: 'default',
197
215
  hooks: { PreToolUse: [{ hooks: [preTool] }] },
216
+ // Needed for live thinking-token progress (SDKThinkingTokensMessage) to
217
+ // flow during a turn. We ignore the fine-grained stream_event partials in
218
+ // the loop; only the coarse thinking_tokens system message is surfaced.
219
+ includePartialMessages: true,
198
220
  }
199
221
  if (cwd) opts.cwd = cwd
200
222
  if (model) opts.model = model
@@ -207,6 +229,13 @@ export function startClaudeSession({ cwd, model, resume, onEvent, requestPermiss
207
229
  if (closed) break
208
230
  switch (m.type) {
209
231
  case 'system':
232
+ // Live thinking-token progress — the running estimate while the
233
+ // model reasons, surfaced as the indicator's ↓ N tokens. Coarse,
234
+ // emitted during extended thinking; not a per-token stream.
235
+ if (m.subtype === 'thinking_tokens') {
236
+ emit({ kind: 'thinking_tokens', tokens: m.estimated_tokens, delta: m.estimated_tokens_delta })
237
+ break
238
+ }
210
239
  if (m.session_id) sessionId = m.session_id
211
240
  emit({ kind: 'system', sessionId, model: m.model || model || null })
212
241
  break
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.6.14",
3
+ "version": "0.6.16",
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": {