lonny-agent 0.1.9 → 0.2.2

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 (68) hide show
  1. package/.kilo/plans/1780293725888-quick-wizard.md +62 -0
  2. package/.lonny/plan-web-cwd-status.md +38 -0
  3. package/README.md +63 -10
  4. package/dist/agent/compaction.d.ts +5 -0
  5. package/dist/agent/compaction.d.ts.map +1 -1
  6. package/dist/agent/compaction.js +5 -0
  7. package/dist/agent/compaction.js.map +1 -1
  8. package/dist/agent/prompt-builder.d.ts.map +1 -1
  9. package/dist/agent/prompt-builder.js +39 -4
  10. package/dist/agent/prompt-builder.js.map +1 -1
  11. package/dist/agent/providers/google.js +1 -1
  12. package/dist/agent/providers/google.js.map +1 -1
  13. package/dist/agent/providers/openai.js +1 -1
  14. package/dist/agent/providers/openai.js.map +1 -1
  15. package/dist/agent/session.d.ts.map +1 -1
  16. package/dist/agent/session.js +18 -2
  17. package/dist/agent/session.js.map +1 -1
  18. package/dist/config/__tests__/context-window.test.d.ts +2 -0
  19. package/dist/config/__tests__/context-window.test.d.ts.map +1 -0
  20. package/dist/config/__tests__/context-window.test.js +80 -0
  21. package/dist/config/__tests__/context-window.test.js.map +1 -0
  22. package/dist/config/index.d.ts +7 -0
  23. package/dist/config/index.d.ts.map +1 -1
  24. package/dist/config/index.js +211 -0
  25. package/dist/config/index.js.map +1 -1
  26. package/dist/pi-tui/tui.d.ts.map +1 -1
  27. package/dist/pi-tui/tui.js +7 -16
  28. package/dist/pi-tui/tui.js.map +1 -1
  29. package/dist/tools/__tests__/edit.test.js +254 -0
  30. package/dist/tools/__tests__/edit.test.js.map +1 -1
  31. package/dist/tools/__tests__/fetch.test.js +11 -9
  32. package/dist/tools/__tests__/fetch.test.js.map +1 -1
  33. package/dist/tools/edit.d.ts.map +1 -1
  34. package/dist/tools/edit.js +205 -41
  35. package/dist/tools/edit.js.map +1 -1
  36. package/dist/tools/install_superpowers.d.ts +3 -0
  37. package/dist/tools/install_superpowers.d.ts.map +1 -0
  38. package/dist/tools/install_superpowers.js +207 -0
  39. package/dist/tools/install_superpowers.js.map +1 -0
  40. package/dist/tui/index.d.ts.map +1 -1
  41. package/dist/tui/index.js +25 -1
  42. package/dist/tui/index.js.map +1 -1
  43. package/dist/web/index.d.ts.map +1 -1
  44. package/dist/web/index.js +65 -0
  45. package/dist/web/index.js.map +1 -1
  46. package/dist/web/public/app.js +9 -0
  47. package/dist/web/public/index.html +6 -0
  48. package/dist/web/public/style.css +11 -0
  49. package/package.json +1 -1
  50. package/src/agent/compaction.ts +6 -0
  51. package/src/agent/prompt-builder.ts +40 -4
  52. package/src/agent/providers/google.ts +1 -1
  53. package/src/agent/providers/openai.ts +1 -1
  54. package/src/agent/session.ts +19 -2
  55. package/src/config/__tests__/context-window.test.ts +94 -0
  56. package/src/config/index.ts +243 -0
  57. package/src/pi-tui/tui.ts +8 -14
  58. package/src/tools/__tests__/edit.test.ts +283 -0
  59. package/src/tools/__tests__/fetch.test.ts +12 -10
  60. package/src/tools/edit.ts +225 -42
  61. package/src/tools/registry.ts +265 -265
  62. package/src/tui/index.ts +23 -1
  63. package/src/web/index.ts +71 -0
  64. package/src/web/public/app.js +9 -0
  65. package/src/web/public/index.html +6 -0
  66. package/src/web/public/style.css +11 -0
  67. package/.lonny/fix-new-command-session-cleanup.md +0 -65
  68. package/.lonny/tui-autocomplete-above.md +0 -62
@@ -1,265 +1,265 @@
1
- import type { FileReadTracker } from '../diff/apply.js'
2
- import { bashTool } from './bash.js'
3
- import { createEditTool } from './edit.js'
4
- import { fmtErr } from './errors.js'
5
- import { createExecTool, updateExecToolDefinition } from './exec.js'
6
- import { fetchTool } from './fetch.js'
7
- import { createFindTool } from './find.js'
8
- import { createGitTool } from './git.js'
9
- import { globTool } from './glob.js'
10
- import { createGrepTool } from './grep.js'
11
- import { createInstallSkillTool } from './install_skill.js'
12
- import { createLsTool } from './ls.js'
13
- import { createReadTool } from './read.js'
14
- import { searchTool } from './search.js'
15
- import type { Tool, ToolCall, ToolDefinition, ToolResult } from './types.js'
16
- import { createWritePlanTool } from './write_plan.js'
17
-
18
- /**
19
- * Normalize tool input to prevent common LLM call misuses.
20
- * The LLM sometimes passes parameters in the wrong format — this function
21
- * auto-corrects those patterns so tools work reliably.
22
- *
23
- * Common misuses handled:
24
- * 1. Passed as a string instead of { ... } object wrapper
25
- * 2. Passed top-level params directly instead of nested in the expected key
26
- * 3. Array passed directly (for tools that expect { items: [...] })
27
- */
28
- function normalizeToolInput(
29
- toolName: string,
30
- _definition: ToolDefinition,
31
- input: Record<string, unknown>,
32
- ): Record<string, unknown> {
33
- // If input is a string (e.g. install_skill("package-name") or bash("ls -la"))
34
- // wrap it into the appropriate key
35
- if (typeof input === 'string') {
36
- if (toolName === 'bash' || toolName === 'git') {
37
- return { command: input }
38
- }
39
- if (
40
- toolName === 'install_skill' ||
41
- toolName === 'find' ||
42
- toolName === 'glob' ||
43
- toolName === 'grep'
44
- ) {
45
- return { pattern: input }
46
- }
47
- if (toolName === 'search' || toolName === 'fetch') {
48
- return { query: input, url: input }
49
- }
50
- if (toolName === 'write_plan') {
51
- return { filename: input, content: input }
52
- }
53
- if (toolName === 'exec') {
54
- return { code: input }
55
- }
56
- // For tools that take a single string param, guess from the definition
57
- const params = Object.keys(_definition.parameters)
58
- if (params.length === 1) {
59
- return { [params[0]]: input }
60
- }
61
- return input
62
- }
63
-
64
- // If input is an array (e.g. read called with ["file.ts"] directly)
65
- if (Array.isArray(input)) {
66
- if (toolName === 'read' || toolName === 'ls') {
67
- return { paths: input, path: (input as string[])[0] }
68
- }
69
- if (toolName === 'edit') {
70
- return { edits: input }
71
- }
72
- return input
73
- }
74
-
75
- // If edit tool gets an empty object, some models hallucinate the call
76
- if (toolName === 'edit' && Object.keys(input).length === 0) {
77
- return { edits: [] }
78
- }
79
-
80
- return input
81
- }
82
-
83
- export interface ToolContext {
84
- cwd: string
85
- autoApprove: boolean
86
- applier: FileReadTracker
87
- mode: 'code' | 'plan' | 'ask'
88
- onPlanWritten?: (display: string) => void
89
- }
90
-
91
- export interface ToolPlugin {
92
- name: string
93
- description: string
94
- create: (context: ToolContext) => Tool
95
- }
96
-
97
- /**
98
- * Extensible ToolRegistry with plugin support.
99
- * Inspired by pi's extension system for dynamic tool registration.
100
- */
101
- export class ToolRegistry {
102
- private tools: Map<string, Tool> = new Map()
103
- private context: ToolContext
104
- private plugins: Map<string, ToolPlugin> = new Map()
105
- /** Reference to the exec tool for dynamic description updates */
106
- private execTool: Tool | null = null
107
-
108
- constructor(context: ToolContext) {
109
- this.context = context
110
- this.registerBuiltins()
111
- }
112
-
113
- /** Register all built-in tools */
114
- private registerBuiltins(): void {
115
- // ask mode: only fetch and search + exec (exec works with any toolset)
116
- if (this.context.mode === 'ask') {
117
- this.register(fetchTool)
118
- this.register(searchTool)
119
- return
120
- }
121
-
122
- this.register(createReadTool(this.context.applier, this.context.cwd))
123
- this.register(globTool)
124
- this.register(createGrepTool(this.context.cwd))
125
- this.register(createLsTool(this.context.cwd))
126
- this.register(createFindTool(this.context.cwd))
127
- this.register(fetchTool)
128
- this.register(searchTool)
129
-
130
- if (this.context.mode === 'code') {
131
- // Code mode: full toolset including write operations
132
- this.register(bashTool)
133
- this.register(createGitTool(this.context.cwd))
134
- this.register(createInstallSkillTool(this.context.cwd))
135
- this.register(createEditTool(this.context.applier, this.context.cwd))
136
- this.registerExecTool()
137
- } else {
138
- // Plan mode: read-only investigation + write_plan
139
- this.register(createWritePlanTool(this.context.cwd, this.context.onPlanWritten))
140
- }
141
- }
142
-
143
- /** Register the exec tool with dynamic type declarations for all registered tools */
144
- private registerExecTool(): void {
145
- const execTool = createExecTool(() => Array.from(this.tools.values()))
146
- this.execTool = execTool
147
- this.register(execTool)
148
- }
149
-
150
- /** Refresh the exec tool's description to include current tool type declarations */
151
- private refreshExecDescription(): void {
152
- if (this.execTool) {
153
- updateExecToolDefinition(this.execTool, this.getDefinitions())
154
- }
155
- }
156
-
157
- setMode(mode: 'code' | 'plan' | 'ask'): void {
158
- // Update context.mode FIRST so registerBuiltins() and mode-based logic
159
- // use the NEW mode, not the stale one (fixes bug when switching from
160
- // ask→code or plan→code where all tools would be missing)
161
- this.context.mode = mode
162
-
163
- if (mode === 'code') {
164
- // Re-register all built-in tools for code mode
165
- this.tools.clear()
166
- this.execTool = null
167
- this.registerBuiltins()
168
- } else if (mode === 'plan') {
169
- // Keep existing tools but swap edit/write_plan, remove exec
170
- if (!this.tools.has('write_plan')) {
171
- this.register(createWritePlanTool(this.context.cwd, this.context.onPlanWritten))
172
- }
173
- this.tools.delete('edit')
174
- this.tools.delete('exec')
175
- this.execTool = null
176
- } else if (mode === 'ask') {
177
- // Ask mode: only fetch and search
178
- this.tools.clear()
179
- this.execTool = null
180
- this.register(fetchTool)
181
- this.register(searchTool)
182
- }
183
- }
184
-
185
- /** Register a single tool */
186
- register(tool: Tool): void {
187
- this.tools.set(tool.definition.name, tool)
188
- // Refresh exec tool description to include the new tool's type declarations
189
- if (this.execTool && tool.definition.name !== 'exec') {
190
- this.refreshExecDescription()
191
- }
192
- }
193
-
194
- /** Register a tool plugin (lazy creation pattern) */
195
- registerPlugin(plugin: ToolPlugin): void {
196
- this.plugins.set(plugin.name, plugin)
197
- // Activate immediately
198
- try {
199
- const tool = plugin.create(this.context)
200
- this.register(tool)
201
- } catch (err) {
202
- console.error(`Failed to activate plugin "${plugin.name}": ${err}`)
203
- }
204
- }
205
-
206
- /** Unregister a tool by name */
207
- unregister(name: string): boolean {
208
- return this.tools.delete(name)
209
- }
210
-
211
- /** Check if a tool is registered */
212
- has(name: string): boolean {
213
- return this.tools.has(name)
214
- }
215
-
216
- /** Re-register write_plan tool with an updated callback */
217
- reRegisterWritePlan(cwd: string, cb?: (display: string) => void): void {
218
- this.tools.delete('write_plan')
219
- this.tools.set('write_plan', createWritePlanTool(cwd, cb))
220
- }
221
-
222
- /** Partially update the context (used by session.onPlanWritten setter) */
223
- updateContext(partial: Partial<ToolContext>): void {
224
- Object.assign(this.context, partial)
225
- }
226
-
227
- /** List all registered tool names */
228
- listTools(): string[] {
229
- return Array.from(this.tools.keys())
230
- }
231
-
232
- getDefinitions(): ToolDefinition[] {
233
- return Array.from(this.tools.values()).map(t => t.definition)
234
- }
235
-
236
- async dispatch(call: ToolCall): Promise<ToolResult> {
237
- const tool = this.tools.get(call.name)
238
- if (!tool) {
239
- return {
240
- success: false,
241
- output: '',
242
- error: `Unknown tool: "${call.name}". Available: ${Array.from(this.tools.keys()).join(', ')}`,
243
- }
244
- }
245
-
246
- // ── Universal input normalization ──────────────────────────────────
247
- // Auto-correct common LLM call misuses before they reach tool execute()
248
- let input = call.input
249
- try {
250
- input = normalizeToolInput(call.name, tool.definition, input)
251
- } catch {
252
- // If normalization throws, let the tool handle it (or fail naturally)
253
- }
254
-
255
- try {
256
- return await tool.execute(input)
257
- } catch (err) {
258
- return {
259
- success: false,
260
- output: '',
261
- error: `Tool "${call.name}" threw: ${fmtErr(err)}`,
262
- }
263
- }
264
- }
265
- }
1
+ import type { FileReadTracker } from '../diff/apply.js'
2
+ import { bashTool } from './bash.js'
3
+ import { createEditTool } from './edit.js'
4
+ import { fmtErr } from './errors.js'
5
+ import { createExecTool, updateExecToolDefinition } from './exec.js'
6
+ import { fetchTool } from './fetch.js'
7
+ import { createFindTool } from './find.js'
8
+ import { createGitTool } from './git.js'
9
+ import { globTool } from './glob.js'
10
+ import { createGrepTool } from './grep.js'
11
+ import { createInstallSkillTool } from './install_skill.js'
12
+ import { createLsTool } from './ls.js'
13
+ import { createReadTool } from './read.js'
14
+ import { searchTool } from './search.js'
15
+ import type { Tool, ToolCall, ToolDefinition, ToolResult } from './types.js'
16
+ import { createWritePlanTool } from './write_plan.js'
17
+
18
+ /**
19
+ * Normalize tool input to prevent common LLM call misuses.
20
+ * The LLM sometimes passes parameters in the wrong format — this function
21
+ * auto-corrects those patterns so tools work reliably.
22
+ *
23
+ * Common misuses handled:
24
+ * 1. Passed as a string instead of { ... } object wrapper
25
+ * 2. Passed top-level params directly instead of nested in the expected key
26
+ * 3. Array passed directly (for tools that expect { items: [...] })
27
+ */
28
+ function normalizeToolInput(
29
+ toolName: string,
30
+ _definition: ToolDefinition,
31
+ input: Record<string, unknown>,
32
+ ): Record<string, unknown> {
33
+ // If input is a string (e.g. install_skill("package-name") or bash("ls -la"))
34
+ // wrap it into the appropriate key
35
+ if (typeof input === 'string') {
36
+ if (toolName === 'bash' || toolName === 'git') {
37
+ return { command: input }
38
+ }
39
+ if (
40
+ toolName === 'install_skill' ||
41
+ toolName === 'find' ||
42
+ toolName === 'glob' ||
43
+ toolName === 'grep'
44
+ ) {
45
+ return { pattern: input }
46
+ }
47
+ if (toolName === 'search' || toolName === 'fetch') {
48
+ return { query: input, url: input }
49
+ }
50
+ if (toolName === 'write_plan') {
51
+ return { filename: input, content: input }
52
+ }
53
+ if (toolName === 'exec') {
54
+ return { code: input }
55
+ }
56
+ // For tools that take a single string param, guess from the definition
57
+ const params = Object.keys(_definition.parameters)
58
+ if (params.length === 1) {
59
+ return { [params[0]]: input }
60
+ }
61
+ return input
62
+ }
63
+
64
+ // If input is an array (e.g. read called with ["file.ts"] directly)
65
+ if (Array.isArray(input)) {
66
+ if (toolName === 'read' || toolName === 'ls') {
67
+ return { paths: input, path: (input as string[])[0] }
68
+ }
69
+ if (toolName === 'edit') {
70
+ return { edits: input }
71
+ }
72
+ return input
73
+ }
74
+
75
+ // If edit tool gets an empty object, some models hallucinate the call
76
+ if (toolName === 'edit' && Object.keys(input).length === 0) {
77
+ return { edits: [] }
78
+ }
79
+
80
+ return input
81
+ }
82
+
83
+ export interface ToolContext {
84
+ cwd: string
85
+ autoApprove: boolean
86
+ applier: FileReadTracker
87
+ mode: 'code' | 'plan' | 'ask'
88
+ onPlanWritten?: (display: string) => void
89
+ }
90
+
91
+ export interface ToolPlugin {
92
+ name: string
93
+ description: string
94
+ create: (context: ToolContext) => Tool
95
+ }
96
+
97
+ /**
98
+ * Extensible ToolRegistry with plugin support.
99
+ * Inspired by pi's extension system for dynamic tool registration.
100
+ */
101
+ export class ToolRegistry {
102
+ private tools: Map<string, Tool> = new Map()
103
+ private context: ToolContext
104
+ private plugins: Map<string, ToolPlugin> = new Map()
105
+ /** Reference to the exec tool for dynamic description updates */
106
+ private execTool: Tool | null = null
107
+
108
+ constructor(context: ToolContext) {
109
+ this.context = context
110
+ this.registerBuiltins()
111
+ }
112
+
113
+ /** Register all built-in tools */
114
+ private registerBuiltins(): void {
115
+ // ask mode: only fetch and search + exec (exec works with any toolset)
116
+ if (this.context.mode === 'ask') {
117
+ this.register(fetchTool)
118
+ this.register(searchTool)
119
+ return
120
+ }
121
+
122
+ this.register(createReadTool(this.context.applier, this.context.cwd))
123
+ this.register(globTool)
124
+ this.register(createGrepTool(this.context.cwd))
125
+ this.register(createLsTool(this.context.cwd))
126
+ this.register(createFindTool(this.context.cwd))
127
+ this.register(fetchTool)
128
+ this.register(searchTool)
129
+
130
+ if (this.context.mode === 'code') {
131
+ // Code mode: full toolset including write operations
132
+ this.register(bashTool)
133
+ this.register(createGitTool(this.context.cwd))
134
+ this.register(createInstallSkillTool(this.context.cwd))
135
+ this.register(createEditTool(this.context.applier, this.context.cwd))
136
+ this.registerExecTool()
137
+ } else {
138
+ // Plan mode: read-only investigation + write_plan
139
+ this.register(createWritePlanTool(this.context.cwd, this.context.onPlanWritten))
140
+ }
141
+ }
142
+
143
+ /** Register the exec tool with dynamic type declarations for all registered tools */
144
+ private registerExecTool(): void {
145
+ const execTool = createExecTool(() => Array.from(this.tools.values()))
146
+ this.execTool = execTool
147
+ this.register(execTool)
148
+ }
149
+
150
+ /** Refresh the exec tool's description to include current tool type declarations */
151
+ private refreshExecDescription(): void {
152
+ if (this.execTool) {
153
+ updateExecToolDefinition(this.execTool, this.getDefinitions())
154
+ }
155
+ }
156
+
157
+ setMode(mode: 'code' | 'plan' | 'ask'): void {
158
+ // Update context.mode FIRST so registerBuiltins() and mode-based logic
159
+ // use the NEW mode, not the stale one (fixes bug when switching from
160
+ // ask→code or plan→code where all tools would be missing)
161
+ this.context.mode = mode
162
+
163
+ if (mode === 'code') {
164
+ // Re-register all built-in tools for code mode
165
+ this.tools.clear()
166
+ this.execTool = null
167
+ this.registerBuiltins()
168
+ } else if (mode === 'plan') {
169
+ // Keep existing tools but swap edit/write_plan, remove exec
170
+ if (!this.tools.has('write_plan')) {
171
+ this.register(createWritePlanTool(this.context.cwd, this.context.onPlanWritten))
172
+ }
173
+ this.tools.delete('edit')
174
+ this.tools.delete('exec')
175
+ this.execTool = null
176
+ } else if (mode === 'ask') {
177
+ // Ask mode: only fetch and search
178
+ this.tools.clear()
179
+ this.execTool = null
180
+ this.register(fetchTool)
181
+ this.register(searchTool)
182
+ }
183
+ }
184
+
185
+ /** Register a single tool */
186
+ register(tool: Tool): void {
187
+ this.tools.set(tool.definition.name, tool)
188
+ // Refresh exec tool description to include the new tool's type declarations
189
+ if (this.execTool && tool.definition.name !== 'exec') {
190
+ this.refreshExecDescription()
191
+ }
192
+ }
193
+
194
+ /** Register a tool plugin (lazy creation pattern) */
195
+ registerPlugin(plugin: ToolPlugin): void {
196
+ this.plugins.set(plugin.name, plugin)
197
+ // Activate immediately
198
+ try {
199
+ const tool = plugin.create(this.context)
200
+ this.register(tool)
201
+ } catch (err) {
202
+ console.error(`Failed to activate plugin "${plugin.name}": ${err}`)
203
+ }
204
+ }
205
+
206
+ /** Unregister a tool by name */
207
+ unregister(name: string): boolean {
208
+ return this.tools.delete(name)
209
+ }
210
+
211
+ /** Check if a tool is registered */
212
+ has(name: string): boolean {
213
+ return this.tools.has(name)
214
+ }
215
+
216
+ /** Re-register write_plan tool with an updated callback */
217
+ reRegisterWritePlan(cwd: string, cb?: (display: string) => void): void {
218
+ this.tools.delete('write_plan')
219
+ this.tools.set('write_plan', createWritePlanTool(cwd, cb))
220
+ }
221
+
222
+ /** Partially update the context (used by session.onPlanWritten setter) */
223
+ updateContext(partial: Partial<ToolContext>): void {
224
+ Object.assign(this.context, partial)
225
+ }
226
+
227
+ /** List all registered tool names */
228
+ listTools(): string[] {
229
+ return Array.from(this.tools.keys())
230
+ }
231
+
232
+ getDefinitions(): ToolDefinition[] {
233
+ return Array.from(this.tools.values()).map(t => t.definition)
234
+ }
235
+
236
+ async dispatch(call: ToolCall): Promise<ToolResult> {
237
+ const tool = this.tools.get(call.name)
238
+ if (!tool) {
239
+ return {
240
+ success: false,
241
+ output: '',
242
+ error: `Unknown tool: "${call.name}". Available: ${Array.from(this.tools.keys()).join(', ')}`,
243
+ }
244
+ }
245
+
246
+ // ── Universal input normalization ──────────────────────────────────
247
+ // Auto-correct common LLM call misuses before they reach tool execute()
248
+ let input = call.input
249
+ try {
250
+ input = normalizeToolInput(call.name, tool.definition, input)
251
+ } catch {
252
+ // If normalization throws, let the tool handle it (or fail naturally)
253
+ }
254
+
255
+ try {
256
+ return await tool.execute(input)
257
+ } catch (err) {
258
+ return {
259
+ success: false,
260
+ output: '',
261
+ error: `Tool "${call.name}" threw: ${fmtErr(err)}`,
262
+ }
263
+ }
264
+ }
265
+ }
package/src/tui/index.ts CHANGED
@@ -39,6 +39,19 @@ import {
39
39
  } from './components.js'
40
40
  import { highlightLine } from './highlight.js'
41
41
 
42
+ /**
43
+ * Invisible spacer that renders N empty lines.
44
+ * Used to reserve space at the bottom of the chat area so the editor
45
+ * overlay doesn't cover the last lines of command output.
46
+ */
47
+ class Spacer {
48
+ constructor(private height: number) {}
49
+ render(_width: number): string[] {
50
+ return Array.from({ length: this.height }, () => '')
51
+ }
52
+ invalidate(): void {}
53
+ }
54
+
42
55
  export async function startTui(config: Config): Promise<void> {
43
56
  let chatContent = ''
44
57
  let isRunning = false
@@ -95,7 +108,13 @@ export async function startTui(config: Config): Promise<void> {
95
108
  const filtered = data.replace(/\x1b\[\?1049[hl]/g, '')
96
109
  if (filtered) origWrite(filtered)
97
110
  }
98
- const tui = new TUI(terminal, false)
111
+ // Show hardware cursor by default so IME (Chinese input method) can
112
+ // position its candidate window at the correct cursor location.
113
+ // The cursor is hidden during agent execution via setShowHardwareCursor(false).
114
+ // Note: on some terminals (Windows Terminal), showing the hardware cursor
115
+ // can interfere with editor rendering layout. If you see editor border gap,
116
+ // set this to false and use PI_HARDWARE_CURSOR=1 env var to enable.
117
+ const tui = new TUI(terminal, true)
99
118
  tui.setClearOnShrink(true)
100
119
  terminal.setTitle(`lonny ${config.model} ${config.provider}`)
101
120
 
@@ -106,6 +125,9 @@ export async function startTui(config: Config): Promise<void> {
106
125
  const chatMarkdown = new Markdown('', 1, 0, markdownTheme)
107
126
  const chatBox = new Box(1, 0)
108
127
  chatBox.addChild(chatMarkdown)
128
+ // Reserve space at the bottom so the editor overlay doesn't cover
129
+ // the last lines of command output. 13 = maxHeight(12) + offsetY(1).
130
+ chatBox.addChild(new Spacer(13))
109
131
 
110
132
  // Chat input — Editor with multi-line support, history, and autocomplete
111
133
  const slashCommands: SlashCommand[] = [
package/src/web/index.ts CHANGED
@@ -4,7 +4,9 @@ import * as path from 'node:path'
4
4
  import * as url from 'node:url'
5
5
  import { type WebSocket, WebSocketServer } from 'ws'
6
6
  import { resetGlobalEventBus } from '../agent/event-bus.js'
7
+ import { ensurePromptsDir, loadPromptTemplates } from '../agent/prompt-templates.js'
7
8
  import { Session, type SessionOutput } from '../agent/session.js'
9
+ import { ensureSkillsDir, loadSkills } from '../agent/skills.js'
8
10
  import type { Config } from '../config/index.js'
9
11
  import { fmtErr } from '../tools/errors.js'
10
12
  import { fetchDeepSeekBalance, isDeepSeekOfficial } from '../tui/balance.js'
@@ -255,6 +257,70 @@ export async function startWebUi(config: Config, port: number): Promise<void> {
255
257
  return
256
258
  }
257
259
 
260
+ if (cmd === 'stop') {
261
+ sessionWithOutput.stop()
262
+ ws.send(JSON.stringify({ type: 'chunk', text: '\nStopped.\n' }))
263
+ ws.send(JSON.stringify({ type: 'done', reason: 'stop' }))
264
+ return
265
+ }
266
+
267
+ if (cmd === 'skills') {
268
+ const skills = loadSkills(config.cwd)
269
+ if (skills.length === 0) {
270
+ ws.send(
271
+ JSON.stringify({
272
+ type: 'chunk',
273
+ text: 'No skills loaded. Create .md files in .lonny/skills/ or run install_superpowers.',
274
+ }),
275
+ )
276
+ } else {
277
+ let msg = `Active Skills (${skills.length}):\n`
278
+ for (const s of skills) {
279
+ msg += ` \u2022 ${s.name}`
280
+ if (s.description) msg += ` - ${s.description}`
281
+ msg += '\n'
282
+ }
283
+ ws.send(JSON.stringify({ type: 'chunk', text: msg }))
284
+ }
285
+ ws.send(JSON.stringify({ type: 'done', reason: 'stop' }))
286
+ return
287
+ }
288
+
289
+ if (cmd === 'prompts') {
290
+ const templates = loadPromptTemplates(config.cwd)
291
+ if (templates.length === 0) {
292
+ ws.send(
293
+ JSON.stringify({
294
+ type: 'chunk',
295
+ text: 'No prompt templates found. Create .md files in .lonny/prompts/.',
296
+ }),
297
+ )
298
+ } else {
299
+ let msg = `Prompt Templates (${templates.length}):\n`
300
+ for (const t of templates) {
301
+ msg += ` \u2022 ${t.name}`
302
+ if (t.description) msg += ` - ${t.description}`
303
+ msg += '\n'
304
+ }
305
+ ws.send(JSON.stringify({ type: 'chunk', text: msg }))
306
+ }
307
+ ws.send(JSON.stringify({ type: 'done', reason: 'stop' }))
308
+ return
309
+ }
310
+
311
+ if (cmd === 'init') {
312
+ ensureSkillsDir(config.cwd)
313
+ ensurePromptsDir(config.cwd)
314
+ ws.send(
315
+ JSON.stringify({
316
+ type: 'chunk',
317
+ text: 'Initialized .lonny/skills/ and .lonny/prompts/.',
318
+ }),
319
+ )
320
+ ws.send(JSON.stringify({ type: 'done', reason: 'stop' }))
321
+ return
322
+ }
323
+
258
324
  if (cmd === 'help') {
259
325
  ws.send(
260
326
  JSON.stringify({
@@ -263,6 +329,10 @@ export async function startWebUi(config: Config, port: number): Promise<void> {
263
329
  '/mode code|plan|ask - Switch mode',
264
330
  '/model <name> - Switch model',
265
331
  '/new - Start a new session',
332
+ '/stop - Stop the running agent',
333
+ '/skills - List active skills',
334
+ '/prompts - List prompt templates',
335
+ '/init - Create .lonny/skills/ & prompts/',
266
336
  '/help - Show this help',
267
337
  ],
268
338
  }),
@@ -344,6 +414,7 @@ export async function startWebUi(config: Config, port: number): Promise<void> {
344
414
  JSON.stringify({
345
415
  type: 'hello',
346
416
  version: 1,
417
+ cwd: config.cwd,
347
418
  mode: config.mode,
348
419
  model: config.model,
349
420
  provider: config.provider,