lonny-agent 0.1.2 → 0.1.9
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/.kilo/plans/1780064105789-mighty-pixel.md +171 -0
- package/.lonny/fix-new-command-session-cleanup.md +65 -0
- package/.lonny/tui-autocomplete-above.md +62 -0
- package/dist/agent/__tests__/compaction.test.js +6 -4
- package/dist/agent/__tests__/compaction.test.js.map +1 -1
- package/dist/agent/llm.d.ts +1 -1
- package/dist/agent/llm.d.ts.map +1 -1
- package/dist/agent/prompt-builder.d.ts.map +1 -1
- package/dist/agent/prompt-builder.js +26 -8
- package/dist/agent/prompt-builder.js.map +1 -1
- package/dist/agent/providers/anthropic.d.ts +1 -1
- package/dist/agent/providers/anthropic.d.ts.map +1 -1
- package/dist/agent/providers/anthropic.js +2 -2
- package/dist/agent/providers/anthropic.js.map +1 -1
- package/dist/agent/providers/google.d.ts +1 -1
- package/dist/agent/providers/google.d.ts.map +1 -1
- package/dist/agent/providers/google.js +2 -1
- package/dist/agent/providers/google.js.map +1 -1
- package/dist/agent/providers/ollama.d.ts +1 -1
- package/dist/agent/providers/ollama.d.ts.map +1 -1
- package/dist/agent/providers/ollama.js +2 -1
- package/dist/agent/providers/ollama.js.map +1 -1
- package/dist/agent/providers/openai.d.ts +1 -1
- package/dist/agent/providers/openai.d.ts.map +1 -1
- package/dist/agent/providers/openai.js +2 -2
- package/dist/agent/providers/openai.js.map +1 -1
- package/dist/agent/session.d.ts +1 -0
- package/dist/agent/session.d.ts.map +1 -1
- package/dist/agent/session.js +8 -2
- package/dist/agent/session.js.map +1 -1
- package/dist/pi-tui/autocomplete.d.ts +54 -0
- package/dist/pi-tui/autocomplete.d.ts.map +1 -0
- package/dist/pi-tui/autocomplete.js +636 -0
- package/dist/pi-tui/autocomplete.js.map +1 -0
- package/dist/pi-tui/components/box.d.ts +22 -0
- package/dist/pi-tui/components/box.d.ts.map +1 -0
- package/dist/pi-tui/components/box.js +104 -0
- package/dist/pi-tui/components/box.js.map +1 -0
- package/dist/pi-tui/components/cancellable-loader.d.ts +22 -0
- package/dist/pi-tui/components/cancellable-loader.d.ts.map +1 -0
- package/dist/pi-tui/components/cancellable-loader.js +35 -0
- package/dist/pi-tui/components/cancellable-loader.js.map +1 -0
- package/dist/pi-tui/components/editor.d.ts +249 -0
- package/dist/pi-tui/components/editor.d.ts.map +1 -0
- package/dist/pi-tui/components/editor.js +1883 -0
- package/dist/pi-tui/components/editor.js.map +1 -0
- package/dist/pi-tui/components/image.d.ts +28 -0
- package/dist/pi-tui/components/image.d.ts.map +1 -0
- package/dist/pi-tui/components/image.js +90 -0
- package/dist/pi-tui/components/image.js.map +1 -0
- package/dist/pi-tui/components/input.d.ts +37 -0
- package/dist/pi-tui/components/input.d.ts.map +1 -0
- package/dist/pi-tui/components/input.js +382 -0
- package/dist/pi-tui/components/input.js.map +1 -0
- package/dist/pi-tui/components/loader.d.ts +31 -0
- package/dist/pi-tui/components/loader.d.ts.map +1 -0
- package/dist/pi-tui/components/loader.js +70 -0
- package/dist/pi-tui/components/loader.js.map +1 -0
- package/dist/pi-tui/components/markdown.d.ts +96 -0
- package/dist/pi-tui/components/markdown.d.ts.map +1 -0
- package/dist/pi-tui/components/markdown.js +647 -0
- package/dist/pi-tui/components/markdown.js.map +1 -0
- package/dist/pi-tui/components/select-list.d.ts +50 -0
- package/dist/pi-tui/components/select-list.d.ts.map +1 -0
- package/dist/pi-tui/components/select-list.js +167 -0
- package/dist/pi-tui/components/select-list.js.map +1 -0
- package/dist/pi-tui/components/settings-list.d.ts +50 -0
- package/dist/pi-tui/components/settings-list.d.ts.map +1 -0
- package/dist/pi-tui/components/settings-list.js +189 -0
- package/dist/pi-tui/components/settings-list.js.map +1 -0
- package/dist/pi-tui/components/spacer.d.ts +12 -0
- package/dist/pi-tui/components/spacer.d.ts.map +1 -0
- package/dist/pi-tui/components/spacer.js +23 -0
- package/dist/pi-tui/components/spacer.js.map +1 -0
- package/dist/pi-tui/components/text.d.ts +19 -0
- package/dist/pi-tui/components/text.d.ts.map +1 -0
- package/dist/pi-tui/components/text.js +91 -0
- package/dist/pi-tui/components/text.js.map +1 -0
- package/dist/pi-tui/components/truncated-text.d.ts +13 -0
- package/dist/pi-tui/components/truncated-text.d.ts.map +1 -0
- package/dist/pi-tui/components/truncated-text.js +51 -0
- package/dist/pi-tui/components/truncated-text.js.map +1 -0
- package/dist/pi-tui/editor-component.d.ts +39 -0
- package/dist/pi-tui/editor-component.d.ts.map +1 -0
- package/dist/pi-tui/editor-component.js +2 -0
- package/dist/pi-tui/editor-component.js.map +1 -0
- package/dist/pi-tui/fuzzy.d.ts +16 -0
- package/dist/pi-tui/fuzzy.d.ts.map +1 -0
- package/dist/pi-tui/fuzzy.js +110 -0
- package/dist/pi-tui/fuzzy.js.map +1 -0
- package/dist/pi-tui/index.d.ts +23 -0
- package/dist/pi-tui/index.d.ts.map +1 -0
- package/dist/pi-tui/index.js +32 -0
- package/dist/pi-tui/index.js.map +1 -0
- package/dist/pi-tui/keybindings.d.ts +193 -0
- package/dist/pi-tui/keybindings.d.ts.map +1 -0
- package/dist/pi-tui/keybindings.js +174 -0
- package/dist/pi-tui/keybindings.js.map +1 -0
- package/dist/pi-tui/keys.d.ts +184 -0
- package/dist/pi-tui/keys.d.ts.map +1 -0
- package/dist/pi-tui/keys.js +1182 -0
- package/dist/pi-tui/keys.js.map +1 -0
- package/dist/pi-tui/kill-ring.d.ts +28 -0
- package/dist/pi-tui/kill-ring.d.ts.map +1 -0
- package/dist/pi-tui/kill-ring.js +44 -0
- package/dist/pi-tui/kill-ring.js.map +1 -0
- package/dist/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node +0 -0
- package/dist/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
- package/dist/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
- package/dist/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
- package/dist/pi-tui/native-modifiers.d.ts +3 -0
- package/dist/pi-tui/native-modifiers.d.ts.map +1 -0
- package/dist/pi-tui/native-modifiers.js +53 -0
- package/dist/pi-tui/native-modifiers.js.map +1 -0
- package/dist/pi-tui/stdin-buffer.d.ts +50 -0
- package/dist/pi-tui/stdin-buffer.d.ts.map +1 -0
- package/dist/pi-tui/stdin-buffer.js +361 -0
- package/dist/pi-tui/stdin-buffer.js.map +1 -0
- package/dist/pi-tui/terminal-image.d.ts +90 -0
- package/dist/pi-tui/terminal-image.d.ts.map +1 -0
- package/dist/pi-tui/terminal-image.js +343 -0
- package/dist/pi-tui/terminal-image.js.map +1 -0
- package/dist/pi-tui/terminal.d.ts +113 -0
- package/dist/pi-tui/terminal.d.ts.map +1 -0
- package/dist/pi-tui/terminal.js +478 -0
- package/dist/pi-tui/terminal.js.map +1 -0
- package/dist/pi-tui/tui.d.ts +227 -0
- package/dist/pi-tui/tui.d.ts.map +1 -0
- package/dist/pi-tui/tui.js +1091 -0
- package/dist/pi-tui/tui.js.map +1 -0
- package/dist/pi-tui/undo-stack.d.ts +17 -0
- package/dist/pi-tui/undo-stack.d.ts.map +1 -0
- package/dist/pi-tui/undo-stack.js +25 -0
- package/dist/pi-tui/undo-stack.js.map +1 -0
- package/dist/pi-tui/utils.d.ts +84 -0
- package/dist/pi-tui/utils.d.ts.map +1 -0
- package/dist/pi-tui/utils.js +1024 -0
- package/dist/pi-tui/utils.js.map +1 -0
- package/dist/pi-tui/word-navigation.d.ts +25 -0
- package/dist/pi-tui/word-navigation.d.ts.map +1 -0
- package/dist/pi-tui/word-navigation.js +98 -0
- package/dist/pi-tui/word-navigation.js.map +1 -0
- package/dist/tools/__tests__/edit.test.js +85 -0
- package/dist/tools/__tests__/edit.test.js.map +1 -1
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +23 -1
- package/dist/tools/edit.js.map +1 -1
- package/dist/tui/components.d.ts +1 -1
- package/dist/tui/components.d.ts.map +1 -1
- package/dist/tui/components.js +1 -1
- package/dist/tui/components.js.map +1 -1
- package/dist/tui/editor-patch.d.ts +13 -0
- package/dist/tui/editor-patch.d.ts.map +1 -0
- package/dist/tui/editor-patch.js +41 -0
- package/dist/tui/editor-patch.js.map +1 -0
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +15 -3
- package/dist/tui/index.js.map +1 -1
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +9 -2
- package/dist/web/index.js.map +1 -1
- package/dist/web/public/app.js +65 -4
- package/dist/web/public/style.css +49 -0
- package/dist/web/session-bridge.d.ts +0 -4
- package/dist/web/session-bridge.d.ts.map +1 -1
- package/dist/web/session-bridge.js +16 -2
- package/dist/web/session-bridge.js.map +1 -1
- package/package.json +5 -3
- package/src/agent/__tests__/compaction.test.ts +6 -4
- package/src/agent/llm.ts +5 -1
- package/src/agent/prompt-builder.ts +27 -8
- package/src/agent/providers/anthropic.ts +15 -8
- package/src/agent/providers/google.ts +6 -1
- package/src/agent/providers/ollama.ts +6 -1
- package/src/agent/providers/openai.ts +21 -12
- package/src/agent/session.ts +12 -2
- package/src/pi-tui/autocomplete.ts +808 -0
- package/src/pi-tui/components/box.ts +137 -0
- package/src/pi-tui/components/cancellable-loader.ts +40 -0
- package/src/pi-tui/components/editor.ts +2321 -0
- package/src/pi-tui/components/image.ts +130 -0
- package/src/pi-tui/components/input.ts +456 -0
- package/src/pi-tui/components/loader.ts +93 -0
- package/src/pi-tui/components/markdown.ts +839 -0
- package/src/pi-tui/components/select-list.ts +263 -0
- package/src/pi-tui/components/settings-list.ts +264 -0
- package/src/pi-tui/components/spacer.ts +28 -0
- package/src/pi-tui/components/text.ts +113 -0
- package/src/pi-tui/components/truncated-text.ts +65 -0
- package/src/pi-tui/editor-component.ts +74 -0
- package/src/pi-tui/fuzzy.ts +137 -0
- package/src/pi-tui/index.ts +115 -0
- package/src/pi-tui/keybindings.ts +245 -0
- package/src/pi-tui/keys.ts +1440 -0
- package/src/pi-tui/kill-ring.ts +46 -0
- package/src/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node +0 -0
- package/src/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
- package/src/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
- package/src/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
- package/src/pi-tui/native-modifiers.ts +65 -0
- package/src/pi-tui/stdin-buffer.ts +434 -0
- package/src/pi-tui/terminal-image.ts +469 -0
- package/src/pi-tui/terminal.ts +594 -0
- package/src/pi-tui/tui.ts +1376 -0
- package/src/pi-tui/undo-stack.ts +28 -0
- package/src/pi-tui/utils.ts +1176 -0
- package/src/pi-tui/word-navigation.ts +127 -0
- package/src/tools/__tests__/edit.test.ts +91 -0
- package/src/tools/edit.ts +27 -1
- package/src/tui/components.ts +2 -2
- package/src/tui/index.ts +24 -10
- package/src/web/index.ts +9 -2
- package/src/web/public/app.js +65 -4
- package/src/web/public/style.css +49 -0
- package/src/web/session-bridge.ts +13 -2
- package/tsconfig.json +2 -2
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { getWordSegmenter, isWhitespaceChar, PUNCTUATION_REGEX } from './utils.js'
|
|
2
|
+
|
|
3
|
+
const wordSegmenter = getWordSegmenter()
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Options for word navigation functions.
|
|
7
|
+
* When omitted, uses the default Intl.Segmenter word segmentation.
|
|
8
|
+
*/
|
|
9
|
+
export interface WordNavigationOptions {
|
|
10
|
+
/** Custom segmenter returning word segments for the given text. */
|
|
11
|
+
segment?: (text: string) => Iterable<Intl.SegmentData>
|
|
12
|
+
/** Predicate identifying atomic segments that should be treated as single units (e.g. paste markers). */
|
|
13
|
+
isAtomicSegment?: (segment: string) => boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Find the cursor position after moving one word backward from `cursor` in `text`.
|
|
18
|
+
* Skips trailing whitespace, then stops at the next word/punctuation boundary.
|
|
19
|
+
*
|
|
20
|
+
* Pure function - does not mutate any state.
|
|
21
|
+
*/
|
|
22
|
+
export function findWordBackward(
|
|
23
|
+
text: string,
|
|
24
|
+
cursor: number,
|
|
25
|
+
options?: WordNavigationOptions,
|
|
26
|
+
): number {
|
|
27
|
+
if (cursor <= 0) return 0
|
|
28
|
+
|
|
29
|
+
const textBeforeCursor = text.slice(0, cursor)
|
|
30
|
+
const segmentFn = options?.segment
|
|
31
|
+
const isAtomic = options?.isAtomicSegment
|
|
32
|
+
const segments = segmentFn
|
|
33
|
+
? [...segmentFn(textBeforeCursor)]
|
|
34
|
+
: [...wordSegmenter.segment(textBeforeCursor)]
|
|
35
|
+
let newCursor = cursor
|
|
36
|
+
|
|
37
|
+
// Skip trailing whitespace
|
|
38
|
+
while (
|
|
39
|
+
segments.length > 0 &&
|
|
40
|
+
!isAtomic?.(segments[segments.length - 1]?.segment || '') &&
|
|
41
|
+
isWhitespaceChar(segments[segments.length - 1]?.segment || '')
|
|
42
|
+
) {
|
|
43
|
+
newCursor -= segments.pop()?.segment.length || 0
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (segments.length === 0) return newCursor
|
|
47
|
+
|
|
48
|
+
const last = segments[segments.length - 1]!
|
|
49
|
+
|
|
50
|
+
if (isAtomic?.(last.segment)) {
|
|
51
|
+
// Skip one atomic segment.
|
|
52
|
+
newCursor -= last.segment.length
|
|
53
|
+
} else if (last.isWordLike) {
|
|
54
|
+
// Skip inside one word-like segment, preserving ASCII punctuation boundaries.
|
|
55
|
+
const segment = last.segment
|
|
56
|
+
const matches = [...segment.matchAll(new RegExp(PUNCTUATION_REGEX, 'g'))]
|
|
57
|
+
if (matches.length <= 0) {
|
|
58
|
+
newCursor -= segment.length
|
|
59
|
+
} else {
|
|
60
|
+
const lastMatch = matches[matches.length - 1]!
|
|
61
|
+
newCursor -= segment.length - (lastMatch.index + lastMatch[0].length)
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
// Skip non-word non-whitespace run (punctuation)
|
|
65
|
+
while (
|
|
66
|
+
segments.length > 0 &&
|
|
67
|
+
!isAtomic?.(segments[segments.length - 1]?.segment || '') &&
|
|
68
|
+
!segments[segments.length - 1]?.isWordLike &&
|
|
69
|
+
!isWhitespaceChar(segments[segments.length - 1]?.segment || '')
|
|
70
|
+
) {
|
|
71
|
+
newCursor -= segments.pop()?.segment.length || 0
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return newCursor
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Find the cursor position after moving one word forward from `cursor` in `text`.
|
|
80
|
+
* Skips leading whitespace, then stops at the next word/punctuation boundary.
|
|
81
|
+
*
|
|
82
|
+
* Pure function - does not mutate any state.
|
|
83
|
+
*/
|
|
84
|
+
export function findWordForward(
|
|
85
|
+
text: string,
|
|
86
|
+
cursor: number,
|
|
87
|
+
options?: WordNavigationOptions,
|
|
88
|
+
): number {
|
|
89
|
+
if (cursor >= text.length) return text.length
|
|
90
|
+
|
|
91
|
+
const textAfterCursor = text.slice(cursor)
|
|
92
|
+
const segmentFn = options?.segment
|
|
93
|
+
const isAtomic = options?.isAtomicSegment
|
|
94
|
+
const segments = segmentFn ? segmentFn(textAfterCursor) : wordSegmenter.segment(textAfterCursor)
|
|
95
|
+
const iterator = segments[Symbol.iterator]()
|
|
96
|
+
let next = iterator.next()
|
|
97
|
+
let newCursor = cursor
|
|
98
|
+
|
|
99
|
+
// Skip leading whitespace
|
|
100
|
+
while (!next.done && !isAtomic?.(next.value.segment) && isWhitespaceChar(next.value.segment)) {
|
|
101
|
+
newCursor += next.value.segment.length
|
|
102
|
+
next = iterator.next()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (next.done) return newCursor
|
|
106
|
+
|
|
107
|
+
if (isAtomic?.(next.value.segment)) {
|
|
108
|
+
// Skip one atomic segment.
|
|
109
|
+
newCursor += next.value.segment.length
|
|
110
|
+
} else if (next.value.isWordLike) {
|
|
111
|
+
// Skip inside one word-like segment, preserving ASCII punctuation boundaries.
|
|
112
|
+
newCursor += PUNCTUATION_REGEX.exec(next.value.segment)?.index ?? next.value.segment.length
|
|
113
|
+
} else {
|
|
114
|
+
// Skip non-word non-whitespace run (punctuation)
|
|
115
|
+
while (
|
|
116
|
+
!next.done &&
|
|
117
|
+
!isAtomic?.(next.value.segment) &&
|
|
118
|
+
!next.value.isWordLike &&
|
|
119
|
+
!isWhitespaceChar(next.value.segment)
|
|
120
|
+
) {
|
|
121
|
+
newCursor += next.value.segment.length
|
|
122
|
+
next = iterator.next()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return newCursor
|
|
127
|
+
}
|
|
@@ -155,6 +155,97 @@ describe('edit tool', () => {
|
|
|
155
155
|
})
|
|
156
156
|
})
|
|
157
157
|
|
|
158
|
+
describe('diff output', () => {
|
|
159
|
+
beforeAll(() => {
|
|
160
|
+
fs.writeFileSync(path.join(tmpDir, 'diff-test.txt'), 'line one\nline two\nline three\n')
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('shows green + for new file creation (no red - lines)', async () => {
|
|
164
|
+
const r = await tool().execute({
|
|
165
|
+
edits: [{ file_path: 'diff-new.txt', old_string: '', new_string: 'hello\nworld' }],
|
|
166
|
+
})
|
|
167
|
+
expect(r.success).toBe(true)
|
|
168
|
+
// Should contain green ANSI code for added lines
|
|
169
|
+
expect(r.output).toContain('\x1b[38;2;0;200;100m') // green
|
|
170
|
+
expect(r.output).toContain('+ hello')
|
|
171
|
+
expect(r.output).toContain('+ world')
|
|
172
|
+
// Should NOT contain red ANSI code (no removed lines)
|
|
173
|
+
expect(r.output).not.toContain('\x1b[38;2;255;80;80m')
|
|
174
|
+
expect(r.output).toContain('Created')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('shows red - for removed lines and green + for added lines on replacement', async () => {
|
|
178
|
+
const r = await tool().execute({
|
|
179
|
+
edits: [
|
|
180
|
+
{
|
|
181
|
+
file_path: 'diff-test.txt',
|
|
182
|
+
old_string: 'line two',
|
|
183
|
+
new_string: 'line TWO',
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
})
|
|
187
|
+
expect(r.success).toBe(true)
|
|
188
|
+
// Should contain red ANSI code for removed content
|
|
189
|
+
expect(r.output).toContain('\x1b[38;2;255;80;80m')
|
|
190
|
+
expect(r.output).toContain('- line two')
|
|
191
|
+
// Should contain green ANSI code for added content
|
|
192
|
+
expect(r.output).toContain('\x1b[38;2;0;200;100m')
|
|
193
|
+
expect(r.output).toContain('+ line TWO')
|
|
194
|
+
expect(r.output).toContain('Edited')
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('shows multi-line diff correctly', async () => {
|
|
198
|
+
const r = await tool().execute({
|
|
199
|
+
edits: [
|
|
200
|
+
{
|
|
201
|
+
file_path: 'diff-test.txt',
|
|
202
|
+
old_string: 'line one\nline TWO',
|
|
203
|
+
new_string: 'line 1\nline 2',
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
})
|
|
207
|
+
expect(r.success).toBe(true)
|
|
208
|
+
// Removed lines
|
|
209
|
+
expect(r.output).toContain('- line one')
|
|
210
|
+
expect(r.output).toContain('- line TWO')
|
|
211
|
+
// Added lines
|
|
212
|
+
expect(r.output).toContain('+ line 1')
|
|
213
|
+
expect(r.output).toContain('+ line 2')
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('handles CRLF in old_string (Windows compatibility)', async () => {
|
|
217
|
+
fs.writeFileSync(path.join(tmpDir, 'crlf-test.txt'), 'a\\r\\nb\\r\\nc\\r\\n')
|
|
218
|
+
const r = await tool().execute({
|
|
219
|
+
edits: [
|
|
220
|
+
{
|
|
221
|
+
file_path: 'crlf-test.txt',
|
|
222
|
+
// Simulate AI sending old_string with \\r\\n (CRLF) on Windows
|
|
223
|
+
old_string: 'b\\r\\n',
|
|
224
|
+
new_string: 'B\\r\\n',
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
})
|
|
228
|
+
expect(r.success).toBe(true)
|
|
229
|
+
const content = fs.readFileSync(path.join(tmpDir, 'crlf-test.txt'), 'utf8')
|
|
230
|
+
expect(content).toContain('B')
|
|
231
|
+
// Should show the normalized diff (with \\n, not \\r\\n)
|
|
232
|
+
expect(r.output).toContain('- b')
|
|
233
|
+
expect(r.output).toContain('+ B')
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('does not show empty red line when old_string is empty', async () => {
|
|
237
|
+
const r = await tool().execute({
|
|
238
|
+
edits: [{ file_path: 'empty-old.txt', old_string: '', new_string: 'only added' }],
|
|
239
|
+
})
|
|
240
|
+
expect(r.success).toBe(true)
|
|
241
|
+
// Should have green + lines
|
|
242
|
+
expect(r.output).toContain('+ only added')
|
|
243
|
+
// Count red-start markers — should be 0 (no removed content)
|
|
244
|
+
const redCount = (r.output.match(/\x1b\[38;2;255;80;80m/g) || []).length
|
|
245
|
+
expect(redCount).toBe(0)
|
|
246
|
+
})
|
|
247
|
+
})
|
|
248
|
+
|
|
158
249
|
describe('create mode', () => {
|
|
159
250
|
const createdDir = () => path.join(tmpDir, 'created-test')
|
|
160
251
|
|
package/src/tools/edit.ts
CHANGED
|
@@ -4,6 +4,24 @@ import type { FileReadTracker } from '../diff/apply.js'
|
|
|
4
4
|
import { fmtErr } from './errors.js'
|
|
5
5
|
import type { Tool, ToolResult } from './types.js'
|
|
6
6
|
|
|
7
|
+
// ── ANSI colors for diff output ─────────────────────────────────────────
|
|
8
|
+
const DIFF_RED = '\x1b[38;2;255;80;80m'
|
|
9
|
+
const DIFF_GREEN = '\x1b[38;2;0;200;100m'
|
|
10
|
+
const DIFF_RESET = '\x1b[0m'
|
|
11
|
+
|
|
12
|
+
function generateDiff(oldStr: string, newStr: string): string {
|
|
13
|
+
const oldLines = oldStr === '' ? [] : oldStr.split('\n')
|
|
14
|
+
const newLines = newStr.split('\n')
|
|
15
|
+
const lines: string[] = []
|
|
16
|
+
for (const line of oldLines) {
|
|
17
|
+
lines.push(` ${DIFF_RED}- ${line}${DIFF_RESET}`)
|
|
18
|
+
}
|
|
19
|
+
for (const line of newLines) {
|
|
20
|
+
lines.push(` ${DIFF_GREEN}+ ${line}${DIFF_RESET}`)
|
|
21
|
+
}
|
|
22
|
+
return lines.join('\n')
|
|
23
|
+
}
|
|
24
|
+
|
|
7
25
|
interface SingleEdit {
|
|
8
26
|
file_path: string
|
|
9
27
|
old_string: string
|
|
@@ -155,6 +173,9 @@ EXAMPLES:
|
|
|
155
173
|
|
|
156
174
|
for (let i = group.edits.length - 1; i >= 0; i--) {
|
|
157
175
|
const e = group.edits[i]
|
|
176
|
+
// Normalize CRLF → LF in AI-provided strings (critical on Windows)
|
|
177
|
+
e.old_string = e.old_string.replace(/\r\n/g, '\n')
|
|
178
|
+
e.new_string = e.new_string.replace(/\r\n/g, '\n')
|
|
158
179
|
|
|
159
180
|
if (e.old_string === '') {
|
|
160
181
|
if (content !== null) {
|
|
@@ -170,8 +191,11 @@ EXAMPLES:
|
|
|
170
191
|
}
|
|
171
192
|
break
|
|
172
193
|
}
|
|
194
|
+
const newLines = e.new_string.split('\n').length
|
|
173
195
|
content = e.new_string
|
|
174
|
-
results.push(
|
|
196
|
+
results.push(
|
|
197
|
+
` Created ${relPath} (${newLines} lines):\n${generateDiff('', e.new_string)}`,
|
|
198
|
+
)
|
|
175
199
|
continue
|
|
176
200
|
}
|
|
177
201
|
|
|
@@ -218,6 +242,8 @@ EXAMPLES:
|
|
|
218
242
|
break
|
|
219
243
|
}
|
|
220
244
|
|
|
245
|
+
const diff = generateDiff(e.old_string, e.new_string)
|
|
246
|
+
results.push(` Edited ${relPath}:\n${diff}`)
|
|
221
247
|
content = content.slice(0, idx) + e.new_string + content.slice(idx + e.old_string.length)
|
|
222
248
|
}
|
|
223
249
|
|
package/src/tui/components.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from 'node:fs'
|
|
2
2
|
import * as path from 'node:path'
|
|
3
|
-
import type { Component, SelectItem, SelectListTheme } from '
|
|
4
|
-
import { SelectList } from '
|
|
3
|
+
import type { Component, SelectItem, SelectListTheme } from '../pi-tui/index.js'
|
|
4
|
+
import { SelectList } from '../pi-tui/index.js'
|
|
5
5
|
import { PLAN_DIR } from '../tools/write_plan.js'
|
|
6
6
|
import { visibleLen } from './utils.js'
|
|
7
7
|
|
package/src/tui/index.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import * as fs from 'node:fs'
|
|
2
2
|
import * as path from 'node:path'
|
|
3
|
+
import { resetGlobalEventBus } from '../agent/event-bus.js'
|
|
4
|
+
import { ensurePromptsDir, loadPromptTemplates } from '../agent/prompt-templates.js'
|
|
5
|
+
import { formatToolInput, Session, type SessionOutput } from '../agent/session.js'
|
|
6
|
+
import { ensureSkillsDir, loadSkills } from '../agent/skills.js'
|
|
7
|
+
import type { Config } from '../config/index.js'
|
|
8
|
+
import { loadTokenUsage, resetTokenUsage } from '../config/tokens.js'
|
|
3
9
|
import type {
|
|
4
10
|
EditorTheme,
|
|
5
11
|
MarkdownTheme,
|
|
@@ -7,7 +13,7 @@ import type {
|
|
|
7
13
|
SelectItem,
|
|
8
14
|
SelectListTheme,
|
|
9
15
|
SlashCommand,
|
|
10
|
-
} from '
|
|
16
|
+
} from '../pi-tui/index.js'
|
|
11
17
|
import {
|
|
12
18
|
Box,
|
|
13
19
|
CombinedAutocompleteProvider,
|
|
@@ -18,13 +24,7 @@ import {
|
|
|
18
24
|
ProcessTerminal,
|
|
19
25
|
Text,
|
|
20
26
|
TUI,
|
|
21
|
-
} from '
|
|
22
|
-
import { resetGlobalEventBus } from '../agent/event-bus.js'
|
|
23
|
-
import { ensurePromptsDir, loadPromptTemplates } from '../agent/prompt-templates.js'
|
|
24
|
-
import { formatToolInput, Session, type SessionOutput } from '../agent/session.js'
|
|
25
|
-
import { ensureSkillsDir, loadSkills } from '../agent/skills.js'
|
|
26
|
-
import type { Config } from '../config/index.js'
|
|
27
|
-
import { loadTokenUsage, resetTokenUsage } from '../config/tokens.js'
|
|
27
|
+
} from '../pi-tui/index.js'
|
|
28
28
|
import { fmtErr } from '../tools/errors.js'
|
|
29
29
|
import { fetchDeepSeekBalance, isDeepSeekOfficial } from './balance.js'
|
|
30
30
|
import {
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
TodoPanel,
|
|
39
39
|
} from './components.js'
|
|
40
40
|
import { highlightLine } from './highlight.js'
|
|
41
|
+
|
|
41
42
|
export async function startTui(config: Config): Promise<void> {
|
|
42
43
|
let chatContent = ''
|
|
43
44
|
let isRunning = false
|
|
@@ -293,7 +294,7 @@ export async function startTui(config: Config): Promise<void> {
|
|
|
293
294
|
anchor: 'bottom-left',
|
|
294
295
|
width: terminal.columns ?? process.stdout.columns ?? 120,
|
|
295
296
|
offsetY: -1,
|
|
296
|
-
maxHeight:
|
|
297
|
+
maxHeight: 12,
|
|
297
298
|
nonCapturing: false,
|
|
298
299
|
})
|
|
299
300
|
showTodoPanel()
|
|
@@ -482,6 +483,15 @@ export async function startTui(config: Config): Promise<void> {
|
|
|
482
483
|
}
|
|
483
484
|
|
|
484
485
|
if (cmd === 'new') {
|
|
486
|
+
// If the agent is running, stop the old session gracefully first.
|
|
487
|
+
// Without this, the pending chat() promise would continue consuming
|
|
488
|
+
// tokens and write stale output into the freshly cleared chat display.
|
|
489
|
+
if (isRunning) {
|
|
490
|
+
session.stop()
|
|
491
|
+
isRunning = false
|
|
492
|
+
loader.setMessage('')
|
|
493
|
+
tui.setShowHardwareCursor(true)
|
|
494
|
+
}
|
|
485
495
|
Session.clearSavedSession(config.cwd)
|
|
486
496
|
resetTokenUsage(config.cwd)
|
|
487
497
|
resetGlobalEventBus()
|
|
@@ -490,6 +500,10 @@ export async function startTui(config: Config): Promise<void> {
|
|
|
490
500
|
chatContent = ''
|
|
491
501
|
chatMarkdown.setText('')
|
|
492
502
|
plansList.clearFilter()
|
|
503
|
+
// Reset editor internal state that setText('') doesn't clear
|
|
504
|
+
;(editor as any).undoStack.clear()
|
|
505
|
+
;(editor as any).history = []
|
|
506
|
+
;(editor as any).killRing.ring = []
|
|
493
507
|
updateFooter()
|
|
494
508
|
return
|
|
495
509
|
}
|
|
@@ -652,7 +666,7 @@ export async function startTui(config: Config): Promise<void> {
|
|
|
652
666
|
anchor: 'bottom-left',
|
|
653
667
|
width: terminal.columns ?? process.stdout.columns ?? 120,
|
|
654
668
|
offsetY: -1,
|
|
655
|
-
maxHeight:
|
|
669
|
+
maxHeight: 12,
|
|
656
670
|
nonCapturing: false,
|
|
657
671
|
})
|
|
658
672
|
|
package/src/web/index.ts
CHANGED
|
@@ -357,8 +357,15 @@ export async function startWebUi(config: Config, port: number): Promise<void> {
|
|
|
357
357
|
sendPlanData()
|
|
358
358
|
})()
|
|
359
359
|
|
|
360
|
-
// Send full session history (exclude system prompt)
|
|
361
|
-
const historyMessages = sessionWithOutput.messages
|
|
360
|
+
// Send full session history (exclude system prompt, strip ANSI from tool results)
|
|
361
|
+
const historyMessages = sessionWithOutput.messages
|
|
362
|
+
.filter(m => m.role !== 'system')
|
|
363
|
+
.map(m => {
|
|
364
|
+
if (m.role === 'tool' && typeof m.content === 'string') {
|
|
365
|
+
return { ...m, content: stripAnsi(m.content) }
|
|
366
|
+
}
|
|
367
|
+
return m
|
|
368
|
+
})
|
|
362
369
|
if (historyMessages.length > 0) {
|
|
363
370
|
ws.send(
|
|
364
371
|
JSON.stringify({
|
package/src/web/public/app.js
CHANGED
|
@@ -257,8 +257,21 @@
|
|
|
257
257
|
const div = document.createElement('div')
|
|
258
258
|
div.className = id ? 'tool-call executing' : 'tool-call'
|
|
259
259
|
div.dataset.toolId = id || ''
|
|
260
|
-
|
|
261
|
-
|
|
260
|
+
|
|
261
|
+
if (name === 'edit') {
|
|
262
|
+
// Special display for edit tool: show file paths being edited
|
|
263
|
+
const edits = input && input.edits
|
|
264
|
+
const paths = Array.isArray(edits) ? edits.map(e => e.file_path).filter(Boolean) : []
|
|
265
|
+
const uniquePaths = [...new Set(paths)]
|
|
266
|
+
const pathsStr =
|
|
267
|
+
uniquePaths.length > 0 ? uniquePaths.map(p => escapeHtml(p)).join(', ') : '(no files)'
|
|
268
|
+
div.innerHTML = `<span class="tool-name tool-name-edit">✏ edit</span> <span class="tool-input tool-input-edit">${pathsStr}</span>`
|
|
269
|
+
} else {
|
|
270
|
+
const inputStr =
|
|
271
|
+
typeof input === 'object' ? JSON.stringify(input).slice(0, 120) : String(input)
|
|
272
|
+
div.innerHTML = `<span class="tool-name">${escapeHtml(name)}</span> <span class="tool-input">${escapeHtml(inputStr)}</span>`
|
|
273
|
+
}
|
|
274
|
+
|
|
262
275
|
if (container && container.matches('.message')) {
|
|
263
276
|
container.appendChild(div)
|
|
264
277
|
} else {
|
|
@@ -268,6 +281,21 @@
|
|
|
268
281
|
scrollToBottom()
|
|
269
282
|
}
|
|
270
283
|
|
|
284
|
+
function renderDiffContent(text) {
|
|
285
|
+
const lines = text.split('\n')
|
|
286
|
+
return lines
|
|
287
|
+
.map(line => {
|
|
288
|
+
const trimmed = line.trim()
|
|
289
|
+
if (trimmed.startsWith('+ ')) {
|
|
290
|
+
return `<span class="diff-added">${escapeHtml(line)}\n</span>`
|
|
291
|
+
} else if (trimmed.startsWith('- ')) {
|
|
292
|
+
return `<span class="diff-removed">${escapeHtml(line)}\n</span>`
|
|
293
|
+
}
|
|
294
|
+
return escapeHtml(line) + '\n'
|
|
295
|
+
})
|
|
296
|
+
.join('')
|
|
297
|
+
}
|
|
298
|
+
|
|
271
299
|
function addToolResult(name, success, outputOrError, id) {
|
|
272
300
|
// Remove from pending tracking and clear executing state
|
|
273
301
|
if (id) {
|
|
@@ -275,6 +303,10 @@
|
|
|
275
303
|
if (pendingEl) {
|
|
276
304
|
pendingEl.classList.remove('executing')
|
|
277
305
|
pendingToolCalls.delete(id)
|
|
306
|
+
// For edit tool, update the tool-call element to show completion summary
|
|
307
|
+
if (name === 'edit' && success) {
|
|
308
|
+
pendingEl.classList.add('completed')
|
|
309
|
+
}
|
|
278
310
|
}
|
|
279
311
|
}
|
|
280
312
|
const container = streamingMsgEl || messagesEl.querySelector('.message:last-child')
|
|
@@ -283,8 +315,37 @@
|
|
|
283
315
|
if (success) {
|
|
284
316
|
let display = outputOrError
|
|
285
317
|
if (display === '(no output)') display = ''
|
|
286
|
-
|
|
287
|
-
|
|
318
|
+
// Special display for edit tool success — show rich diff content
|
|
319
|
+
if (name === 'edit' && display) {
|
|
320
|
+
const lines = display.split('\n')
|
|
321
|
+
const summaryLines = []
|
|
322
|
+
const diffLines = []
|
|
323
|
+
for (const line of lines) {
|
|
324
|
+
const trimmed = line.trim()
|
|
325
|
+
if (
|
|
326
|
+
trimmed.startsWith('Edited ') ||
|
|
327
|
+
trimmed.startsWith('Created ') ||
|
|
328
|
+
trimmed.startsWith('Deleted ')
|
|
329
|
+
) {
|
|
330
|
+
summaryLines.push(trimmed)
|
|
331
|
+
} else if (trimmed.startsWith('- ') || trimmed.startsWith('+ ')) {
|
|
332
|
+
diffLines.push(line)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
let html = `<span class="tool-result-success">✔ ${escapeHtml(name)}</span>`
|
|
336
|
+
if (summaryLines.length > 0) {
|
|
337
|
+
html += `<span class="tool-result-summary">${summaryLines.map(s => escapeHtml(s)).join('<br>')}</span>`
|
|
338
|
+
}
|
|
339
|
+
if (diffLines.length > 0) {
|
|
340
|
+
html += `<span class="tool-result-diff"><pre><code>${renderDiffContent(diffLines.join('\n'))}</code></pre></span>`
|
|
341
|
+
} else if (display.includes(' FAIL ')) {
|
|
342
|
+
html += `<span class="tool-result-diff"><pre>${escapeHtml(display)}</pre></span>`
|
|
343
|
+
}
|
|
344
|
+
div.innerHTML = html
|
|
345
|
+
} else {
|
|
346
|
+
const summary = typeof display === 'string' ? display.slice(0, 80) : ''
|
|
347
|
+
div.innerHTML = `<span class="tool-result-success">✔ ${escapeHtml(name)}</span>${summary ? ' ' + escapeHtml(summary) : ''}`
|
|
348
|
+
}
|
|
288
349
|
} else {
|
|
289
350
|
div.innerHTML = `<span class="tool-result-error">✖ ${escapeHtml(name)}</span> ${escapeHtml(outputOrError)}`
|
|
290
351
|
}
|
package/src/web/public/style.css
CHANGED
|
@@ -347,10 +347,59 @@ body {
|
|
|
347
347
|
}
|
|
348
348
|
|
|
349
349
|
.tool-call .tool-name { color: var(--accent-dim); }
|
|
350
|
+
.tool-call .tool-name.tool-name-edit { color: var(--warning); }
|
|
350
351
|
.tool-call .tool-input { color: var(--text-dim); }
|
|
352
|
+
.tool-call .tool-input.tool-input-edit { font-family: var(--font-mono); font-size: 0.85em; }
|
|
353
|
+
.tool-call.completed { opacity: 0.6; }
|
|
354
|
+
.tool-call.completed .tool-name::before { display: none; }
|
|
351
355
|
|
|
352
356
|
.tool-result .tool-result-success { color: var(--success); }
|
|
353
357
|
.tool-result .tool-result-error { color: var(--error); }
|
|
358
|
+
.tool-result .tool-result-summary {
|
|
359
|
+
display: block;
|
|
360
|
+
color: var(--text-secondary);
|
|
361
|
+
font-size: 0.85em;
|
|
362
|
+
margin-top: 2px;
|
|
363
|
+
line-height: 1.3;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/* ── Edit Diff Display ─────────────────────────────────────────── */
|
|
367
|
+
.tool-result .tool-result-diff {
|
|
368
|
+
display: block;
|
|
369
|
+
margin-top: 2px;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.tool-result .tool-result-diff pre {
|
|
373
|
+
max-height: 300px;
|
|
374
|
+
max-width: 100%;
|
|
375
|
+
overflow-y: auto;
|
|
376
|
+
overflow-x: auto;
|
|
377
|
+
background: var(--input-bg);
|
|
378
|
+
border: 1px solid var(--border);
|
|
379
|
+
border-radius: 6px;
|
|
380
|
+
padding: 8px 12px;
|
|
381
|
+
margin: 4px 0;
|
|
382
|
+
font-size: 12px;
|
|
383
|
+
line-height: 1.4;
|
|
384
|
+
white-space: pre;
|
|
385
|
+
word-break: break-all;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.tool-result .tool-result-diff code {
|
|
389
|
+
font-family: var(--font-mono);
|
|
390
|
+
font-size: inherit;
|
|
391
|
+
color: inherit;
|
|
392
|
+
background: none;
|
|
393
|
+
padding: 0;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.tool-result .diff-added {
|
|
397
|
+
color: var(--success);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.tool-result .diff-removed {
|
|
401
|
+
color: var(--error);
|
|
402
|
+
}
|
|
354
403
|
|
|
355
404
|
/* ── Thinking Block ──────────────────────────────────────────── */
|
|
356
405
|
.thinking-block {
|
|
@@ -7,6 +7,11 @@ import type { Config } from '../config/index.js'
|
|
|
7
7
|
* Listens to EventBus events and forwards them as JSON messages.
|
|
8
8
|
*/
|
|
9
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
|
+
|
|
10
15
|
export interface WsMessage {
|
|
11
16
|
type: string
|
|
12
17
|
[key: string]: unknown
|
|
@@ -49,12 +54,18 @@ export function startSessionBridge(
|
|
|
49
54
|
|
|
50
55
|
const unsubToolResult = bus.on(EventChannels.TOOL_RESULT, data => {
|
|
51
56
|
const d = data as { name: string; id: string; output: string }
|
|
52
|
-
send({
|
|
57
|
+
send({
|
|
58
|
+
type: 'tool_result',
|
|
59
|
+
name: d.name,
|
|
60
|
+
id: d.id,
|
|
61
|
+
success: true,
|
|
62
|
+
output: stripAnsi(d.output),
|
|
63
|
+
})
|
|
53
64
|
})
|
|
54
65
|
|
|
55
66
|
const unsubToolError = bus.on(EventChannels.TOOL_ERROR, data => {
|
|
56
67
|
const d = data as { name: string; id: string; error: string }
|
|
57
|
-
send({ type: 'tool_result', name: d.name, id: d.id, success: false, error: d.error })
|
|
68
|
+
send({ type: 'tool_result', name: d.name, id: d.id, success: false, error: stripAnsi(d.error) })
|
|
58
69
|
})
|
|
59
70
|
|
|
60
71
|
const unsubTurnEnd = bus.on(EventChannels.TURN_END, data => {
|