thinkpool-pair 0.7.352 → 0.7.353

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.
@@ -30,9 +30,11 @@
30
30
  // path routes command/file approval requests through the shared room cards.
31
31
  import fs from 'node:fs'
32
32
  import { CODEX_COMMAND_CATALOG } from './codex-commands.mjs'
33
+ import { normalizeEditKind, summarizeTextDiff } from './edit-diff.mjs'
33
34
 
34
35
  const safeMcpPart = (value) => String(value || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '_')
35
36
  const mcpToolName = (item) => `mcp__${safeMcpPart(item?.server)}__${safeMcpPart(item?.tool)}`
37
+ const webSearchQuery = (item) => item?.query || item?.action?.query || item?.action?.url || ''
36
38
  const resultText = (value) => {
37
39
  if (typeof value === 'string') return value
38
40
  if (value == null) return ''
@@ -99,15 +101,16 @@ export class CodexEventMapper {
99
101
  this._emit({ kind: 'assistant', blocks: [{ type: 'tool_use', id: it.id, name: 'Bash', input: { command: it.command || '' } }], parentToolUseId: null })
100
102
  } else if (it.type === 'file_change') {
101
103
  const before = (it.changes || []).map((c) => {
104
+ const kind = normalizeEditKind(c.kind)
102
105
  let content = ''
103
- if (c.kind !== 'add' && c.path) {
106
+ if (kind !== 'add' && c.path) {
104
107
  try { content = this._fs.readFileSync(c.path, 'utf8') } catch { /* missing/unreadable → empty */ }
105
108
  }
106
- return { path: c.path || '', kind: c.kind || 'update', content }
109
+ return { path: c.path || '', kind, content }
107
110
  })
108
111
  this._fileBefore.set(it.id, before)
109
112
  } else if (it.type === 'web_search') {
110
- this._emit({ kind: 'assistant', blocks: [{ type: 'tool_use', id: it.id, name: 'WebSearch', input: { query: it.query || '' } }], parentToolUseId: null })
113
+ this._emit({ kind: 'assistant', blocks: [{ type: 'tool_use', id: it.id, name: 'WebSearch', input: { query: webSearchQuery(it) } }], parentToolUseId: null })
111
114
  } else if (it.type === 'mcp_tool_call') {
112
115
  this._emit({ kind: 'assistant', blocks: [{ type: 'tool_use', id: it.id, name: mcpToolName(it), input: it.arguments || {} }], parentToolUseId: null })
113
116
  }
@@ -137,7 +140,17 @@ export class CodexEventMapper {
137
140
  if (c.kind !== 'delete' && c.path) {
138
141
  try { next = this._fs.readFileSync(c.path, 'utf8') } catch { /* deleted/unreadable → empty */ }
139
142
  }
140
- return { file_path: c.path, path: c.path, old_string: c.content, new_string: next, kind: c.kind }
143
+ const summary = summarizeTextDiff(c.content, next, { context: 2, maxLines: 60 })
144
+ return {
145
+ file_path: c.path,
146
+ path: c.path,
147
+ kind: c.kind,
148
+ added: summary.added,
149
+ removed: summary.removed,
150
+ diff_preview: summary.preview,
151
+ old_string: c.content,
152
+ new_string: next,
153
+ }
141
154
  })
142
155
  const first = edits[0] || { file_path: '', path: '', old_string: '', new_string: '', kind: 'update' }
143
156
  const name = edits.length > 1 ? 'MultiEdit' : first.kind === 'add' ? 'Write' : 'Edit'
@@ -149,7 +162,8 @@ export class CodexEventMapper {
149
162
  this._emit({ kind: 'assistant', blocks: [{ type: 'tool_use', id: it.id, name, input }], parentToolUseId: null })
150
163
  this._emit({ kind: 'tool_result', toolUseId: it.id, content: [{ type: 'text', text: `${name === 'Write' ? 'wrote' : 'edited'} ${first.path}` }], isError: false, durationMs, parentToolUseId: null })
151
164
  } else if (it.type === 'web_search') {
152
- this._emit({ kind: 'tool_result', toolUseId: it.id, content: [{ type: 'text', text: it.query || 'search completed' }], isError: false, durationMs, parentToolUseId: null })
165
+ const query = webSearchQuery(it)
166
+ this._emit({ kind: 'tool_result', toolUseId: it.id, toolInput: { query }, content: [{ type: 'text', text: query || 'search completed' }], isError: false, durationMs, parentToolUseId: null })
153
167
  } else if (it.type === 'mcp_tool_call') {
154
168
  this._emit({ kind: 'tool_result', toolUseId: it.id, content: [{ type: 'text', text: it.error?.message || resultText(it.result) }], isError: it.status === 'failed' || !!it.error, durationMs, parentToolUseId: null })
155
169
  }
package/edit-diff.mjs ADDED
@@ -0,0 +1,136 @@
1
+ // Bridge-local diff primitives. Keep this module self-contained: the npm
2
+ // package cannot import the web app's ../src tree.
3
+ export const normalizeEditKind = (value) => {
4
+ const kind = typeof value === 'object' ? value?.type : value
5
+ if (kind === 'add' || kind === 'delete' || kind === 'rename') return kind
6
+ return 'update'
7
+ }
8
+
9
+ const splitLines = (value) => {
10
+ if (!value) return []
11
+ const lines = String(value).replace(/\r\n?/g, '\n').split('\n')
12
+ if (lines.at(-1) === '') lines.pop()
13
+ return lines
14
+ }
15
+
16
+ const fallbackDiff = (before, after) => {
17
+ let start = 0
18
+ while (start < before.length && start < after.length && before[start] === after[start]) start++
19
+ let oldEnd = before.length
20
+ let newEnd = after.length
21
+ while (oldEnd > start && newEnd > start && before[oldEnd - 1] === after[newEnd - 1]) {
22
+ oldEnd--
23
+ newEnd--
24
+ }
25
+ return [
26
+ ...before.slice(0, start).map((text) => ({ type: 'context', text })),
27
+ ...before.slice(start, oldEnd).map((text) => ({ type: 'remove', text })),
28
+ ...after.slice(start, newEnd).map((text) => ({ type: 'add', text })),
29
+ ...before.slice(oldEnd).map((text) => ({ type: 'context', text })),
30
+ ]
31
+ }
32
+
33
+ function lineDiff(before, after, maxDistance = 600) {
34
+ if (!before.length) return after.map((text) => ({ type: 'add', text }))
35
+ if (!after.length) return before.map((text) => ({ type: 'remove', text }))
36
+ const max = Math.min(before.length + after.length, maxDistance)
37
+ const frontier = new Map([[1, 0]])
38
+ const trace = []
39
+ for (let distance = 0; distance <= max; distance++) {
40
+ trace.push(new Map(frontier))
41
+ for (let diagonal = -distance; diagonal <= distance; diagonal += 2) {
42
+ const down = frontier.get(diagonal + 1) ?? -1
43
+ const right = frontier.get(diagonal - 1) ?? -1
44
+ let oldIndex = diagonal === -distance || (diagonal !== distance && right < down)
45
+ ? Math.max(0, down)
46
+ : Math.max(0, right + 1)
47
+ let newIndex = oldIndex - diagonal
48
+ while (oldIndex < before.length && newIndex < after.length && before[oldIndex] === after[newIndex]) {
49
+ oldIndex++
50
+ newIndex++
51
+ }
52
+ frontier.set(diagonal, oldIndex)
53
+ if (oldIndex < before.length || newIndex < after.length) continue
54
+
55
+ const operations = []
56
+ let x = before.length
57
+ let y = after.length
58
+ for (let d = distance; d >= 0; d--) {
59
+ const snapshot = trace[d]
60
+ const k = x - y
61
+ const snapshotDown = snapshot.get(k + 1) ?? -1
62
+ const snapshotRight = snapshot.get(k - 1) ?? -1
63
+ const previousK = k === -d || (k !== d && snapshotRight < snapshotDown) ? k + 1 : k - 1
64
+ const previousX = Math.max(0, snapshot.get(previousK) ?? 0)
65
+ const previousY = previousX - previousK
66
+ while (x > previousX && y > previousY) {
67
+ operations.push({ type: 'context', text: before[x - 1] })
68
+ x--
69
+ y--
70
+ }
71
+ if (d === 0) break
72
+ if (x === previousX) {
73
+ operations.push({ type: 'add', text: after[y - 1] })
74
+ y--
75
+ } else {
76
+ operations.push({ type: 'remove', text: before[x - 1] })
77
+ x--
78
+ }
79
+ }
80
+ return operations.reverse()
81
+ }
82
+ }
83
+ return fallbackDiff(before, after)
84
+ }
85
+
86
+ const previewLine = (type, text = '') => {
87
+ if (type === 'add') return `+${text}`
88
+ if (type === 'remove') return `-${text}`
89
+ if (type === 'context') return ` ${text}`
90
+ return `…${text}`
91
+ }
92
+
93
+ function compactOperations(operations, context = 2, maxLines = 80) {
94
+ const changed = []
95
+ operations.forEach((operation, index) => {
96
+ if (operation.type !== 'context') changed.push(index)
97
+ })
98
+ if (!changed.length) return []
99
+
100
+ const ranges = []
101
+ for (const index of changed) {
102
+ const start = Math.max(0, index - context)
103
+ const end = Math.min(operations.length, index + context + 1)
104
+ const previous = ranges.at(-1)
105
+ if (previous && start <= previous.end) previous.end = Math.max(previous.end, end)
106
+ else ranges.push({ start, end })
107
+ }
108
+
109
+ const compacted = []
110
+ ranges.forEach((range, index) => {
111
+ if (index > 0) compacted.push({ type: 'omit', text: `${range.start - ranges[index - 1].end} unchanged lines` })
112
+ compacted.push(...operations.slice(range.start, range.end))
113
+ })
114
+ if (compacted.length <= maxLines) return compacted
115
+
116
+ const head = Math.floor((maxLines - 1) / 2)
117
+ const tail = maxLines - head - 1
118
+ return [
119
+ ...compacted.slice(0, head),
120
+ { type: 'omit', text: `${compacted.length - head - tail} preview lines hidden` },
121
+ ...compacted.slice(-tail),
122
+ ]
123
+ }
124
+
125
+ export function summarizeTextDiff(oldText, newText, options = {}) {
126
+ const operations = lineDiff(splitLines(oldText), splitLines(newText), options.maxDistance)
127
+ const added = operations.reduce((total, operation) => total + (operation.type === 'add' ? 1 : 0), 0)
128
+ const removed = operations.reduce((total, operation) => total + (operation.type === 'remove' ? 1 : 0), 0)
129
+ const lines = compactOperations(operations, options.context ?? 2, options.maxLines ?? 80)
130
+ return {
131
+ added,
132
+ removed,
133
+ lines,
134
+ preview: lines.map((line) => previewLine(line.type, line.text)).join('\n'),
135
+ }
136
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.352",
3
+ "version": "0.7.353",
4
4
  "description": "Connect Claude Code, Codex, or Hermes on your computer to a Thinkpool Code room.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,6 +34,7 @@
34
34
  "codex-mcp-http.mjs",
35
35
  "lane-worktree.mjs",
36
36
  "codex-event-mapper.mjs",
37
+ "edit-diff.mjs",
37
38
  "cumulative-event-relay.mjs",
38
39
  "codex-commands.mjs",
39
40
  "acp-client.mjs",
package/terminal-name.mjs CHANGED
@@ -224,7 +224,7 @@ export function cleanTerminalName(value) {
224
224
  const words = name.split(/\s+/).filter(Boolean)
225
225
  const compactCjk = words.length === 1
226
226
  && /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u.test(name)
227
- if ((words.length < 2 && !compactCjk) || words.length > 4) return null
227
+ if ((words.length < 2 && !compactCjk) || words.length > 3) return null
228
228
  return name || null
229
229
  }
230
230
 
@@ -268,7 +268,7 @@ const taskTitle = (value) => {
268
268
  if (!body) return null
269
269
  const grammar = analyzeGrammar(body)
270
270
  if (grammar.negativePreference) {
271
- const subject = contentWords(negativePreferenceSubject(grammar)).slice(0, 3)
271
+ const subject = contentWords(negativePreferenceSubject(grammar)).slice(0, 2)
272
272
  return subject.length ? cleanTerminalName(['Avoid', ...subject.map(titleWord)].join(' ')) : null
273
273
  }
274
274
  const informationRequest = grammar.informationRequest
@@ -286,11 +286,11 @@ const taskTitle = (value) => {
286
286
  if (!verb && /\bopen(?:s|ed|ing)?\b/i.test(body)) verb = 'Open'
287
287
 
288
288
  if (/\bautomatic(?:ally)?\b[\s\S]{0,24}\bnam(?:e|ing)\b/i.test(signalText) && /\bterminals?\b/i.test(signalText)) {
289
- if (/\bcontext\b[\s\S]{0,24}\broom\b|\broom\b[\s\S]{0,24}\bcontext\b/i.test(signalText)) return 'Auto-Name Terminals From Context'
289
+ if (/\bcontext\b[\s\S]{0,24}\broom\b|\broom\b[\s\S]{0,24}\bcontext\b/i.test(signalText)) return 'Contextual Terminal Names'
290
290
  verb = 'Auto-Name'
291
291
  }
292
- if (verb === 'Snap' && /\bterminal row\b/i.test(signalText) && /\bview\b/i.test(signalText)) return 'Snap Terminal Row View'
293
- if (verb === 'Snap' && /\bclicked terminal\b/i.test(signalText) && /\bview\b/i.test(signalText)) return 'Snap Clicked Terminal View'
292
+ if (verb === 'Snap' && /\bterminal row\b/i.test(signalText) && /\bview\b/i.test(signalText)) return 'Snap Terminal Row'
293
+ if (verb === 'Snap' && /\bclicked terminal\b/i.test(signalText) && /\bview\b/i.test(signalText)) return 'Snap Clicked Terminal'
294
294
 
295
295
  let domain = []
296
296
  let objectText = body
@@ -311,7 +311,7 @@ const taskTitle = (value) => {
311
311
  }
312
312
  const picked = [...domain, ...object]
313
313
  .filter((word, index, all) => all.findIndex((other) => other.toLowerCase() === word.toLowerCase()) === index)
314
- .slice(0, verb ? 3 : 4)
314
+ .slice(0, verb ? 2 : 3)
315
315
  if (!picked.length) return null
316
316
  return cleanTerminalName([verb, ...picked.map(titleWord)].filter(Boolean).join(' '))
317
317
  }