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.
Files changed (216) hide show
  1. package/.kilo/plans/1780064105789-mighty-pixel.md +171 -0
  2. package/.lonny/fix-new-command-session-cleanup.md +65 -0
  3. package/.lonny/tui-autocomplete-above.md +62 -0
  4. package/dist/agent/__tests__/compaction.test.js +6 -4
  5. package/dist/agent/__tests__/compaction.test.js.map +1 -1
  6. package/dist/agent/llm.d.ts +1 -1
  7. package/dist/agent/llm.d.ts.map +1 -1
  8. package/dist/agent/prompt-builder.d.ts.map +1 -1
  9. package/dist/agent/prompt-builder.js +26 -8
  10. package/dist/agent/prompt-builder.js.map +1 -1
  11. package/dist/agent/providers/anthropic.d.ts +1 -1
  12. package/dist/agent/providers/anthropic.d.ts.map +1 -1
  13. package/dist/agent/providers/anthropic.js +2 -2
  14. package/dist/agent/providers/anthropic.js.map +1 -1
  15. package/dist/agent/providers/google.d.ts +1 -1
  16. package/dist/agent/providers/google.d.ts.map +1 -1
  17. package/dist/agent/providers/google.js +2 -1
  18. package/dist/agent/providers/google.js.map +1 -1
  19. package/dist/agent/providers/ollama.d.ts +1 -1
  20. package/dist/agent/providers/ollama.d.ts.map +1 -1
  21. package/dist/agent/providers/ollama.js +2 -1
  22. package/dist/agent/providers/ollama.js.map +1 -1
  23. package/dist/agent/providers/openai.d.ts +1 -1
  24. package/dist/agent/providers/openai.d.ts.map +1 -1
  25. package/dist/agent/providers/openai.js +2 -2
  26. package/dist/agent/providers/openai.js.map +1 -1
  27. package/dist/agent/session.d.ts +1 -0
  28. package/dist/agent/session.d.ts.map +1 -1
  29. package/dist/agent/session.js +8 -2
  30. package/dist/agent/session.js.map +1 -1
  31. package/dist/pi-tui/autocomplete.d.ts +54 -0
  32. package/dist/pi-tui/autocomplete.d.ts.map +1 -0
  33. package/dist/pi-tui/autocomplete.js +636 -0
  34. package/dist/pi-tui/autocomplete.js.map +1 -0
  35. package/dist/pi-tui/components/box.d.ts +22 -0
  36. package/dist/pi-tui/components/box.d.ts.map +1 -0
  37. package/dist/pi-tui/components/box.js +104 -0
  38. package/dist/pi-tui/components/box.js.map +1 -0
  39. package/dist/pi-tui/components/cancellable-loader.d.ts +22 -0
  40. package/dist/pi-tui/components/cancellable-loader.d.ts.map +1 -0
  41. package/dist/pi-tui/components/cancellable-loader.js +35 -0
  42. package/dist/pi-tui/components/cancellable-loader.js.map +1 -0
  43. package/dist/pi-tui/components/editor.d.ts +249 -0
  44. package/dist/pi-tui/components/editor.d.ts.map +1 -0
  45. package/dist/pi-tui/components/editor.js +1883 -0
  46. package/dist/pi-tui/components/editor.js.map +1 -0
  47. package/dist/pi-tui/components/image.d.ts +28 -0
  48. package/dist/pi-tui/components/image.d.ts.map +1 -0
  49. package/dist/pi-tui/components/image.js +90 -0
  50. package/dist/pi-tui/components/image.js.map +1 -0
  51. package/dist/pi-tui/components/input.d.ts +37 -0
  52. package/dist/pi-tui/components/input.d.ts.map +1 -0
  53. package/dist/pi-tui/components/input.js +382 -0
  54. package/dist/pi-tui/components/input.js.map +1 -0
  55. package/dist/pi-tui/components/loader.d.ts +31 -0
  56. package/dist/pi-tui/components/loader.d.ts.map +1 -0
  57. package/dist/pi-tui/components/loader.js +70 -0
  58. package/dist/pi-tui/components/loader.js.map +1 -0
  59. package/dist/pi-tui/components/markdown.d.ts +96 -0
  60. package/dist/pi-tui/components/markdown.d.ts.map +1 -0
  61. package/dist/pi-tui/components/markdown.js +647 -0
  62. package/dist/pi-tui/components/markdown.js.map +1 -0
  63. package/dist/pi-tui/components/select-list.d.ts +50 -0
  64. package/dist/pi-tui/components/select-list.d.ts.map +1 -0
  65. package/dist/pi-tui/components/select-list.js +167 -0
  66. package/dist/pi-tui/components/select-list.js.map +1 -0
  67. package/dist/pi-tui/components/settings-list.d.ts +50 -0
  68. package/dist/pi-tui/components/settings-list.d.ts.map +1 -0
  69. package/dist/pi-tui/components/settings-list.js +189 -0
  70. package/dist/pi-tui/components/settings-list.js.map +1 -0
  71. package/dist/pi-tui/components/spacer.d.ts +12 -0
  72. package/dist/pi-tui/components/spacer.d.ts.map +1 -0
  73. package/dist/pi-tui/components/spacer.js +23 -0
  74. package/dist/pi-tui/components/spacer.js.map +1 -0
  75. package/dist/pi-tui/components/text.d.ts +19 -0
  76. package/dist/pi-tui/components/text.d.ts.map +1 -0
  77. package/dist/pi-tui/components/text.js +91 -0
  78. package/dist/pi-tui/components/text.js.map +1 -0
  79. package/dist/pi-tui/components/truncated-text.d.ts +13 -0
  80. package/dist/pi-tui/components/truncated-text.d.ts.map +1 -0
  81. package/dist/pi-tui/components/truncated-text.js +51 -0
  82. package/dist/pi-tui/components/truncated-text.js.map +1 -0
  83. package/dist/pi-tui/editor-component.d.ts +39 -0
  84. package/dist/pi-tui/editor-component.d.ts.map +1 -0
  85. package/dist/pi-tui/editor-component.js +2 -0
  86. package/dist/pi-tui/editor-component.js.map +1 -0
  87. package/dist/pi-tui/fuzzy.d.ts +16 -0
  88. package/dist/pi-tui/fuzzy.d.ts.map +1 -0
  89. package/dist/pi-tui/fuzzy.js +110 -0
  90. package/dist/pi-tui/fuzzy.js.map +1 -0
  91. package/dist/pi-tui/index.d.ts +23 -0
  92. package/dist/pi-tui/index.d.ts.map +1 -0
  93. package/dist/pi-tui/index.js +32 -0
  94. package/dist/pi-tui/index.js.map +1 -0
  95. package/dist/pi-tui/keybindings.d.ts +193 -0
  96. package/dist/pi-tui/keybindings.d.ts.map +1 -0
  97. package/dist/pi-tui/keybindings.js +174 -0
  98. package/dist/pi-tui/keybindings.js.map +1 -0
  99. package/dist/pi-tui/keys.d.ts +184 -0
  100. package/dist/pi-tui/keys.d.ts.map +1 -0
  101. package/dist/pi-tui/keys.js +1182 -0
  102. package/dist/pi-tui/keys.js.map +1 -0
  103. package/dist/pi-tui/kill-ring.d.ts +28 -0
  104. package/dist/pi-tui/kill-ring.d.ts.map +1 -0
  105. package/dist/pi-tui/kill-ring.js +44 -0
  106. package/dist/pi-tui/kill-ring.js.map +1 -0
  107. package/dist/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node +0 -0
  108. package/dist/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
  109. package/dist/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  110. package/dist/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  111. package/dist/pi-tui/native-modifiers.d.ts +3 -0
  112. package/dist/pi-tui/native-modifiers.d.ts.map +1 -0
  113. package/dist/pi-tui/native-modifiers.js +53 -0
  114. package/dist/pi-tui/native-modifiers.js.map +1 -0
  115. package/dist/pi-tui/stdin-buffer.d.ts +50 -0
  116. package/dist/pi-tui/stdin-buffer.d.ts.map +1 -0
  117. package/dist/pi-tui/stdin-buffer.js +361 -0
  118. package/dist/pi-tui/stdin-buffer.js.map +1 -0
  119. package/dist/pi-tui/terminal-image.d.ts +90 -0
  120. package/dist/pi-tui/terminal-image.d.ts.map +1 -0
  121. package/dist/pi-tui/terminal-image.js +343 -0
  122. package/dist/pi-tui/terminal-image.js.map +1 -0
  123. package/dist/pi-tui/terminal.d.ts +113 -0
  124. package/dist/pi-tui/terminal.d.ts.map +1 -0
  125. package/dist/pi-tui/terminal.js +478 -0
  126. package/dist/pi-tui/terminal.js.map +1 -0
  127. package/dist/pi-tui/tui.d.ts +227 -0
  128. package/dist/pi-tui/tui.d.ts.map +1 -0
  129. package/dist/pi-tui/tui.js +1091 -0
  130. package/dist/pi-tui/tui.js.map +1 -0
  131. package/dist/pi-tui/undo-stack.d.ts +17 -0
  132. package/dist/pi-tui/undo-stack.d.ts.map +1 -0
  133. package/dist/pi-tui/undo-stack.js +25 -0
  134. package/dist/pi-tui/undo-stack.js.map +1 -0
  135. package/dist/pi-tui/utils.d.ts +84 -0
  136. package/dist/pi-tui/utils.d.ts.map +1 -0
  137. package/dist/pi-tui/utils.js +1024 -0
  138. package/dist/pi-tui/utils.js.map +1 -0
  139. package/dist/pi-tui/word-navigation.d.ts +25 -0
  140. package/dist/pi-tui/word-navigation.d.ts.map +1 -0
  141. package/dist/pi-tui/word-navigation.js +98 -0
  142. package/dist/pi-tui/word-navigation.js.map +1 -0
  143. package/dist/tools/__tests__/edit.test.js +85 -0
  144. package/dist/tools/__tests__/edit.test.js.map +1 -1
  145. package/dist/tools/edit.d.ts.map +1 -1
  146. package/dist/tools/edit.js +23 -1
  147. package/dist/tools/edit.js.map +1 -1
  148. package/dist/tui/components.d.ts +1 -1
  149. package/dist/tui/components.d.ts.map +1 -1
  150. package/dist/tui/components.js +1 -1
  151. package/dist/tui/components.js.map +1 -1
  152. package/dist/tui/editor-patch.d.ts +13 -0
  153. package/dist/tui/editor-patch.d.ts.map +1 -0
  154. package/dist/tui/editor-patch.js +41 -0
  155. package/dist/tui/editor-patch.js.map +1 -0
  156. package/dist/tui/index.d.ts.map +1 -1
  157. package/dist/tui/index.js +15 -3
  158. package/dist/tui/index.js.map +1 -1
  159. package/dist/web/index.d.ts.map +1 -1
  160. package/dist/web/index.js +9 -2
  161. package/dist/web/index.js.map +1 -1
  162. package/dist/web/public/app.js +65 -4
  163. package/dist/web/public/style.css +49 -0
  164. package/dist/web/session-bridge.d.ts +0 -4
  165. package/dist/web/session-bridge.d.ts.map +1 -1
  166. package/dist/web/session-bridge.js +16 -2
  167. package/dist/web/session-bridge.js.map +1 -1
  168. package/package.json +5 -3
  169. package/src/agent/__tests__/compaction.test.ts +6 -4
  170. package/src/agent/llm.ts +5 -1
  171. package/src/agent/prompt-builder.ts +27 -8
  172. package/src/agent/providers/anthropic.ts +15 -8
  173. package/src/agent/providers/google.ts +6 -1
  174. package/src/agent/providers/ollama.ts +6 -1
  175. package/src/agent/providers/openai.ts +21 -12
  176. package/src/agent/session.ts +12 -2
  177. package/src/pi-tui/autocomplete.ts +808 -0
  178. package/src/pi-tui/components/box.ts +137 -0
  179. package/src/pi-tui/components/cancellable-loader.ts +40 -0
  180. package/src/pi-tui/components/editor.ts +2321 -0
  181. package/src/pi-tui/components/image.ts +130 -0
  182. package/src/pi-tui/components/input.ts +456 -0
  183. package/src/pi-tui/components/loader.ts +93 -0
  184. package/src/pi-tui/components/markdown.ts +839 -0
  185. package/src/pi-tui/components/select-list.ts +263 -0
  186. package/src/pi-tui/components/settings-list.ts +264 -0
  187. package/src/pi-tui/components/spacer.ts +28 -0
  188. package/src/pi-tui/components/text.ts +113 -0
  189. package/src/pi-tui/components/truncated-text.ts +65 -0
  190. package/src/pi-tui/editor-component.ts +74 -0
  191. package/src/pi-tui/fuzzy.ts +137 -0
  192. package/src/pi-tui/index.ts +115 -0
  193. package/src/pi-tui/keybindings.ts +245 -0
  194. package/src/pi-tui/keys.ts +1440 -0
  195. package/src/pi-tui/kill-ring.ts +46 -0
  196. package/src/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node +0 -0
  197. package/src/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
  198. package/src/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  199. package/src/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  200. package/src/pi-tui/native-modifiers.ts +65 -0
  201. package/src/pi-tui/stdin-buffer.ts +434 -0
  202. package/src/pi-tui/terminal-image.ts +469 -0
  203. package/src/pi-tui/terminal.ts +594 -0
  204. package/src/pi-tui/tui.ts +1376 -0
  205. package/src/pi-tui/undo-stack.ts +28 -0
  206. package/src/pi-tui/utils.ts +1176 -0
  207. package/src/pi-tui/word-navigation.ts +127 -0
  208. package/src/tools/__tests__/edit.test.ts +91 -0
  209. package/src/tools/edit.ts +27 -1
  210. package/src/tui/components.ts +2 -2
  211. package/src/tui/index.ts +24 -10
  212. package/src/web/index.ts +9 -2
  213. package/src/web/public/app.js +65 -4
  214. package/src/web/public/style.css +49 -0
  215. package/src/web/session-bridge.ts +13 -2
  216. package/tsconfig.json +2 -2
@@ -26,7 +26,19 @@ export function buildSystemPrompt(config: Config): string {
26
26
  - \`search\`: Search the web using Tavily (query: string, search_depth?: string, include_answer?: boolean, max_results?: number, topic?: string, days?: number)
27
27
  `
28
28
  }
29
- const planOnly = mode === 'plan'
29
+ if (mode === 'plan') {
30
+ return `Available tools (read-only investigation + write_plan):
31
+ - \`read\`: Read file contents (paths: string[])
32
+ - \`glob\`: Find files by glob pattern (pattern: string)
33
+ - \`grep\`: Search file content by regex (pattern: string, include?: string, path?: string)
34
+ - \`ls\`: List directory (path?: string)
35
+ - \`bash\`: Execute read-only shell commands for investigation (NEVER modify files)
36
+ - \`find\`: Find files by name pattern (pattern: string, path?: string, maxResults?: number)
37
+ - \`git\`: Run read-only git commands (command: string)
38
+ - \`search\`: Search the web using Tavily (query: string, search_depth?: string, include_answer?: boolean, max_results?: number, topic?: string, days?: number)
39
+ - \`write_plan\`: Save plan/todo markdown into .lonny/ folder
40
+ `
41
+ }
30
42
  return `Available tools:
31
43
  - \`read\`: Read file contents (paths: string[])
32
44
  - \`glob\`: Find files by glob pattern (pattern: string)
@@ -34,7 +46,7 @@ export function buildSystemPrompt(config: Config): string {
34
46
  - \`ls\`: List directory (path?: string)
35
47
  - \`bash\`: Execute a shell command
36
48
  - \`edit\`: Replace text in files — call with {"edits": [{"file_path", "old_string", "new_string"}]} (array required)
37
- ${planOnly ? '- `write_plan`: Save plan/todo markdown into .lonny/ folder\n' : ''}- \`install_skill\`: Install an npm package as a skill — fetches package info from npm, runs npm install, and creates a .lonny/skills/ file with usage instructions for the AI
49
+ - \`install_skill\`: Install an npm package as a skill — fetches package info from npm, runs npm install, and creates a .lonny/skills/ file with usage instructions for the AI
38
50
  - \`find\`: Find files by name pattern (pattern: string, path?: string, maxResults?: number)
39
51
  - \`git\`: Run read-only git commands (command: string)
40
52
  - \`search\`: Search the web using Tavily (query: string, search_depth?: string, include_answer?: boolean, max_results?: number, topic?: string, days?: number)
@@ -59,10 +71,12 @@ ${getToolListForMode(config.mode)}`
59
71
 
60
72
  RULES (plan-specific):
61
73
  1. Read first: Use read/grep/glob tools to gather all context you need before planning.
62
- 2. You CANNOT edit source files you have no code edit tools. Only read and analyze.
63
- 3. Use \`bash\` for read-only commands only.
64
- 4. You MUST persist the final plan AND todo list to a file in \`.lonny/\` using \`write_plan\`. The \`write_plan\` content MUST include both ## Plan and ## Todo List sections.
65
- 5. You MUST also include the todo list in your text response to the user (not just in the file).
74
+ 2. You NEVER edit source files. You ONLY use read-only tools (read/glob/grep/ls/find/git/search) and bash (read-only commands only). You do NOT have access to edit, install_skill, or exec.
75
+ 3. Use \`bash\` for investigation only NEVER to modify files, install packages, or run write operations.
76
+ 4. Your ONLY output is a plan file saved via \`write_plan\`. You CANNOT modify the codebase directly.
77
+ 5. You MUST persist the final plan AND todo list to a file in \`.lonny/\` using \`write_plan\`. The \`write_plan\` content MUST include both ## Plan and ## Todo List sections.
78
+ 6. You MUST also include the todo list in your text response to the user (not just in the file).
79
+ 7. If the user asks you to modify files, run write commands, or install packages — refuse and explain they need to switch to code mode (\`/mode code\`).
66
80
 
67
81
  OUTPUT FORMAT (you MUST include both in write_plan AND in your response text):
68
82
 
@@ -113,8 +127,13 @@ RULES (code-specific):
113
127
  ${isWindows ? ' - Use PowerShell. Do NOT use Unix commands like `cat`, `ls`, `grep`, `which`, `chmod`, `mv`, `cp`, `rm`, `touch`, `mkdir`, `uname`, etc.' : ''}
114
128
  ${isWindows ? ' - Use `type` instead of `cat`, `dir` instead of `ls`, `where` instead of `which`' : ''}`
115
129
 
130
+ // Plan mode uses its own standalone tool list inside modeInstructions — skip sharedRules
131
+ const rulesSection =
132
+ config.mode === 'plan'
133
+ ? ''
134
+ : `
135
+ ${sharedRules}`
116
136
  return `${modeInstructions}
117
137
 
118
- ${envSection}
119
- ${sharedRules}${skillsSection}`
138
+ ${envSection}${rulesSection}${skillsSection}`
120
139
  }
@@ -12,7 +12,11 @@ export class AnthropicProvider implements LLMProvider {
12
12
  this.model = model || 'claude-sonnet-4-20250514'
13
13
  }
14
14
 
15
- async *chat(messages: LLMMessage[], tools: ToolDefinition[]): AsyncGenerator<LLMChunk> {
15
+ async *chat(
16
+ messages: LLMMessage[],
17
+ tools: ToolDefinition[],
18
+ signal?: AbortSignal,
19
+ ): AsyncGenerator<LLMChunk> {
16
20
  const anthropicTools: Anthropic.Tool[] = tools.map(t => {
17
21
  const properties: Record<string, unknown> = {}
18
22
  for (const [key, param] of Object.entries(t.parameters)) {
@@ -71,13 +75,16 @@ export class AnthropicProvider implements LLMProvider {
71
75
  return { role: 'user' as const, content: '' }
72
76
  })
73
77
 
74
- const stream = this.client.messages.stream({
75
- model: this.model,
76
- system: systemMsg?.content || '',
77
- messages: anthropicMessages,
78
- tools: anthropicTools.length > 0 ? anthropicTools : undefined,
79
- max_tokens: 8192,
80
- })
78
+ const stream = this.client.messages.stream(
79
+ {
80
+ model: this.model,
81
+ system: systemMsg?.content || '',
82
+ messages: anthropicMessages,
83
+ tools: anthropicTools.length > 0 ? anthropicTools : undefined,
84
+ max_tokens: 8192,
85
+ },
86
+ signal ? { signal } : undefined,
87
+ )
81
88
 
82
89
  let currentToolUse: {
83
90
  id: string
@@ -48,7 +48,11 @@ export class GoogleProvider implements LLMProvider {
48
48
  this.baseUrl = baseURL || 'https://generativelanguage.googleapis.com/v1beta'
49
49
  }
50
50
 
51
- async *chat(messages: LLMMessage[], tools: ToolDefinition[]): AsyncGenerator<LLMChunk> {
51
+ async *chat(
52
+ messages: LLMMessage[],
53
+ tools: ToolDefinition[],
54
+ signal?: AbortSignal,
55
+ ): AsyncGenerator<LLMChunk> {
52
56
  const systemInstruction = messages.find(m => m.role === 'system')?.content || ''
53
57
  const nonSystemMessages = messages.filter(m => m.role !== 'system')
54
58
 
@@ -135,6 +139,7 @@ export class GoogleProvider implements LLMProvider {
135
139
  'Content-Type': 'application/json',
136
140
  'x-goog-api-key': this.apiKey,
137
141
  },
142
+ signal,
138
143
  body: JSON.stringify(body),
139
144
  })
140
145
 
@@ -40,7 +40,11 @@ export class OllamaProvider implements LLMProvider {
40
40
  this.model = model || 'llama3.2'
41
41
  }
42
42
 
43
- async *chat(messages: LLMMessage[], tools: ToolDefinition[]): AsyncGenerator<LLMChunk> {
43
+ async *chat(
44
+ messages: LLMMessage[],
45
+ tools: ToolDefinition[],
46
+ signal?: AbortSignal,
47
+ ): AsyncGenerator<LLMChunk> {
44
48
  // Build Ollama-format messages
45
49
  const ollamaMessages: OllamaMessage[] = messages.map(m => {
46
50
  if (m.role === 'system') {
@@ -108,6 +112,7 @@ export class OllamaProvider implements LLMProvider {
108
112
  const response = await fetch(`${this.baseUrl}/api/chat`, {
109
113
  method: 'POST',
110
114
  headers: { 'Content-Type': 'application/json' },
115
+ signal,
111
116
  body: JSON.stringify(body),
112
117
  })
113
118
 
@@ -1,4 +1,5 @@
1
1
  import OpenAI from 'openai'
2
+ import type { RequestOptions } from 'openai/core.js'
2
3
  import type {
3
4
  ChatCompletionChunk,
4
5
  ChatCompletionMessageParam,
@@ -47,7 +48,11 @@ export class OpenAIProvider implements LLMProvider {
47
48
  this.strictTools = strictTools ?? (baseURL ? /beta/i.test(baseURL) : false)
48
49
  }
49
50
 
50
- async *chat(messages: LLMMessage[], tools: ToolDefinition[]): AsyncGenerator<LLMChunk> {
51
+ async *chat(
52
+ messages: LLMMessage[],
53
+ tools: ToolDefinition[],
54
+ signal?: AbortSignal,
55
+ ): AsyncGenerator<LLMChunk> {
51
56
  const openAIFormattedTools: ChatCompletionTool[] = tools.map(t => {
52
57
  const properties: Record<string, unknown> = {}
53
58
  for (const [key, param] of Object.entries(t.parameters)) {
@@ -117,18 +122,22 @@ export class OpenAIProvider implements LLMProvider {
117
122
  const stream: Stream<ChatCompletionChunk> = await (
118
123
  this.client.chat.completions.create as (
119
124
  params: ExtendedCreateParams,
125
+ options?: RequestOptions,
120
126
  ) => Promise<Stream<ChatCompletionChunk>>
121
- )({
122
- model: this.model,
123
- messages: openAIMessages,
124
- tools: openAIFormattedTools.length > 0 ? openAIFormattedTools : undefined,
125
- stream: true,
126
- stream_options: { include_usage: true },
127
- ...(this.thinking
128
- ? { thinking: { type: 'enabled' }, reasoning_effort: this.reasoningEffort || 'high' }
129
- : {}),
130
- ...(this.enableCache ? { enable_cache: true } : {}),
131
- })
127
+ )(
128
+ {
129
+ model: this.model,
130
+ messages: openAIMessages,
131
+ tools: openAIFormattedTools.length > 0 ? openAIFormattedTools : undefined,
132
+ stream: true,
133
+ stream_options: { include_usage: true },
134
+ ...(this.thinking
135
+ ? { thinking: { type: 'enabled' }, reasoning_effort: this.reasoningEffort || 'high' }
136
+ : {}),
137
+ ...(this.enableCache ? { enable_cache: true } : {}),
138
+ },
139
+ signal ? { signal } : undefined,
140
+ )
132
141
 
133
142
  let currentToolCall: {
134
143
  id: string
@@ -155,7 +155,7 @@ function printToolResult(tc: ToolCall, result: ToolResult, output?: SessionOutpu
155
155
  writeOut(` ${GY}│${RS} ${GR}✔${RS} edit\n`, output)
156
156
  if (result.output) {
157
157
  for (const l of result.output.split('\n')) {
158
- if (l.trim()) writeOut(` ${GY}│${RS} ${GY}${l.trim()}${RS}\n`, output)
158
+ if (l.trim()) writeOut(` ${GY}│${RS} ${l.trim()}\n`, output)
159
159
  }
160
160
  }
161
161
  } else if (tc.name === 'write_plan') {
@@ -259,6 +259,7 @@ export class Session {
259
259
  turnApiCalls: number = 0
260
260
  totalApiCalls: number = 0
261
261
  private stopped: boolean = false
262
+ private abortController: AbortController | null = null
262
263
 
263
264
  constructor(config: Config, output?: SessionOutput) {
264
265
  this.config = config
@@ -368,6 +369,8 @@ export class Session {
368
369
  /** Stop the current conversation gracefully */
369
370
  stop(): void {
370
371
  this.stopped = true
372
+ // Abort any in-flight LLM stream to stop token consumption immediately
373
+ this.abortController?.abort()
371
374
  }
372
375
 
373
376
  /** Check if the session was stopped */
@@ -396,6 +399,9 @@ export class Session {
396
399
 
397
400
  // Reset stopped flag for new conversation
398
401
  this.resetStopped()
402
+ // Create a new AbortController for this chat invocation
403
+ // (a new controller is needed each time because abort() is one-shot)
404
+ this.abortController = new AbortController()
399
405
 
400
406
  // Declare toolCalls outside the loop so we can reference it in stop check
401
407
  let toolCalls: ToolCall[] = []
@@ -419,7 +425,11 @@ export class Session {
419
425
  bus.emit(EventChannels.TURN_START, { prompt: userPrompt, iteration: iterations })
420
426
  bus.emit(EventChannels.LLM_STREAM_START, { iteration: iterations })
421
427
 
422
- const stream = this.provider.chat(this.messages, this.registry.getDefinitions())
428
+ const stream = this.provider.chat(
429
+ this.messages,
430
+ this.registry.getDefinitions(),
431
+ this.abortController.signal,
432
+ )
423
433
 
424
434
  for await (const chunk of stream) {
425
435
  if (chunk.reasoning_content) {