@oh-my-pi/pi-coding-agent 5.5.0 → 5.6.70

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 (98) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/docs/python-repl.md +77 -0
  3. package/examples/hooks/snake.ts +7 -7
  4. package/package.json +5 -5
  5. package/src/bun-imports.d.ts +6 -0
  6. package/src/cli/args.ts +7 -0
  7. package/src/cli/setup-cli.ts +231 -0
  8. package/src/cli.ts +2 -0
  9. package/src/core/agent-session.ts +118 -15
  10. package/src/core/bash-executor.ts +3 -84
  11. package/src/core/compaction/compaction.ts +10 -5
  12. package/src/core/extensions/index.ts +2 -0
  13. package/src/core/extensions/loader.ts +13 -1
  14. package/src/core/extensions/runner.ts +50 -2
  15. package/src/core/extensions/types.ts +67 -2
  16. package/src/core/keybindings.ts +51 -1
  17. package/src/core/prompt-templates.ts +15 -0
  18. package/src/core/python-executor-display.test.ts +42 -0
  19. package/src/core/python-executor-lifecycle.test.ts +99 -0
  20. package/src/core/python-executor-mapping.test.ts +41 -0
  21. package/src/core/python-executor-per-call.test.ts +49 -0
  22. package/src/core/python-executor-session.test.ts +103 -0
  23. package/src/core/python-executor-streaming.test.ts +77 -0
  24. package/src/core/python-executor-timeout.test.ts +35 -0
  25. package/src/core/python-executor.lifecycle.test.ts +139 -0
  26. package/src/core/python-executor.result.test.ts +49 -0
  27. package/src/core/python-executor.test.ts +180 -0
  28. package/src/core/python-executor.ts +313 -0
  29. package/src/core/python-gateway-coordinator.ts +832 -0
  30. package/src/core/python-kernel-display.test.ts +54 -0
  31. package/src/core/python-kernel-env.test.ts +138 -0
  32. package/src/core/python-kernel-session.test.ts +87 -0
  33. package/src/core/python-kernel-ws.test.ts +104 -0
  34. package/src/core/python-kernel.lifecycle.test.ts +249 -0
  35. package/src/core/python-kernel.test.ts +461 -0
  36. package/src/core/python-kernel.ts +1182 -0
  37. package/src/core/python-modules.test.ts +102 -0
  38. package/src/core/python-modules.ts +110 -0
  39. package/src/core/python-prelude.py +889 -0
  40. package/src/core/python-prelude.test.ts +140 -0
  41. package/src/core/python-prelude.ts +3 -0
  42. package/src/core/sdk.ts +24 -6
  43. package/src/core/session-manager.ts +174 -82
  44. package/src/core/settings-manager-python.test.ts +23 -0
  45. package/src/core/settings-manager.ts +202 -0
  46. package/src/core/streaming-output.test.ts +26 -0
  47. package/src/core/streaming-output.ts +100 -0
  48. package/src/core/system-prompt.python.test.ts +17 -0
  49. package/src/core/system-prompt.ts +3 -1
  50. package/src/core/timings.ts +1 -1
  51. package/src/core/tools/bash.ts +13 -2
  52. package/src/core/tools/edit-diff.ts +9 -1
  53. package/src/core/tools/index.test.ts +50 -23
  54. package/src/core/tools/index.ts +83 -1
  55. package/src/core/tools/python-execution.test.ts +68 -0
  56. package/src/core/tools/python-fallback.test.ts +72 -0
  57. package/src/core/tools/python-renderer.test.ts +36 -0
  58. package/src/core/tools/python-tool-mode.test.ts +43 -0
  59. package/src/core/tools/python.test.ts +121 -0
  60. package/src/core/tools/python.ts +760 -0
  61. package/src/core/tools/renderers.ts +2 -0
  62. package/src/core/tools/schema-validation.test.ts +1 -0
  63. package/src/core/tools/task/executor.ts +146 -3
  64. package/src/core/tools/task/worker-protocol.ts +32 -2
  65. package/src/core/tools/task/worker.ts +182 -15
  66. package/src/index.ts +6 -0
  67. package/src/main.ts +136 -40
  68. package/src/modes/interactive/components/custom-editor.ts +16 -31
  69. package/src/modes/interactive/components/extensions/extension-dashboard.ts +5 -16
  70. package/src/modes/interactive/components/extensions/extension-list.ts +5 -13
  71. package/src/modes/interactive/components/history-search.ts +5 -8
  72. package/src/modes/interactive/components/hook-editor.ts +3 -4
  73. package/src/modes/interactive/components/hook-input.ts +3 -3
  74. package/src/modes/interactive/components/hook-selector.ts +5 -15
  75. package/src/modes/interactive/components/index.ts +1 -0
  76. package/src/modes/interactive/components/keybinding-hints.ts +66 -0
  77. package/src/modes/interactive/components/model-selector.ts +53 -66
  78. package/src/modes/interactive/components/oauth-selector.ts +5 -5
  79. package/src/modes/interactive/components/session-selector.ts +29 -23
  80. package/src/modes/interactive/components/settings-defs.ts +404 -196
  81. package/src/modes/interactive/components/settings-selector.ts +14 -10
  82. package/src/modes/interactive/components/status-line-segment-editor.ts +7 -7
  83. package/src/modes/interactive/components/tool-execution.ts +8 -0
  84. package/src/modes/interactive/components/tree-selector.ts +29 -23
  85. package/src/modes/interactive/components/user-message-selector.ts +6 -17
  86. package/src/modes/interactive/controllers/command-controller.ts +86 -37
  87. package/src/modes/interactive/controllers/event-controller.ts +8 -0
  88. package/src/modes/interactive/controllers/extension-ui-controller.ts +51 -0
  89. package/src/modes/interactive/controllers/input-controller.ts +42 -6
  90. package/src/modes/interactive/interactive-mode.ts +56 -30
  91. package/src/modes/interactive/theme/theme-schema.json +2 -2
  92. package/src/modes/interactive/types.ts +6 -1
  93. package/src/modes/interactive/utils/ui-helpers.ts +2 -1
  94. package/src/modes/print-mode.ts +23 -0
  95. package/src/modes/rpc/rpc-mode.ts +21 -0
  96. package/src/prompts/agents/reviewer.md +1 -1
  97. package/src/prompts/system/system-prompt.md +32 -1
  98. package/src/prompts/tools/python.md +91 -0
@@ -13,6 +13,8 @@ import { getCapabilities } from "@oh-my-pi/pi-tui";
13
13
  import type {
14
14
  ImageProviderOption,
15
15
  NotificationMethod,
16
+ PythonKernelMode,
17
+ PythonToolMode,
16
18
  SettingsManager,
17
19
  StatusLinePreset,
18
20
  StatusLineSeparatorStyle,
@@ -76,21 +78,32 @@ const THINKING_DESCRIPTIONS: Record<ThinkingLevel, string> = {
76
78
  /**
77
79
  * All settings definitions.
78
80
  * Order determines display order within each tab.
81
+ *
82
+ * Tabs:
83
+ * - behavior: Core agent behavior (compaction, modes, retries, notifications)
84
+ * - tools: Tool-specific settings (bash, git, python, edit, MCP, skills)
85
+ * - display: Visual/UI settings (theme, images, thinking)
86
+ * - voice: Voice mode and TTSR settings
87
+ * - status: Status line configuration
88
+ * - lsp: LSP integration settings
89
+ * - exa: Exa search tool settings
79
90
  */
80
91
  export const SETTINGS_DEFS: SettingDef[] = [
81
- // Config tab
92
+ // ═══════════════════════════════════════════════════════════════════════════
93
+ // Behavior tab - Core agent behavior
94
+ // ═══════════════════════════════════════════════════════════════════════════
82
95
  {
83
96
  id: "autoCompact",
84
- tab: "config",
97
+ tab: "behavior",
85
98
  type: "boolean",
86
99
  label: "Auto-compact",
87
100
  description: "Automatically compact context when it gets too large",
88
101
  get: (sm) => sm.getCompactionEnabled(),
89
- set: (sm, v) => sm.setCompactionEnabled(v), // Also handled in session
102
+ set: (sm, v) => sm.setCompactionEnabled(v),
90
103
  },
91
104
  {
92
105
  id: "branchSummaries",
93
- tab: "config",
106
+ tab: "behavior",
94
107
  type: "boolean",
95
108
  label: "Branch summaries",
96
109
  description: "Prompt to summarize when leaving a branch",
@@ -99,102 +112,96 @@ export const SETTINGS_DEFS: SettingDef[] = [
99
112
  },
100
113
  {
101
114
  id: "todoCompletion",
102
- tab: "config",
115
+ tab: "behavior",
103
116
  type: "boolean",
104
117
  label: "Todo completion",
105
- description: "Remind agent to complete todos before stopping (up to 3 reminders)",
118
+ description: "Remind agent to complete todos before stopping",
106
119
  get: (sm) => sm.getTodoCompletionEnabled(),
107
120
  set: (sm, v) => sm.setTodoCompletionEnabled(v),
108
121
  },
109
122
  {
110
- id: "showImages",
111
- tab: "config",
112
- type: "boolean",
113
- label: "Show images",
114
- description: "Render images inline in terminal",
115
- get: (sm) => sm.getShowImages(),
116
- set: (sm, v) => sm.setShowImages(v),
117
- condition: () => !!getCapabilities().images,
118
- },
119
- {
120
- id: "voiceEnabled",
121
- tab: "config",
122
- type: "boolean",
123
- label: "Voice mode",
124
- description: "Enable realtime voice input/output (Ctrl+Y toggle, auto-send on silence)",
125
- get: (sm) => sm.getVoiceEnabled(),
126
- set: (sm, v) => sm.setVoiceEnabled(v),
127
- },
128
- {
129
- id: "completionNotification",
130
- tab: "config",
131
- type: "enum",
132
- label: "Completion notification",
133
- description: "Notify when the agent completes",
134
- values: ["auto", "bell", "osc99", "osc9", "off"],
135
- get: (sm) => sm.getNotificationOnComplete(),
136
- set: (sm, v) => sm.setNotificationOnComplete(v as NotificationMethod),
137
- },
138
- {
139
- id: "autoResizeImages",
140
- tab: "config",
141
- type: "boolean",
142
- label: "Auto-resize images",
143
- description: "Resize large images to 2000x2000 max for better model compatibility",
144
- get: (sm) => sm.getImageAutoResize(),
145
- set: (sm, v) => sm.setImageAutoResize(v),
146
- },
147
- {
148
- id: "blockImages",
149
- tab: "config",
150
- type: "boolean",
151
- label: "Block images",
152
- description: "Prevent images from being sent to LLM providers",
153
- get: (sm) => sm.getBlockImages(),
154
- set: (sm, v) => sm.setBlockImages(v),
123
+ id: "todoCompletionMaxReminders",
124
+ tab: "behavior",
125
+ type: "submenu",
126
+ label: "Todo max reminders",
127
+ description: "Maximum reminders to complete todos before giving up",
128
+ get: (sm) => String(sm.getTodoCompletionMaxReminders()),
129
+ set: (sm, v) => sm.setTodoCompletionMaxReminders(Number.parseInt(v, 10)),
130
+ getOptions: () => [
131
+ { value: "1", label: "1 reminder" },
132
+ { value: "2", label: "2 reminders" },
133
+ { value: "3", label: "3 reminders" },
134
+ { value: "5", label: "5 reminders" },
135
+ ],
155
136
  },
156
137
  {
157
138
  id: "steeringMode",
158
- tab: "config",
139
+ tab: "behavior",
159
140
  type: "enum",
160
141
  label: "Steering mode",
161
142
  description: "How to process queued messages while agent is working",
162
143
  values: ["one-at-a-time", "all"],
163
144
  get: (sm) => sm.getSteeringMode(),
164
- set: (sm, v) => sm.setSteeringMode(v as "all" | "one-at-a-time"), // Also handled in session
145
+ set: (sm, v) => sm.setSteeringMode(v as "all" | "one-at-a-time"),
165
146
  },
166
147
  {
167
148
  id: "followUpMode",
168
- tab: "config",
149
+ tab: "behavior",
169
150
  type: "enum",
170
151
  label: "Follow-up mode",
171
152
  description: "How to drain follow-up messages after a turn completes",
172
153
  values: ["one-at-a-time", "all"],
173
154
  get: (sm) => sm.getFollowUpMode(),
174
- set: (sm, v) => sm.setFollowUpMode(v as "one-at-a-time" | "all"), // Also handled in session
155
+ set: (sm, v) => sm.setFollowUpMode(v as "one-at-a-time" | "all"),
175
156
  },
176
157
  {
177
158
  id: "interruptMode",
178
- tab: "config",
159
+ tab: "behavior",
179
160
  type: "enum",
180
161
  label: "Interrupt mode",
181
162
  description: "When steering messages interrupt tool execution",
182
163
  values: ["immediate", "wait"],
183
164
  get: (sm) => sm.getInterruptMode(),
184
- set: (sm, v) => sm.setInterruptMode(v as "immediate" | "wait"), // Also handled in session
165
+ set: (sm, v) => sm.setInterruptMode(v as "immediate" | "wait"),
185
166
  },
186
167
  {
187
- id: "hideThinking",
188
- tab: "config",
168
+ id: "retryMaxRetries",
169
+ tab: "behavior",
170
+ type: "submenu",
171
+ label: "Retry max attempts",
172
+ description: "Maximum retry attempts on API errors",
173
+ get: (sm) => String(sm.getRetryMaxRetries()),
174
+ set: (sm, v) => sm.setRetryMaxRetries(Number.parseInt(v, 10)),
175
+ getOptions: () => [
176
+ { value: "1", label: "1 retry" },
177
+ { value: "2", label: "2 retries" },
178
+ { value: "3", label: "3 retries" },
179
+ { value: "5", label: "5 retries" },
180
+ { value: "10", label: "10 retries" },
181
+ ],
182
+ },
183
+ {
184
+ id: "completionNotification",
185
+ tab: "behavior",
186
+ type: "enum",
187
+ label: "Completion notification",
188
+ description: "Notify when the agent completes",
189
+ values: ["auto", "bell", "osc99", "osc9", "off"],
190
+ get: (sm) => sm.getNotificationOnComplete(),
191
+ set: (sm, v) => sm.setNotificationOnComplete(v as NotificationMethod),
192
+ },
193
+ {
194
+ id: "startupQuiet",
195
+ tab: "behavior",
189
196
  type: "boolean",
190
- label: "Hide thinking",
191
- description: "Hide thinking blocks in assistant responses",
192
- get: (sm) => sm.getHideThinkingBlock(),
193
- set: (sm, v) => sm.setHideThinkingBlock(v),
197
+ label: "Startup quiet",
198
+ description: "Skip welcome screen and startup status messages",
199
+ get: (sm) => sm.getStartupQuiet(),
200
+ set: (sm, v) => sm.setStartupQuiet(v),
194
201
  },
195
202
  {
196
203
  id: "collapseChangelog",
197
- tab: "config",
204
+ tab: "behavior",
198
205
  type: "boolean",
199
206
  label: "Collapse changelog",
200
207
  description: "Show condensed changelog after updates",
@@ -203,7 +210,7 @@ export const SETTINGS_DEFS: SettingDef[] = [
203
210
  },
204
211
  {
205
212
  id: "doubleEscapeAction",
206
- tab: "config",
213
+ tab: "behavior",
207
214
  type: "enum",
208
215
  label: "Double-escape action",
209
216
  description: "Action when pressing Escape twice with empty editor",
@@ -211,18 +218,31 @@ export const SETTINGS_DEFS: SettingDef[] = [
211
218
  get: (sm) => sm.getDoubleEscapeAction(),
212
219
  set: (sm, v) => sm.setDoubleEscapeAction(v as "branch" | "tree"),
213
220
  },
221
+
222
+ // ═══════════════════════════════════════════════════════════════════════════
223
+ // Tools tab - Tool-specific settings
224
+ // ═══════════════════════════════════════════════════════════════════════════
214
225
  {
215
226
  id: "bashInterceptor",
216
- tab: "config",
227
+ tab: "tools",
217
228
  type: "boolean",
218
229
  label: "Bash interceptor",
219
230
  description: "Block shell commands that have dedicated tools (grep, cat, etc.)",
220
231
  get: (sm) => sm.getBashInterceptorEnabled(),
221
232
  set: (sm, v) => sm.setBashInterceptorEnabled(v),
222
233
  },
234
+ {
235
+ id: "bashInterceptorSimpleLs",
236
+ tab: "tools",
237
+ type: "boolean",
238
+ label: "Intercept simple ls",
239
+ description: "Intercept bare ls commands (when bash interceptor is enabled)",
240
+ get: (sm) => sm.getBashInterceptorSimpleLsEnabled(),
241
+ set: (sm, v) => sm.setBashInterceptorSimpleLsEnabled(v),
242
+ },
223
243
  {
224
244
  id: "gitTool",
225
- tab: "config",
245
+ tab: "tools",
226
246
  type: "boolean",
227
247
  label: "Git tool",
228
248
  description: "Enable structured Git tool",
@@ -230,17 +250,37 @@ export const SETTINGS_DEFS: SettingDef[] = [
230
250
  set: (sm, v) => sm.setGitToolEnabled(v),
231
251
  },
232
252
  {
233
- id: "mcpProjectConfig",
234
- tab: "config",
253
+ id: "pythonToolMode",
254
+ tab: "tools",
255
+ type: "enum",
256
+ label: "Python tool mode",
257
+ description: "How Python code is executed",
258
+ values: ["ipy-only", "bash-only", "both"],
259
+ get: (sm) => sm.getPythonToolMode(),
260
+ set: (sm, v) => sm.setPythonToolMode(v as PythonToolMode),
261
+ },
262
+ {
263
+ id: "pythonKernelMode",
264
+ tab: "tools",
265
+ type: "enum",
266
+ label: "Python kernel mode",
267
+ description: "Whether to keep IPython kernel alive across calls",
268
+ values: ["session", "per-call"],
269
+ get: (sm) => sm.getPythonKernelMode(),
270
+ set: (sm, v) => sm.setPythonKernelMode(v as PythonKernelMode),
271
+ },
272
+ {
273
+ id: "pythonSharedGateway",
274
+ tab: "tools",
235
275
  type: "boolean",
236
- label: "MCP project config",
237
- description: "Load .mcp.json/mcp.json from project root",
238
- get: (sm) => sm.getMCPProjectConfigEnabled(),
239
- set: (sm, v) => sm.setMCPProjectConfigEnabled(v),
276
+ label: "Python shared gateway",
277
+ description: "Share IPython kernel gateway across pi instances",
278
+ get: (sm) => sm.getPythonSharedGateway(),
279
+ set: (sm, v) => sm.setPythonSharedGateway(v),
240
280
  },
241
281
  {
242
282
  id: "editFuzzyMatch",
243
- tab: "config",
283
+ tab: "tools",
244
284
  type: "boolean",
245
285
  label: "Edit fuzzy match",
246
286
  description: "Accept high-confidence fuzzy matches for whitespace/indentation differences",
@@ -248,76 +288,44 @@ export const SETTINGS_DEFS: SettingDef[] = [
248
288
  set: (sm, v) => sm.setEditFuzzyMatch(v),
249
289
  },
250
290
  {
251
- id: "ttsrEnabled",
252
- tab: "config",
291
+ id: "mcpProjectConfig",
292
+ tab: "tools",
253
293
  type: "boolean",
254
- label: "TTSR enabled",
255
- description: "Time Traveling Stream Rules: interrupt agent when output matches rule patterns",
256
- get: (sm) => sm.getTtsrEnabled(),
257
- set: (sm, v) => sm.setTtsrEnabled(v),
258
- },
259
- {
260
- id: "ttsrContextMode",
261
- tab: "config",
262
- type: "enum",
263
- label: "TTSR context mode",
264
- description: "What to do with partial output when TTSR triggers",
265
- values: ["discard", "keep"],
266
- get: (sm) => sm.getTtsrContextMode(),
267
- set: (sm, v) => sm.setTtsrContextMode(v as "keep" | "discard"),
268
- },
269
- {
270
- id: "ttsrRepeatMode",
271
- tab: "config",
272
- type: "enum",
273
- label: "TTSR repeat mode",
274
- description: "How rules can repeat: once per session or after a message gap",
275
- values: ["once", "after-gap"],
276
- get: (sm) => sm.getTtsrRepeatMode(),
277
- set: (sm, v) => sm.setTtsrRepeatMode(v as "once" | "after-gap"),
294
+ label: "MCP project config",
295
+ description: "Load .mcp.json/mcp.json from project root",
296
+ get: (sm) => sm.getMCPProjectConfigEnabled(),
297
+ set: (sm, v) => sm.setMCPProjectConfigEnabled(v),
278
298
  },
279
299
  {
280
- id: "thinkingLevel",
281
- tab: "config",
282
- type: "submenu",
283
- label: "Thinking level",
284
- description: "Reasoning depth for thinking-capable models",
285
- get: (sm) => sm.getDefaultThinkingLevel() ?? "off",
286
- set: (sm, v) => sm.setDefaultThinkingLevel(v as ThinkingLevel), // Also handled in session
287
- getOptions: () =>
288
- (["off", "minimal", "low", "medium", "high", "xhigh"] as ThinkingLevel[]).map((level) => ({
289
- value: level,
290
- label: level,
291
- description: THINKING_DESCRIPTIONS[level],
292
- })),
300
+ id: "skillCommands",
301
+ tab: "tools",
302
+ type: "boolean",
303
+ label: "Skill commands",
304
+ description: "Register skills as /skill:name commands",
305
+ get: (sm) => sm.getEnableSkillCommands(),
306
+ set: (sm, v) => sm.setEnableSkillCommands(v),
293
307
  },
294
308
  {
295
- id: "theme",
296
- tab: "config",
297
- type: "submenu",
298
- label: "Theme",
299
- description: "Color theme for the interface",
300
- get: (sm) => sm.getTheme() ?? "dark",
301
- set: (sm, v) => sm.setTheme(v),
302
- getOptions: () => [], // Filled dynamically from context
309
+ id: "claudeUserCommands",
310
+ tab: "tools",
311
+ type: "boolean",
312
+ label: "Claude user commands",
313
+ description: "Load commands from ~/.claude/commands/",
314
+ get: (sm) => sm.getCommandsEnableClaudeUser(),
315
+ set: (sm, v) => sm.setCommandsEnableClaudeUser(v),
303
316
  },
304
317
  {
305
- id: "symbolPreset",
306
- tab: "config",
307
- type: "submenu",
308
- label: "Symbol preset",
309
- description: "Icon/symbol style (overrides theme default)",
310
- get: (sm) => sm.getSymbolPreset() ?? "unicode",
311
- set: (sm, v) => sm.setSymbolPreset(v as SymbolPreset),
312
- getOptions: () => [
313
- { value: "unicode", label: "Unicode", description: "Standard Unicode symbols (default)" },
314
- { value: "nerd", label: "Nerd Font", description: "Nerd Font icons (requires Nerd Font)" },
315
- { value: "ascii", label: "ASCII", description: "ASCII-only characters (maximum compatibility)" },
316
- ],
318
+ id: "claudeProjectCommands",
319
+ tab: "tools",
320
+ type: "boolean",
321
+ label: "Claude project commands",
322
+ description: "Load commands from .claude/commands/",
323
+ get: (sm) => sm.getCommandsEnableClaudeProject(),
324
+ set: (sm, v) => sm.setCommandsEnableClaudeProject(v),
317
325
  },
318
326
  {
319
327
  id: "webSearchProvider",
320
- tab: "config",
328
+ tab: "tools",
321
329
  type: "submenu",
322
330
  label: "Web search provider",
323
331
  description: "Provider for web search tool",
@@ -332,7 +340,7 @@ export const SETTINGS_DEFS: SettingDef[] = [
332
340
  },
333
341
  {
334
342
  id: "imageProvider",
335
- tab: "config",
343
+ tab: "tools",
336
344
  type: "submenu",
337
345
  label: "Image provider",
338
346
  description: "Provider for image generation tool",
@@ -345,92 +353,203 @@ export const SETTINGS_DEFS: SettingDef[] = [
345
353
  ],
346
354
  },
347
355
 
348
- // LSP tab
356
+ // ═══════════════════════════════════════════════════════════════════════════
357
+ // Display tab - Visual/UI settings
358
+ // ═══════════════════════════════════════════════════════════════════════════
349
359
  {
350
- id: "lspFormatOnWrite",
351
- tab: "lsp",
352
- type: "boolean",
353
- label: "Format on write",
354
- description: "Automatically format code files using LSP after writing",
355
- get: (sm) => sm.getLspFormatOnWrite(),
356
- set: (sm, v) => sm.setLspFormatOnWrite(v),
360
+ id: "theme",
361
+ tab: "display",
362
+ type: "submenu",
363
+ label: "Theme",
364
+ description: "Color theme for the interface",
365
+ get: (sm) => sm.getTheme() ?? "dark",
366
+ set: (sm, v) => sm.setTheme(v),
367
+ getOptions: () => [], // Filled dynamically from context
357
368
  },
358
369
  {
359
- id: "lspDiagnosticsOnWrite",
360
- tab: "lsp",
361
- type: "boolean",
362
- label: "Diagnostics on write",
363
- description: "Return LSP diagnostics (errors/warnings) after writing code files",
364
- get: (sm) => sm.getLspDiagnosticsOnWrite(),
365
- set: (sm, v) => sm.setLspDiagnosticsOnWrite(v),
370
+ id: "symbolPreset",
371
+ tab: "display",
372
+ type: "submenu",
373
+ label: "Symbol preset",
374
+ description: "Icon/symbol style (overrides theme default)",
375
+ get: (sm) => sm.getSymbolPreset() ?? "unicode",
376
+ set: (sm, v) => sm.setSymbolPreset(v as SymbolPreset),
377
+ getOptions: () => [
378
+ { value: "unicode", label: "Unicode", description: "Standard Unicode symbols (default)" },
379
+ { value: "nerd", label: "Nerd Font", description: "Nerd Font icons (requires Nerd Font)" },
380
+ { value: "ascii", label: "ASCII", description: "ASCII-only characters (maximum compatibility)" },
381
+ ],
366
382
  },
367
383
  {
368
- id: "lspDiagnosticsOnEdit",
369
- tab: "lsp",
384
+ id: "thinkingLevel",
385
+ tab: "display",
386
+ type: "submenu",
387
+ label: "Thinking level",
388
+ description: "Reasoning depth for thinking-capable models",
389
+ get: (sm) => sm.getDefaultThinkingLevel() ?? "off",
390
+ set: (sm, v) => sm.setDefaultThinkingLevel(v as ThinkingLevel),
391
+ getOptions: () =>
392
+ (["off", "minimal", "low", "medium", "high", "xhigh"] as ThinkingLevel[]).map((level) => ({
393
+ value: level,
394
+ label: level,
395
+ description: THINKING_DESCRIPTIONS[level],
396
+ })),
397
+ },
398
+ {
399
+ id: "hideThinking",
400
+ tab: "display",
370
401
  type: "boolean",
371
- label: "Diagnostics on edit",
372
- description: "Return LSP diagnostics (errors/warnings) after editing code files",
373
- get: (sm) => sm.getLspDiagnosticsOnEdit(),
374
- set: (sm, v) => sm.setLspDiagnosticsOnEdit(v),
402
+ label: "Hide thinking",
403
+ description: "Hide thinking blocks in assistant responses",
404
+ get: (sm) => sm.getHideThinkingBlock(),
405
+ set: (sm, v) => sm.setHideThinkingBlock(v),
375
406
  },
376
-
377
- // Exa tab
378
407
  {
379
- id: "exaEnabled",
380
- tab: "exa",
408
+ id: "showImages",
409
+ tab: "display",
381
410
  type: "boolean",
382
- label: "Exa enabled",
383
- description: "Master toggle for all Exa search tools",
384
- get: (sm) => sm.getExaSettings().enabled,
385
- set: (sm, v) => sm.setExaEnabled(v),
411
+ label: "Show images",
412
+ description: "Render images inline in terminal",
413
+ get: (sm) => sm.getShowImages(),
414
+ set: (sm, v) => sm.setShowImages(v),
415
+ condition: () => !!getCapabilities().images,
386
416
  },
387
417
  {
388
- id: "exaSearch",
389
- tab: "exa",
418
+ id: "autoResizeImages",
419
+ tab: "display",
390
420
  type: "boolean",
391
- label: "Exa search",
392
- description: "Basic search, deep search, code search, crawl",
393
- get: (sm) => sm.getExaSettings().enableSearch,
394
- set: (sm, v) => sm.setExaSearchEnabled(v),
421
+ label: "Auto-resize images",
422
+ description: "Resize large images to 2000x2000 max for better model compatibility",
423
+ get: (sm) => sm.getImageAutoResize(),
424
+ set: (sm, v) => sm.setImageAutoResize(v),
395
425
  },
396
426
  {
397
- id: "exaLinkedin",
398
- tab: "exa",
427
+ id: "blockImages",
428
+ tab: "display",
399
429
  type: "boolean",
400
- label: "Exa LinkedIn",
401
- description: "Search LinkedIn for people and companies",
402
- get: (sm) => sm.getExaSettings().enableLinkedin,
403
- set: (sm, v) => sm.setExaLinkedinEnabled(v),
430
+ label: "Block images",
431
+ description: "Prevent images from being sent to LLM providers",
432
+ get: (sm) => sm.getBlockImages(),
433
+ set: (sm, v) => sm.setBlockImages(v),
404
434
  },
405
435
  {
406
- id: "exaCompany",
407
- tab: "exa",
436
+ id: "showHardwareCursor",
437
+ tab: "display",
408
438
  type: "boolean",
409
- label: "Exa company",
410
- description: "Comprehensive company research tool",
411
- get: (sm) => sm.getExaSettings().enableCompany,
412
- set: (sm, v) => sm.setExaCompanyEnabled(v),
439
+ label: "Hardware cursor",
440
+ description: "Show terminal cursor for IME support (default: on for Linux/macOS)",
441
+ get: (sm) => sm.getShowHardwareCursor(),
442
+ set: (sm, v) => sm.setShowHardwareCursor(v),
413
443
  },
444
+
445
+ // ═══════════════════════════════════════════════════════════════════════════
446
+ // Voice tab - Voice mode and TTSR settings
447
+ // ═══════════════════════════════════════════════════════════════════════════
414
448
  {
415
- id: "exaResearcher",
416
- tab: "exa",
449
+ id: "voiceEnabled",
450
+ tab: "voice",
417
451
  type: "boolean",
418
- label: "Exa researcher",
419
- description: "AI-powered deep research tasks",
420
- get: (sm) => sm.getExaSettings().enableResearcher,
421
- set: (sm, v) => sm.setExaResearcherEnabled(v),
452
+ label: "Voice mode",
453
+ description: "Enable realtime voice input/output (Ctrl+Y toggle, auto-send on silence)",
454
+ get: (sm) => sm.getVoiceEnabled(),
455
+ set: (sm, v) => sm.setVoiceEnabled(v),
422
456
  },
423
457
  {
424
- id: "exaWebsets",
425
- tab: "exa",
458
+ id: "voiceTtsModel",
459
+ tab: "voice",
460
+ type: "submenu",
461
+ label: "TTS model",
462
+ description: "Text-to-speech model for voice output",
463
+ get: (sm) => sm.getVoiceTtsModel(),
464
+ set: (sm, v) => sm.setVoiceTtsModel(v),
465
+ getOptions: () => [
466
+ { value: "gpt-4o-mini-tts", label: "GPT-4o Mini TTS", description: "Fast and efficient" },
467
+ { value: "tts-1", label: "TTS-1", description: "Standard quality" },
468
+ { value: "tts-1-hd", label: "TTS-1 HD", description: "Higher quality" },
469
+ ],
470
+ },
471
+ {
472
+ id: "voiceTtsVoice",
473
+ tab: "voice",
474
+ type: "submenu",
475
+ label: "TTS voice",
476
+ description: "Voice for text-to-speech output",
477
+ get: (sm) => sm.getVoiceTtsVoice(),
478
+ set: (sm, v) => sm.setVoiceTtsVoice(v),
479
+ getOptions: () => [
480
+ { value: "alloy", label: "Alloy", description: "Neutral" },
481
+ { value: "echo", label: "Echo", description: "Male" },
482
+ { value: "fable", label: "Fable", description: "British" },
483
+ { value: "onyx", label: "Onyx", description: "Deep male" },
484
+ { value: "nova", label: "Nova", description: "Female" },
485
+ { value: "shimmer", label: "Shimmer", description: "Female" },
486
+ ],
487
+ },
488
+ {
489
+ id: "voiceTtsFormat",
490
+ tab: "voice",
491
+ type: "submenu",
492
+ label: "TTS format",
493
+ description: "Audio format for voice output",
494
+ get: (sm) => sm.getVoiceTtsFormat(),
495
+ set: (sm, v) => sm.setVoiceTtsFormat(v as "wav" | "mp3" | "opus" | "aac" | "flac"),
496
+ getOptions: () => [
497
+ { value: "wav", label: "WAV", description: "Uncompressed, best quality" },
498
+ { value: "mp3", label: "MP3", description: "Compressed, widely compatible" },
499
+ { value: "opus", label: "Opus", description: "Efficient compression" },
500
+ { value: "aac", label: "AAC", description: "Apple-friendly" },
501
+ { value: "flac", label: "FLAC", description: "Lossless compression" },
502
+ ],
503
+ },
504
+ {
505
+ id: "ttsrEnabled",
506
+ tab: "voice",
426
507
  type: "boolean",
427
- label: "Exa websets",
428
- description: "Webset management and enrichment tools",
429
- get: (sm) => sm.getExaSettings().enableWebsets,
430
- set: (sm, v) => sm.setExaWebsetsEnabled(v),
508
+ label: "TTSR enabled",
509
+ description: "Time Traveling Stream Rules: interrupt agent when output matches patterns",
510
+ get: (sm) => sm.getTtsrEnabled(),
511
+ set: (sm, v) => sm.setTtsrEnabled(v),
512
+ },
513
+ {
514
+ id: "ttsrContextMode",
515
+ tab: "voice",
516
+ type: "enum",
517
+ label: "TTSR context mode",
518
+ description: "What to do with partial output when TTSR triggers",
519
+ values: ["discard", "keep"],
520
+ get: (sm) => sm.getTtsrContextMode(),
521
+ set: (sm, v) => sm.setTtsrContextMode(v as "keep" | "discard"),
522
+ },
523
+ {
524
+ id: "ttsrRepeatMode",
525
+ tab: "voice",
526
+ type: "enum",
527
+ label: "TTSR repeat mode",
528
+ description: "How rules can repeat: once per session or after a message gap",
529
+ values: ["once", "after-gap"],
530
+ get: (sm) => sm.getTtsrRepeatMode(),
531
+ set: (sm, v) => sm.setTtsrRepeatMode(v as "once" | "after-gap"),
532
+ },
533
+ {
534
+ id: "ttsrRepeatGap",
535
+ tab: "voice",
536
+ type: "submenu",
537
+ label: "TTSR repeat gap",
538
+ description: "Messages before a rule can trigger again (when repeat mode is after-gap)",
539
+ get: (sm) => String(sm.getTtsrRepeatGap()),
540
+ set: (sm, v) => sm.setTtsrRepeatGap(Number.parseInt(v, 10)),
541
+ getOptions: () => [
542
+ { value: "5", label: "5 messages" },
543
+ { value: "10", label: "10 messages" },
544
+ { value: "15", label: "15 messages" },
545
+ { value: "20", label: "20 messages" },
546
+ { value: "30", label: "30 messages" },
547
+ ],
431
548
  },
432
549
 
433
- // Status Line tab
550
+ // ═══════════════════════════════════════════════════════════════════════════
551
+ // Status tab - Status line configuration
552
+ // ═══════════════════════════════════════════════════════════════════════════
434
553
  {
435
554
  id: "statusLinePreset",
436
555
  tab: "status",
@@ -487,7 +606,7 @@ export const SETTINGS_DEFS: SettingDef[] = [
487
606
  label: "Configure segments",
488
607
  description: "Choose and arrange status line segments",
489
608
  get: () => "configure...",
490
- set: () => {}, // Handled specially
609
+ set: () => {},
491
610
  getOptions: () => [{ value: "open", label: "Open segment editor..." }],
492
611
  },
493
612
  {
@@ -693,6 +812,95 @@ export const SETTINGS_DEFS: SettingDef[] = [
693
812
  }
694
813
  },
695
814
  },
815
+
816
+ // ═══════════════════════════════════════════════════════════════════════════
817
+ // LSP tab - LSP integration settings
818
+ // ═══════════════════════════════════════════════════════════════════════════
819
+ {
820
+ id: "lspFormatOnWrite",
821
+ tab: "lsp",
822
+ type: "boolean",
823
+ label: "Format on write",
824
+ description: "Automatically format code files using LSP after writing",
825
+ get: (sm) => sm.getLspFormatOnWrite(),
826
+ set: (sm, v) => sm.setLspFormatOnWrite(v),
827
+ },
828
+ {
829
+ id: "lspDiagnosticsOnWrite",
830
+ tab: "lsp",
831
+ type: "boolean",
832
+ label: "Diagnostics on write",
833
+ description: "Return LSP diagnostics (errors/warnings) after writing code files",
834
+ get: (sm) => sm.getLspDiagnosticsOnWrite(),
835
+ set: (sm, v) => sm.setLspDiagnosticsOnWrite(v),
836
+ },
837
+ {
838
+ id: "lspDiagnosticsOnEdit",
839
+ tab: "lsp",
840
+ type: "boolean",
841
+ label: "Diagnostics on edit",
842
+ description: "Return LSP diagnostics (errors/warnings) after editing code files",
843
+ get: (sm) => sm.getLspDiagnosticsOnEdit(),
844
+ set: (sm, v) => sm.setLspDiagnosticsOnEdit(v),
845
+ },
846
+
847
+ // ═══════════════════════════════════════════════════════════════════════════
848
+ // Exa tab - Exa search tool settings
849
+ // ═══════════════════════════════════════════════════════════════════════════
850
+ {
851
+ id: "exaEnabled",
852
+ tab: "exa",
853
+ type: "boolean",
854
+ label: "Exa enabled",
855
+ description: "Master toggle for all Exa search tools",
856
+ get: (sm) => sm.getExaSettings().enabled,
857
+ set: (sm, v) => sm.setExaEnabled(v),
858
+ },
859
+ {
860
+ id: "exaSearch",
861
+ tab: "exa",
862
+ type: "boolean",
863
+ label: "Exa search",
864
+ description: "Basic search, deep search, code search, crawl",
865
+ get: (sm) => sm.getExaSettings().enableSearch,
866
+ set: (sm, v) => sm.setExaSearchEnabled(v),
867
+ },
868
+ {
869
+ id: "exaLinkedin",
870
+ tab: "exa",
871
+ type: "boolean",
872
+ label: "Exa LinkedIn",
873
+ description: "Search LinkedIn for people and companies",
874
+ get: (sm) => sm.getExaSettings().enableLinkedin,
875
+ set: (sm, v) => sm.setExaLinkedinEnabled(v),
876
+ },
877
+ {
878
+ id: "exaCompany",
879
+ tab: "exa",
880
+ type: "boolean",
881
+ label: "Exa company",
882
+ description: "Comprehensive company research tool",
883
+ get: (sm) => sm.getExaSettings().enableCompany,
884
+ set: (sm, v) => sm.setExaCompanyEnabled(v),
885
+ },
886
+ {
887
+ id: "exaResearcher",
888
+ tab: "exa",
889
+ type: "boolean",
890
+ label: "Exa researcher",
891
+ description: "AI-powered deep research tasks",
892
+ get: (sm) => sm.getExaSettings().enableResearcher,
893
+ set: (sm, v) => sm.setExaResearcherEnabled(v),
894
+ },
895
+ {
896
+ id: "exaWebsets",
897
+ tab: "exa",
898
+ type: "boolean",
899
+ label: "Exa websets",
900
+ description: "Webset management and enrichment tools",
901
+ get: (sm) => sm.getExaSettings().enableWebsets,
902
+ set: (sm, v) => sm.setExaWebsetsEnabled(v),
903
+ },
696
904
  ];
697
905
 
698
906
  /**