lonny-agent 0.2.4 → 0.2.6

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 (102) hide show
  1. package/README.md +326 -326
  2. package/dist/agent/llm.d.ts +4 -0
  3. package/dist/agent/llm.d.ts.map +1 -1
  4. package/dist/agent/prompt-builder.d.ts.map +1 -1
  5. package/dist/agent/prompt-builder.js +13 -9
  6. package/dist/agent/prompt-builder.js.map +1 -1
  7. package/dist/agent/providers/google.d.ts.map +1 -1
  8. package/dist/agent/providers/google.js.map +1 -1
  9. package/dist/agent/providers/openai.d.ts.map +1 -1
  10. package/dist/agent/providers/openai.js +43 -3
  11. package/dist/agent/providers/openai.js.map +1 -1
  12. package/dist/agent/session.d.ts +4 -0
  13. package/dist/agent/session.d.ts.map +1 -1
  14. package/dist/agent/session.js +45 -7
  15. package/dist/agent/session.js.map +1 -1
  16. package/dist/config/index.d.ts.map +1 -1
  17. package/dist/config/index.js +7 -1
  18. package/dist/config/index.js.map +1 -1
  19. package/dist/tools/__tests__/bash.test.js +1 -1
  20. package/dist/tools/__tests__/bash.test.js.map +1 -1
  21. package/dist/tools/__tests__/edit.test.js +875 -371
  22. package/dist/tools/__tests__/edit.test.js.map +1 -1
  23. package/dist/tools/__tests__/grep.test.js +32 -0
  24. package/dist/tools/__tests__/grep.test.js.map +1 -1
  25. package/dist/tools/__tests__/sed.test.d.ts +2 -0
  26. package/dist/tools/__tests__/sed.test.d.ts.map +1 -0
  27. package/dist/tools/__tests__/sed.test.js +228 -0
  28. package/dist/tools/__tests__/sed.test.js.map +1 -0
  29. package/dist/tools/bash.d.ts.map +1 -1
  30. package/dist/tools/bash.js +83 -10
  31. package/dist/tools/bash.js.map +1 -1
  32. package/dist/tools/edit.d.ts +50 -9
  33. package/dist/tools/edit.d.ts.map +1 -1
  34. package/dist/tools/edit.js +328 -158
  35. package/dist/tools/edit.js.map +1 -1
  36. package/dist/tools/grep.d.ts.map +1 -1
  37. package/dist/tools/grep.js +54 -30
  38. package/dist/tools/grep.js.map +1 -1
  39. package/dist/tools/registry.d.ts +0 -6
  40. package/dist/tools/registry.d.ts.map +1 -1
  41. package/dist/tools/registry.js +2 -29
  42. package/dist/tools/registry.js.map +1 -1
  43. package/dist/tools/sed.d.ts +4 -0
  44. package/dist/tools/sed.d.ts.map +1 -0
  45. package/dist/tools/sed.js +121 -0
  46. package/dist/tools/sed.js.map +1 -0
  47. package/dist/web/index.d.ts.map +1 -1
  48. package/dist/web/index.js +26 -3
  49. package/dist/web/index.js.map +1 -1
  50. package/dist/web/public/app.js +5 -855
  51. package/dist/web/public/confirm.js +70 -0
  52. package/dist/web/public/index.html +121 -120
  53. package/dist/web/public/input.js +107 -0
  54. package/dist/web/public/messages.js +395 -0
  55. package/dist/web/public/sidebar.js +82 -0
  56. package/dist/web/public/state.js +72 -0
  57. package/dist/web/public/style.css +996 -949
  58. package/dist/web/public/utils.js +37 -0
  59. package/dist/web/public/websocket.js +267 -0
  60. package/dist/web/public/ws.js +19 -0
  61. package/dist/web/session-bridge.d.ts.map +1 -1
  62. package/dist/web/session-bridge.js +3 -7
  63. package/dist/web/session-bridge.js.map +1 -1
  64. package/package.json +55 -54
  65. package/scripts/copy-native.mjs +24 -0
  66. package/scripts/copy-web.mjs +15 -0
  67. package/src/agent/index.ts +25 -25
  68. package/src/agent/llm.ts +4 -0
  69. package/src/agent/prompt-builder.ts +13 -9
  70. package/src/agent/providers/anthropic.ts +179 -179
  71. package/src/agent/providers/google.ts +210 -211
  72. package/src/agent/providers/ollama.ts +186 -186
  73. package/src/agent/providers/openai.ts +61 -5
  74. package/src/agent/session.ts +59 -3
  75. package/src/config/index.ts +342 -335
  76. package/src/tools/__tests__/bash.test.ts +1 -1
  77. package/src/tools/__tests__/edit.test.ts +981 -385
  78. package/src/tools/__tests__/grep.test.ts +35 -0
  79. package/src/tools/bash.ts +97 -11
  80. package/src/tools/edit.ts +362 -169
  81. package/src/tools/git.ts +76 -76
  82. package/src/tools/grep.ts +57 -30
  83. package/src/tools/registry.ts +2 -31
  84. package/src/tui/index.ts +786 -786
  85. package/src/web/index.ts +25 -3
  86. package/src/web/public/app.js +5 -855
  87. package/src/web/public/confirm.js +70 -0
  88. package/src/web/public/index.html +121 -120
  89. package/src/web/public/input.js +107 -0
  90. package/src/web/public/messages.js +395 -0
  91. package/src/web/public/sidebar.js +82 -0
  92. package/src/web/public/state.js +72 -0
  93. package/src/web/public/style.css +996 -949
  94. package/src/web/public/utils.js +37 -0
  95. package/src/web/public/websocket.js +267 -0
  96. package/src/web/public/ws.js +19 -0
  97. package/src/web/session-bridge.ts +193 -194
  98. package/.claude/settings.local.json +0 -14
  99. package/.kilo/plans/1780064105789-mighty-pixel.md +0 -171
  100. package/.kilo/plans/1780293725888-quick-wizard.md +0 -62
  101. package/.lonny/plan-web-cwd-status.md +0 -38
  102. package/src/tools/exec.ts +0 -348
@@ -1,194 +1,193 @@
1
- import { EventChannels, getGlobalEventBus } from '../agent/event-bus.js'
2
- import type { Session } from '../agent/session.js'
3
- import type { Config } from '../config/index.js'
4
-
5
- /**
6
- * Bridge between Session/EventBus and WebSocket.
7
- * Listens to EventBus events and forwards them as JSON messages.
8
- */
9
-
10
- /** Strip ANSI escape codes from a string (terminal colors are meaningless in the browser) */
11
- function stripAnsi(text: string): string {
12
- return text.replace(/\x1b\[[0-9;]*m/g, '')
13
- }
14
-
15
- export interface WsMessage {
16
- type: string
17
- [key: string]: unknown
18
- }
19
-
20
- export type SendFn = (msg: WsMessage) => void
21
-
22
- const WS_PROTOCOL_VERSION = 1
23
-
24
- export function startSessionBridge(
25
- session: Session,
26
- config: Config,
27
- send: SendFn,
28
- ): { close: () => void; sendMessage: (text: string) => Promise<void> } {
29
- const bus = getGlobalEventBus()
30
-
31
- // Send initial handshake with current token stats
32
- send({
33
- type: 'hello',
34
- version: WS_PROTOCOL_VERSION,
35
- mode: config.mode,
36
- model: config.model,
37
- provider: config.provider,
38
- totalIn: session.totalInputTokens,
39
- totalOut: session.totalOutputTokens,
40
- totalApi: session.totalApiCalls,
41
- })
42
-
43
- // ── Subscribe to EventBus ──
44
-
45
- const unsubTurnStart = bus.on(EventChannels.TURN_START, data => {
46
- const d = data as { prompt?: string }
47
- send({ type: 'turn_start', prompt: d.prompt || '' })
48
- })
49
-
50
- const unsubToolCall = bus.on(EventChannels.TOOL_CALL, data => {
51
- const d = data as { name: string; input: Record<string, unknown>; id: string }
52
- send({ type: 'tool_call', name: d.name, input: d.input, id: d.id })
53
- })
54
-
55
- const unsubToolResult = bus.on(EventChannels.TOOL_RESULT, data => {
56
- const d = data as { name: string; id: string; output: string }
57
- send({
58
- type: 'tool_result',
59
- name: d.name,
60
- id: d.id,
61
- success: true,
62
- output: stripAnsi(d.output),
63
- })
64
- })
65
-
66
- const unsubToolError = bus.on(EventChannels.TOOL_ERROR, data => {
67
- const d = data as { name: string; id: string; error: string }
68
- send({ type: 'tool_result', name: d.name, id: d.id, success: false, error: stripAnsi(d.error) })
69
- })
70
-
71
- const unsubTurnEnd = bus.on(EventChannels.TURN_END, data => {
72
- const d = data as { iterations: number; toolCallCount: number }
73
- send({ type: 'turn_end', iterations: d.iterations, toolCallCount: d.toolCallCount })
74
- })
75
-
76
- const unsubThinking = bus.on(EventChannels.THINKING, data => {
77
- const d = data as { text: string }
78
- send({ type: 'thinking', text: d.text })
79
- })
80
-
81
- const unsubThinkingEnd = bus.on(EventChannels.THINKING_END, () => {
82
- send({ type: 'thinking_end' })
83
- })
84
-
85
- const unsubCompaction = bus.on(EventChannels.COMPACTION_TRIGGERED, data => {
86
- const d = data as { before: number; after: number }
87
- send({ type: 'compaction', before: d.before, after: d.after })
88
- })
89
-
90
- const unsubTokenStats = bus.on(EventChannels.TOKEN_STATS, data => {
91
- const d = data as {
92
- turnIn: number
93
- turnOut: number
94
- totalIn: number
95
- totalOut: number
96
- turnApi: number
97
- totalApi: number
98
- }
99
- send({ type: 'token_stats', ...d })
100
- })
101
-
102
- // ── Handle incoming messages from client ──
103
-
104
- async function handleClientMessage(msg: WsMessage): Promise<void> {
105
- switch (msg.type) {
106
- case 'message': {
107
- const text = String(msg.text || '')
108
- if (!text.trim()) return
109
-
110
- // Handle slash commands
111
- if (text.startsWith('/')) {
112
- const parts = text.slice(1).split(/\s+/)
113
- const cmd = parts[0]
114
- const arg = parts.slice(1).join(' ')
115
-
116
- if (cmd === 'mode' && (arg === 'code' || arg === 'plan' || arg === 'ask')) {
117
- session.setMode(arg as 'code' | 'plan' | 'ask')
118
- send({ type: 'mode_changed', mode: arg })
119
- return
120
- }
121
-
122
- if (cmd === 'model' && arg) {
123
- session.config.model = arg
124
- session.setMode(session.config.mode)
125
- send({ type: 'model_changed', model: arg })
126
- return
127
- }
128
-
129
- if (cmd === 'new') {
130
- const { Session } = await import('../agent/session.js')
131
- Session.clearSavedSession(config.cwd)
132
- send({ type: 'session_cleared' })
133
- return
134
- }
135
-
136
- if (cmd === 'help') {
137
- send({
138
- type: 'help',
139
- commands: [
140
- '/mode code|plan|ask - Switch mode',
141
- '/model <name> - Switch model',
142
- '/new - Start a new session',
143
- '/help - Show this help',
144
- ],
145
- })
146
- return
147
- }
148
-
149
- send({ type: 'error', message: `Unknown command: ${cmd}` })
150
- return
151
- }
152
-
153
- // Send user message to session
154
- try {
155
- await session.chat(text)
156
- send({ type: 'done', reason: 'stop' })
157
- } catch (err) {
158
- const errMsg = err instanceof Error ? err.message : String(err)
159
- send({ type: 'error', message: errMsg })
160
- send({ type: 'done', reason: 'error' })
161
- }
162
- return
163
- }
164
-
165
- case 'ping':
166
- send({ type: 'pong' })
167
- return
168
-
169
- case 'stop':
170
- session.stop()
171
- return
172
-
173
- default:
174
- send({ type: 'error', message: `Unknown message type: ${msg.type}` })
175
- }
176
- }
177
-
178
- return {
179
- close: () => {
180
- unsubTurnStart()
181
- unsubToolCall()
182
- unsubToolResult()
183
- unsubToolError()
184
- unsubTurnEnd()
185
- unsubThinking()
186
- unsubThinkingEnd()
187
- unsubCompaction()
188
- unsubTokenStats()
189
- },
190
- sendMessage: async (text: string) => {
191
- await handleClientMessage({ type: 'message', text })
192
- },
193
- }
194
- }
1
+ import { EventChannels, getGlobalEventBus } from '../agent/event-bus.js'
2
+ import type { Session } from '../agent/session.js'
3
+ import type { Config } from '../config/index.js'
4
+
5
+ /**
6
+ * Bridge between Session/EventBus and WebSocket.
7
+ * Listens to EventBus events and forwards them as JSON messages.
8
+ */
9
+
10
+ /** Strip ANSI escape codes from a string (terminal colors are meaningless in the browser) */
11
+ function stripAnsi(text: string): string {
12
+ return text.replace(/\x1b\[[0-9;]*m/g, '')
13
+ }
14
+
15
+ export interface WsMessage {
16
+ type: string
17
+ [key: string]: unknown
18
+ }
19
+
20
+ export type SendFn = (msg: WsMessage) => void
21
+
22
+ const WS_PROTOCOL_VERSION = 1
23
+
24
+ export function startSessionBridge(
25
+ session: Session,
26
+ config: Config,
27
+ send: SendFn,
28
+ ): { close: () => void; sendMessage: (text: string) => Promise<void> } {
29
+ const bus = getGlobalEventBus()
30
+
31
+ // Send initial handshake with current token stats
32
+ send({
33
+ type: 'hello',
34
+ version: WS_PROTOCOL_VERSION,
35
+ mode: config.mode,
36
+ model: config.model,
37
+ provider: config.provider,
38
+ totalIn: session.totalInputTokens,
39
+ totalOut: session.totalOutputTokens,
40
+ totalApi: session.totalApiCalls,
41
+ totalCacheHit: session.totalCacheHitTokens || undefined,
42
+ totalCacheMiss: session.totalCacheMissTokens || undefined,
43
+ })
44
+
45
+ // ── Subscribe to EventBus ──
46
+
47
+ const unsubTurnStart = bus.on(EventChannels.TURN_START, data => {
48
+ const d = data as { prompt?: string }
49
+ send({ type: 'turn_start', prompt: d.prompt || '' })
50
+ })
51
+
52
+ const unsubToolCall = bus.on(EventChannels.TOOL_CALL, data => {
53
+ const d = data as { name: string; input: Record<string, unknown>; id: string }
54
+ send({ type: 'tool_call', name: d.name, input: d.input, id: d.id })
55
+ })
56
+
57
+ const unsubToolResult = bus.on(EventChannels.TOOL_RESULT, data => {
58
+ const d = data as { name: string; id: string; output: string }
59
+ send({
60
+ type: 'tool_result',
61
+ name: d.name,
62
+ id: d.id,
63
+ success: true,
64
+ output: d.name === 'edit' ? d.output : stripAnsi(d.output),
65
+ })
66
+ })
67
+
68
+ const unsubToolError = bus.on(EventChannels.TOOL_ERROR, data => {
69
+ const d = data as { name: string; id: string; error: string }
70
+ send({ type: 'tool_result', name: d.name, id: d.id, success: false, error: stripAnsi(d.error) })
71
+ })
72
+
73
+ const unsubTurnEnd = bus.on(EventChannels.TURN_END, data => {
74
+ const d = data as { iterations: number; toolCallCount: number }
75
+ send({ type: 'turn_end', iterations: d.iterations, toolCallCount: d.toolCallCount })
76
+ })
77
+
78
+ const unsubThinking = bus.on(EventChannels.THINKING, data => {
79
+ const d = data as { text: string }
80
+ send({ type: 'thinking', text: d.text })
81
+ })
82
+
83
+ const unsubThinkingEnd = bus.on(EventChannels.THINKING_END, () => {
84
+ send({ type: 'thinking_end' })
85
+ })
86
+
87
+ const unsubCompaction = bus.on(EventChannels.COMPACTION_TRIGGERED, data => {
88
+ const d = data as { before: number; after: number }
89
+ send({ type: 'compaction', before: d.before, after: d.after })
90
+ })
91
+
92
+ const unsubTokenStats = bus.on(EventChannels.TOKEN_STATS, data => {
93
+ const d = data as {
94
+ turnIn: number
95
+ turnOut: number
96
+ totalIn: number
97
+ totalOut: number
98
+ turnApi: number
99
+ totalApi: number
100
+ turnCacheHit?: number
101
+ turnCacheMiss?: number
102
+ totalCacheHit?: number
103
+ totalCacheMiss?: number
104
+ }
105
+ send({ type: 'token_stats', ...d })
106
+ })
107
+
108
+ // ── Handle incoming messages from client ──
109
+
110
+ async function handleClientMessage(msg: WsMessage): Promise<void> {
111
+ switch (msg.type) {
112
+ case 'message': {
113
+ const text = String(msg.text || '')
114
+ if (!text.trim()) return
115
+
116
+ // Handle slash commands
117
+ if (text.startsWith('/')) {
118
+ const parts = text.slice(1).split(/\s+/)
119
+ const cmd = parts[0]
120
+ const arg = parts.slice(1).join(' ')
121
+
122
+ if (cmd === 'mode' && (arg === 'code' || arg === 'plan' || arg === 'ask')) {
123
+ session.setMode(arg as 'code' | 'plan' | 'ask')
124
+ send({ type: 'mode_changed', mode: arg })
125
+ return
126
+ }
127
+
128
+ if (cmd === 'model' && arg) {
129
+ session.config.model = arg
130
+ session.setMode(session.config.mode)
131
+ send({ type: 'model_changed', model: arg })
132
+ return
133
+ }
134
+
135
+ if (cmd === 'help') {
136
+ send({
137
+ type: 'help',
138
+ commands: [
139
+ '/mode code|plan|ask - Switch mode',
140
+ '/model <name> - Switch model',
141
+ '/new - Start a new session',
142
+ '/help - Show this help',
143
+ ],
144
+ })
145
+ return
146
+ }
147
+
148
+ send({ type: 'error', message: `Unknown command: ${cmd}` })
149
+ return
150
+ }
151
+
152
+ // Send user message to session
153
+ try {
154
+ await session.chat(text)
155
+ send({ type: 'done', reason: 'stop' })
156
+ } catch (err) {
157
+ const errMsg = err instanceof Error ? err.message : String(err)
158
+ send({ type: 'error', message: errMsg })
159
+ send({ type: 'done', reason: 'error' })
160
+ }
161
+ return
162
+ }
163
+
164
+ case 'ping':
165
+ send({ type: 'pong' })
166
+ return
167
+
168
+ case 'stop':
169
+ session.stop()
170
+ return
171
+
172
+ default:
173
+ send({ type: 'error', message: `Unknown message type: ${msg.type}` })
174
+ }
175
+ }
176
+
177
+ return {
178
+ close: () => {
179
+ unsubTurnStart()
180
+ unsubToolCall()
181
+ unsubToolResult()
182
+ unsubToolError()
183
+ unsubTurnEnd()
184
+ unsubThinking()
185
+ unsubThinkingEnd()
186
+ unsubCompaction()
187
+ unsubTokenStats()
188
+ },
189
+ sendMessage: async (text: string) => {
190
+ await handleClientMessage({ type: 'message', text })
191
+ },
192
+ }
193
+ }
@@ -1,14 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(npm run:*)",
5
- "Bash(grep:*)",
6
- "Bash(npx tsc:*)",
7
- "Bash(npm test:*)",
8
- "Bash(gh release:*)",
9
- "Read(//c/Program Files/GitHub CLI//**)",
10
- "Read(//c/Program Files \\(x86\\)/**)",
11
- "Bash(\"/c/Program Files/GitHub CLI/gh.exe\" release create 0.2.4 --title \"v0.2.4\" --generate-notes)"
12
- ]
13
- }
14
- }
@@ -1,171 +0,0 @@
1
- # Plan: Vendor `@earendil-works/pi-tui` source
2
-
3
- ## Objective
4
-
5
- Replace the `@earendil-works/pi-tui` npm dependency with vendored source code from [pi/packages/tui](https://github.com/earendil-works/pi/tree/main/packages/tui), so the project no longer depends on that npm package.
6
-
7
- ## Current state
8
-
9
- - `package.json` depends on `"@earendil-works/pi-tui": "0.75.5"`
10
- - A patch file `patches/@earendil-works+pi-tui+0.75.5.patch` applies 2 modifications (autocomplete above editor, alternate screen buffer on start/stop)
11
- - `patch-package` is in devDependencies for applying the patch
12
- - Only 2 source files import from `@earendil-works/pi-tui`:
13
- - `src/tui/index.ts` (type imports + 11 class exports)
14
- - `src/tui/components.ts` (Component type + SelectList class)
15
- - Internal Editor access: `(editor as any).killRing`, `.undoStack`, `.history`
16
-
17
- ## Steps
18
-
19
- ### 1. Download pi-tui source from GitHub
20
-
21
- Clone a shallow copy of the repo to get the source at the matching version or the main branch:
22
-
23
- ```bash
24
- # From project root temp dir
25
- git clone --depth 1 --branch main https://github.com/earendil-works/pi.git /tmp/pi-source
26
- ```
27
-
28
- Alternatively, if git clone is not feasible, download each file individually from GitHub.
29
-
30
- Source files needed from `packages/tui/src/`:
31
- ```
32
- src/
33
- index.ts - barrel exports
34
- autocomplete.ts - CombinedAutocompleteProvider, AutocompleteProvider, etc.
35
- editor-component.ts - EditorComponent type
36
- fuzzy.ts - fuzzyFilter, fuzzyMatch
37
- keybindings.ts - KeybindingsManager, TUI_KEYBINDINGS, etc.
38
- keys.ts - Key, matchesKey, parseKey, etc.
39
- kill-ring.ts - KillRing
40
- native-modifiers.ts - native keyboard modifiers
41
- stdin-buffer.ts - StdinBuffer
42
- terminal-image.ts - image rendering
43
- terminal.ts - ProcessTerminal, Terminal interface
44
- tui.ts - TUI, Container, Component, OverlayHandle, OverlayOptions, Focusable, CURSOR_MARKER, etc.
45
- undo-stack.ts - UndoStack
46
- utils.ts - visibleWidth, truncateToWidth, wrapTextWithAnsi, etc.
47
- word-navigation.ts - word boundary navigation
48
- components/
49
- box.ts
50
- cancellable-loader.ts
51
- editor.ts
52
- image.ts
53
- input.ts
54
- loader.ts
55
- markdown.ts
56
- select-list.ts
57
- settings-list.ts
58
- spacer.ts
59
- text.ts
60
- truncated-text.ts
61
- ```
62
-
63
- Also copy `native/` directory (Windows VT input helper):
64
- ```
65
- native/win32/prebuilds/win32-x64/win32-console-mode.node
66
- native/win32/prebuilds/win32-arm64/win32-console-mode.node
67
- ```
68
-
69
- ### 2. Place vendored code at `src/pi-tui/`
70
-
71
- ```
72
- src/
73
- pi-tui/
74
- index.ts
75
- autocomplete.ts
76
- editor-component.ts
77
- fuzzy.ts
78
- keybindings.ts
79
- keys.ts
80
- kill-ring.ts
81
- native-modifiers.ts
82
- stdin-buffer.ts
83
- terminal-image.ts
84
- terminal.ts
85
- tui.ts
86
- undo-stack.ts
87
- utils.ts
88
- word-navigation.ts
89
- components/
90
- box.ts
91
- cancellable-loader.ts
92
- editor.ts
93
- image.ts
94
- input.ts
95
- loader.ts
96
- markdown.ts
97
- select-list.ts
98
- settings-list.ts
99
- spacer.ts
100
- text.ts
101
- truncated-text.ts
102
- native/
103
- win32/prebuilds/win32-x64/win32-console-mode.node
104
- win32/prebuilds/win32-arm64/win32-console-mode.node
105
- ```
106
-
107
- ### 3. Apply the two manual patches to the vendored source
108
-
109
- The patch file modifies the **compiled dist JS**, but we need to apply equivalent changes to the **TypeScript source**:
110
-
111
- **Patch 1** — `components/editor.ts`: Move autocomplete rendering above the editor
112
- - Find the `render()` method where autocomplete lines are appended
113
- - Change `result.push(...acLines)` → `result.unshift(...acLines)` (or equivalent logic)
114
-
115
- **Patch 2** — `tui.ts`: Add alternate screen buffer on start/stop
116
- - In `start()`: add `this.terminal.write("\x1b[?1049h");` before `this.terminal.start(…)`
117
- - In `stop()`: add `this.terminal.write("\x1b[?1049l");` before cursor/timer cleanup
118
-
119
- ### 4. Update import paths
120
-
121
- **`src/tui/index.ts`** — change:
122
- ```ts
123
- import type { EditorTheme, MarkdownTheme, OverlayHandle, SelectItem, SelectListTheme, SlashCommand } from '@earendil-works/pi-tui'
124
- import { Box, CombinedAutocompleteProvider, Container, Editor, Loader, Markdown, ProcessTerminal, Text, TUI } from '@earendil-works/pi-tui'
125
- ```
126
- to:
127
- ```ts
128
- import type { EditorTheme, MarkdownTheme, OverlayHandle, SelectItem, SelectListTheme, SlashCommand } from '../pi-tui/index.js'
129
- import { Box, CombinedAutocompleteProvider, Container, Editor, Loader, Markdown, ProcessTerminal, Text, TUI } from '../pi-tui/index.js'
130
- ```
131
-
132
- **`src/tui/components.ts`** — change:
133
- ```ts
134
- import type { Component, SelectItem, SelectListTheme } from '@earendil-works/pi-tui'
135
- import { SelectList } from '@earendil-works/pi-tui'
136
- ```
137
- to:
138
- ```ts
139
- import type { Component, SelectItem, SelectListTheme } from '../pi-tui/index.js'
140
- import { SelectList } from '../pi-tui/index.js'
141
- ```
142
-
143
- ### 5. Keep `marked` and `get-east-asian-width` as runtime dependencies
144
-
145
- These are pi-tui's own runtime dependencies used by `Markdown` and `visibleWidth` respectively. They remain in `dependencies`.
146
-
147
- ### 6. Remove pi-tui-related npm setup
148
-
149
- - `package.json`: Remove `"@earendil-works/pi-tui": "0.75.5"` from `dependencies`
150
- - `package.json`: Remove `"patch-package": "^8.0.1"` from `devDependencies` (only existed for pi-tui patching)
151
- - `package.json`: Change `"postinstall": "npx patch-package"` to `"postinstall": "husky"` (or remove entirely if not needed)
152
- - Delete `patches/@earendil-works+pi-tui+0.75.5.patch`
153
- - Delete `patches/` directory if empty
154
- - Run `npm uninstall @earendil-works/pi-tui` and `npm uninstall patch-package` to update lockfile
155
-
156
- ### 7. Update `package-lock.json`
157
-
158
- Run `npm install` to regenerate `package-lock.json` without pi-tui/patch-package.
159
-
160
- ### 8. Verify
161
-
162
- - `npm run build` — tsc should compile successfully
163
- - `npm run lint` — biome should pass
164
- - `npm test` — vitest tests should pass
165
- - Verify the TUI starts correctly with `npm run dev`
166
-
167
- ## Open questions
168
-
169
- 1. Should we use the exact version `0.75.5` source (may need to checkout a tag) or the latest `main` branch source? **Answer: main branch is fine unless it introduces breaking changes.**
170
- 2. The `native-modifiers.ts` file on GitHub may export functions used by `terminal.ts`. If unavailable, we can stub it or inline the native module loading logic.
171
- 3. If the GitHub raw URLs are flaky, alternative: download from npm tarball and reverse-engineer from dist JS (less ideal).
@@ -1,62 +0,0 @@
1
- # Plan: Optimize edit tool error handling for model
2
-
3
- ## Problem
4
- The model calls the `edit` tool with errors very frequently. The most common failures:
5
- 1. `old_string not found` (whitespace issues, stale content, hallucinated content)
6
- 2. `old_string appears MULTIPLE times` (insufficient context)
7
- 3. File doesn't exist / already exists
8
- 4. Input format misuse (already mostly auto-corrected)
9
-
10
- Error messages are returned to the model (via `session.ts:687` as `ERROR: ${result.error}`), but they lack actionable recovery guidance, and noise obscures the core issue.
11
-
12
- ## Changes
13
-
14
- ### 1. Use `FileReadTracker.checkModified()` to detect stale content
15
- The `FileReadTracker` has a `checkModified()` method that is never called in production code. The edit tool should call it before attempting edits to detect:
16
- - File was never read in this session → suggest `read` first
17
- - File was externally modified since last read → suggest re-reading
18
-
19
- This catches a major source of `old_string not found` errors — the model editing content it hasn't read, or editing stale content.
20
-
21
- **Location**: `edit.ts`, in the `for (const [resolved, group] of fileGroups)` loop, before processing edits.
22
-
23
- ### 2. Include proximity hint in `firstError`
24
- Currently the hint (near-line snippet / file content) is only in `results.push()` but NOT in `firstError`. This means the compact "First error:" line at the bottom of the batch error message lacks the most useful context.
25
-
26
- **Fix**: Update lines ~339-342 to include hint in `firstError`.
27
-
28
- ### 3. Add actionable "Suggestion:" line to failure messages
29
- When `old_string not found`, the error should tell the model what to do:
30
- - "Suggestion: Read the file again with `read({ paths: [...] })` to get current content, then retry with exact matching text. Include 2-3 lines of surrounding context for uniqueness."
31
- - "Suggestion: Whitespace-tolerant matching was attempted but no match found. The text content differs — not just whitespace."
32
-
33
- When `old_string appears MULTIPLE times`:
34
- - "Suggestion: Include more surrounding context lines (2-3 lines before and after) in old_string to make the match unique."
35
-
36
- When `File not found`:
37
- - "Suggestion: Check the file path or create the file first with old_string: '' (empty string)."
38
-
39
- When `File already exists` (on create):
40
- - "Suggestion: Use edit with old_string to replace existing content, or use a different file path."
41
-
42
- ### 4. Clean up batch failure output
43
- When a batch fails, the current output includes ALL results (even successful edits that were rolled back). This is noisy.
44
-
45
- **Fix**: Only show FAIL lines in the batch error, plus a brief summary like "N edit(s) before failure were rolled back."
46
-
47
- ### 5. Improve "multiple matches" error with location hints
48
- When old_string matches multiple times, show the file content at each match location (similar to the "near line" hint for not-found).
49
-
50
- ### 6. Better `edits` format error
51
- Tighten the "edits array required" error to be more compact.
52
-
53
- ## Files to modify
54
- - `src/tools/edit.ts` — all the above changes
55
-
56
- ## Tests
57
- - All existing tests in `src/tools/__tests__/edit.test.ts` must pass
58
- - Some test assertions may need updating if error message content changes
59
- - Specifically:
60
- - `expect(r.error).toContain('rolled back')` — still valid since rolled-back summary stays
61
- - `expect(r.error).toContain('hello')` — still valid since hint stays in error
62
- - Other `toContain('not found')`, `toContain('MULTIPLE times')` — still valid