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.
- package/codex-event-mapper.mjs +19 -5
- package/edit-diff.mjs +136 -0
- package/package.json +2 -1
- package/terminal-name.mjs +6 -6
package/codex-event-mapper.mjs
CHANGED
|
@@ -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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 >
|
|
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,
|
|
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 '
|
|
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
|
|
293
|
-
if (verb === 'Snap' && /\bclicked terminal\b/i.test(signalText) && /\bview\b/i.test(signalText)) return 'Snap Clicked Terminal
|
|
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 ?
|
|
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
|
}
|