@oh-my-pi/pi-coding-agent 9.3.1 → 9.6.0

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 (88) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/examples/hooks/snake.ts +5 -5
  3. package/package.json +9 -8
  4. package/src/capability/index.ts +7 -9
  5. package/src/cli/config-cli.ts +86 -73
  6. package/src/cli/update-cli.ts +45 -3
  7. package/src/commit/agentic/agent.ts +4 -4
  8. package/src/commit/agentic/index.ts +6 -5
  9. package/src/commit/agentic/tools/analyze-file.ts +5 -7
  10. package/src/commit/agentic/tools/index.ts +3 -3
  11. package/src/commit/model-selection.ts +13 -17
  12. package/src/commit/pipeline.ts +5 -5
  13. package/src/config/model-registry.ts +7 -0
  14. package/src/config/settings-schema.ts +836 -0
  15. package/src/config/settings.ts +702 -0
  16. package/src/discovery/helpers.ts +55 -11
  17. package/src/exa/index.ts +1 -1
  18. package/src/exec/bash-executor.ts +13 -13
  19. package/src/exec/shell-session.ts +15 -3
  20. package/src/export/ttsr.ts +1 -1
  21. package/src/extensibility/skills.ts +40 -9
  22. package/src/index.ts +2 -10
  23. package/src/ipy/gateway-coordinator.ts +5 -159
  24. package/src/ipy/kernel.ts +6 -171
  25. package/src/ipy/runtime.ts +198 -0
  26. package/src/lsp/client.ts +14 -1
  27. package/src/lsp/defaults.json +0 -6
  28. package/src/lsp/index.ts +1 -1
  29. package/src/lsp/types.ts +2 -0
  30. package/src/main.ts +26 -48
  31. package/src/modes/components/armin.ts +7 -7
  32. package/src/modes/components/extensions/extension-dashboard.ts +33 -13
  33. package/src/modes/components/extensions/extension-list.ts +2 -2
  34. package/src/modes/components/footer.ts +5 -5
  35. package/src/modes/components/history-search.ts +2 -1
  36. package/src/modes/components/hook-selector.ts +2 -2
  37. package/src/modes/components/index.ts +1 -1
  38. package/src/modes/components/model-selector.ts +7 -7
  39. package/src/modes/components/session-selector.ts +2 -1
  40. package/src/modes/components/settings-defs.ts +210 -915
  41. package/src/modes/components/settings-selector.ts +80 -106
  42. package/src/modes/components/status-line/types.ts +2 -8
  43. package/src/modes/components/status-line-segment-editor.ts +4 -4
  44. package/src/modes/components/status-line.ts +28 -5
  45. package/src/modes/components/welcome.ts +3 -3
  46. package/src/modes/controllers/command-controller.ts +2 -2
  47. package/src/modes/controllers/event-controller.ts +9 -8
  48. package/src/modes/controllers/input-controller.ts +19 -15
  49. package/src/modes/controllers/selector-controller.ts +30 -14
  50. package/src/modes/interactive-mode.ts +10 -10
  51. package/src/modes/rpc/rpc-mode.ts +10 -0
  52. package/src/modes/rpc/rpc-types.ts +3 -0
  53. package/src/modes/types.ts +2 -2
  54. package/src/modes/utils/ui-helpers.ts +4 -3
  55. package/src/patch/index.ts +7 -7
  56. package/src/patch/normalize.ts +3 -1
  57. package/src/prompts/system/plan-mode-active.md +5 -4
  58. package/src/prompts/system/system-prompt.md +0 -1
  59. package/src/prompts/tools/bash.md +12 -2
  60. package/src/prompts/tools/task.md +180 -73
  61. package/src/sdk.ts +38 -61
  62. package/src/session/agent-session.ts +66 -55
  63. package/src/session/agent-storage.ts +1 -1
  64. package/src/session/session-manager.ts +10 -10
  65. package/src/system-prompt.ts +2 -2
  66. package/src/task/executor.ts +9 -9
  67. package/src/task/index.ts +2 -2
  68. package/src/tools/ask.ts +5 -6
  69. package/src/tools/bash-interceptor.ts +39 -1
  70. package/src/tools/bash-normalize.ts +126 -0
  71. package/src/tools/bash.ts +31 -5
  72. package/src/tools/find.ts +51 -33
  73. package/src/tools/gemini-image.ts +7 -8
  74. package/src/tools/index.ts +5 -23
  75. package/src/tools/plan-mode-guard.ts +1 -6
  76. package/src/tools/python.ts +29 -4
  77. package/src/tools/read.ts +2 -2
  78. package/src/tools/write.ts +2 -2
  79. package/src/tui/output-block.ts +2 -2
  80. package/src/tui/utils.ts +2 -2
  81. package/src/utils/ignore-files.ts +119 -0
  82. package/src/web/search/auth.ts +6 -58
  83. package/src/web/search/index.ts +2 -6
  84. package/src/web/search/providers/anthropic.ts +6 -6
  85. package/src/web/search/providers/exa.ts +2 -62
  86. package/src/web/search/providers/perplexity.ts +7 -53
  87. package/examples/sdk/10-settings.ts +0 -37
  88. package/src/config/settings-manager.ts +0 -2015
@@ -1,961 +1,256 @@
1
1
  /**
2
- * Declarative settings definitions.
2
+ * Declarative settings definitions for the UI.
3
3
  *
4
- * Each setting is defined once here and the UI is generated automatically.
5
- * To add a new setting:
6
- * 1. Add it to SettingsManager (getter/setter)
7
- * 2. Add the definition here
8
- * 3. Add the handler in interactive-mode.ts settingsHandlers
4
+ * This file derives UI definitions from the schema - no duplicate get/set wrappers.
5
+ * To add a new setting to the UI:
6
+ * 1. Add it to settings-schema.ts with a `ui` field
7
+ * 2. That's it - it appears in the UI automatically
9
8
  */
10
- import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
11
9
  import { TERMINAL_INFO } from "@oh-my-pi/pi-tui";
12
- import type {
13
- ImageProviderOption,
14
- KimiApiFormatOption,
15
- NotificationMethod,
16
- PythonKernelMode,
17
- PythonToolMode,
18
- SettingsManager,
19
- StatusLinePreset,
20
- StatusLineSeparatorStyle,
21
- SymbolPreset,
22
- WebSearchProviderOption,
23
- } from "../../config/settings-manager";
24
- import { getPreset } from "./status-line/presets";
10
+ import {
11
+ getDefault,
12
+ getEnumValues,
13
+ getPathsForTab,
14
+ getType,
15
+ getUi,
16
+ type SettingPath,
17
+ type SettingTab,
18
+ } from "../../config/settings-schema";
19
+
20
+ // ═══════════════════════════════════════════════════════════════════════════
21
+ // UI Definition Types
22
+ // ═══════════════════════════════════════════════════════════════════════════
25
23
 
26
- // Setting value types
27
24
  export type SettingValue = boolean | string;
28
25
 
29
- // Base definition for all settings
30
26
  interface BaseSettingDef {
31
- id: string;
27
+ path: SettingPath;
32
28
  label: string;
33
29
  description: string;
34
- tab: string;
30
+ tab: SettingTab;
35
31
  }
36
32
 
37
- // Boolean toggle setting
38
33
  export interface BooleanSettingDef extends BaseSettingDef {
39
34
  type: "boolean";
40
- get: (sm: SettingsManager) => boolean;
41
- set: (sm: SettingsManager, value: boolean) => void;
42
- /** If provided, setting is only shown when this returns true */
43
35
  condition?: () => boolean;
44
36
  }
45
37
 
46
- // Enum setting (inline toggle between values)
47
38
  export interface EnumSettingDef extends BaseSettingDef {
48
39
  type: "enum";
49
40
  values: readonly string[];
50
- get: (sm: SettingsManager) => string;
51
- set: (sm: SettingsManager, value: string) => void;
52
41
  }
53
42
 
54
- // Submenu setting (opens a selection list)
55
43
  export interface SubmenuSettingDef extends BaseSettingDef {
56
44
  type: "submenu";
57
- get: (sm: SettingsManager) => string;
58
- set: (sm: SettingsManager, value: string) => void;
59
- /** Get available options dynamically */
60
- getOptions: (sm: SettingsManager) => Array<{ value: string; label: string; description?: string }>;
61
- /** Called when selection changes (for preview) */
45
+ getOptions: () => Array<{ value: string; label: string; description?: string }>;
62
46
  onPreview?: (value: string) => void;
63
- /** Called when submenu is cancelled (to restore preview) */
64
47
  onPreviewCancel?: (originalValue: string) => void;
65
48
  }
66
49
 
67
50
  export type SettingDef = BooleanSettingDef | EnumSettingDef | SubmenuSettingDef;
68
51
 
69
- const THINKING_DESCRIPTIONS: Record<ThinkingLevel, string> = {
52
+ // ═══════════════════════════════════════════════════════════════════════════
53
+ // Condition Functions
54
+ // ═══════════════════════════════════════════════════════════════════════════
55
+
56
+ const CONDITIONS: Record<string, () => boolean> = {
57
+ hasImageProtocol: () => !!TERMINAL_INFO.imageProtocol,
58
+ };
59
+
60
+ // ═══════════════════════════════════════════════════════════════════════════
61
+ // Submenu Option Providers
62
+ // ═══════════════════════════════════════════════════════════════════════════
63
+
64
+ type OptionProvider = () => Array<{ value: string; label: string; description?: string }>;
65
+
66
+ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
67
+ // Retry max retries
68
+ "retry.maxRetries": () => [
69
+ { value: "1", label: "1 retry" },
70
+ { value: "2", label: "2 retries" },
71
+ { value: "3", label: "3 retries" },
72
+ { value: "5", label: "5 retries" },
73
+ { value: "10", label: "10 retries" },
74
+ ],
75
+ // Todo max reminders
76
+ "todoCompletion.maxReminders": () => [
77
+ { value: "1", label: "1 reminder" },
78
+ { value: "2", label: "2 reminders" },
79
+ { value: "3", label: "3 reminders" },
80
+ { value: "5", label: "5 reminders" },
81
+ ],
82
+ // Ask timeout
83
+ "ask.timeout": () => [
84
+ { value: "0", label: "Disabled" },
85
+ { value: "15", label: "15 seconds" },
86
+ { value: "30", label: "30 seconds" },
87
+ { value: "60", label: "60 seconds" },
88
+ { value: "120", label: "120 seconds" },
89
+ ],
90
+ // Edit fuzzy threshold
91
+ "edit.fuzzyThreshold": () => [
92
+ { value: "0.85", label: "0.85", description: "Lenient" },
93
+ { value: "0.90", label: "0.90", description: "Moderate" },
94
+ { value: "0.95", label: "0.95", description: "Default" },
95
+ { value: "0.98", label: "0.98", description: "Strict" },
96
+ ],
97
+ // TTSR repeat gap
98
+ "ttsr.repeatGap": () => [
99
+ { value: "5", label: "5 messages" },
100
+ { value: "10", label: "10 messages" },
101
+ { value: "15", label: "15 messages" },
102
+ { value: "20", label: "20 messages" },
103
+ { value: "30", label: "30 messages" },
104
+ ],
105
+ // Provider options
106
+ "providers.webSearch": () => [
107
+ { value: "auto", label: "Auto", description: "Priority: Exa > Perplexity > Anthropic" },
108
+ { value: "exa", label: "Exa", description: "Requires EXA_API_KEY" },
109
+ { value: "perplexity", label: "Perplexity", description: "Requires PERPLEXITY_API_KEY" },
110
+ { value: "anthropic", label: "Anthropic", description: "Uses Anthropic web search" },
111
+ ],
112
+ "providers.image": () => [
113
+ { value: "auto", label: "Auto", description: "Priority: OpenRouter > Gemini" },
114
+ { value: "gemini", label: "Gemini", description: "Requires GEMINI_API_KEY" },
115
+ { value: "openrouter", label: "OpenRouter", description: "Requires OPENROUTER_API_KEY" },
116
+ ],
117
+ "providers.kimiApiFormat": () => [
118
+ { value: "openai", label: "OpenAI", description: "api.kimi.com" },
119
+ { value: "anthropic", label: "Anthropic", description: "api.moonshot.ai" },
120
+ ],
121
+ // Symbol preset
122
+ symbolPreset: () => [
123
+ { value: "unicode", label: "Unicode", description: "Standard symbols (default)" },
124
+ { value: "nerd", label: "Nerd Font", description: "Requires Nerd Font" },
125
+ { value: "ascii", label: "ASCII", description: "Maximum compatibility" },
126
+ ],
127
+ // Status line preset
128
+ "statusLine.preset": () => [
129
+ { value: "default", label: "Default", description: "Model, path, git, context, tokens, cost" },
130
+ { value: "minimal", label: "Minimal", description: "Path and git only" },
131
+ { value: "compact", label: "Compact", description: "Model, git, cost, context" },
132
+ { value: "full", label: "Full", description: "All segments including time" },
133
+ { value: "nerd", label: "Nerd", description: "Maximum info with Nerd Font icons" },
134
+ { value: "ascii", label: "ASCII", description: "No special characters" },
135
+ { value: "custom", label: "Custom", description: "User-defined segments" },
136
+ ],
137
+ // Status line separator
138
+ "statusLine.separator": () => [
139
+ { value: "powerline", label: "Powerline", description: "Solid arrows (Nerd Font)" },
140
+ { value: "powerline-thin", label: "Thin chevron", description: "Thin arrows (Nerd Font)" },
141
+ { value: "slash", label: "Slash", description: "Forward slashes" },
142
+ { value: "pipe", label: "Pipe", description: "Vertical pipes" },
143
+ { value: "block", label: "Block", description: "Solid blocks" },
144
+ { value: "none", label: "None", description: "Space only" },
145
+ { value: "ascii", label: "ASCII", description: "Greater-than signs" },
146
+ ],
147
+ };
148
+
149
+ const THINKING_DESCRIPTIONS: Record<string, string> = {
70
150
  off: "No reasoning",
71
- minimal: "Very brief reasoning (~1k tokens)",
72
- low: "Light reasoning (~2k tokens)",
73
- medium: "Moderate reasoning (~8k tokens)",
74
- high: "Deep reasoning (~16k tokens)",
75
- xhigh: "Maximum reasoning (~32k tokens)",
151
+ minimal: "Very brief (~1k tokens)",
152
+ low: "Light (~2k tokens)",
153
+ medium: "Moderate (~8k tokens)",
154
+ high: "Deep (~16k tokens)",
155
+ xhigh: "Maximum (~32k tokens)",
76
156
  };
77
157
 
78
- /**
79
- * All settings definitions.
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
- * - ttsr: Time Traveling Stream Rules settings
87
- * - status: Status line configuration
88
- * - lsp: LSP integration settings
89
- * - exa: Exa search tool settings
90
- */
91
- export const SETTINGS_DEFS: SettingDef[] = [
92
- // ═══════════════════════════════════════════════════════════════════════════
93
- // Behavior tab - Core agent behavior
94
- // ═══════════════════════════════════════════════════════════════════════════
95
- {
96
- id: "autoCompact",
97
- tab: "behavior",
98
- type: "boolean",
99
- label: "Auto-compact",
100
- description: "Automatically compact context when it gets too large",
101
- get: sm => sm.getCompactionEnabled(),
102
- set: (sm, v) => sm.setCompactionEnabled(v),
103
- },
104
- {
105
- id: "branchSummaries",
106
- tab: "behavior",
107
- type: "boolean",
108
- label: "Branch summaries",
109
- description: "Prompt to summarize when leaving a branch",
110
- get: sm => sm.getBranchSummaryEnabled(),
111
- set: (sm, v) => sm.setBranchSummaryEnabled(v),
112
- },
113
- {
114
- id: "todoCompletion",
115
- tab: "behavior",
116
- type: "boolean",
117
- label: "Todo completion",
118
- description: "Remind agent to complete todos before stopping",
119
- get: sm => sm.getTodoCompletionEnabled(),
120
- set: (sm, v) => sm.setTodoCompletionEnabled(v),
121
- },
122
- {
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
- ],
136
- },
137
- {
138
- id: "steeringMode",
139
- tab: "behavior",
140
- type: "enum",
141
- label: "Steering mode",
142
- description: "How to process queued messages while agent is working",
143
- values: ["one-at-a-time", "all"],
144
- get: sm => sm.getSteeringMode(),
145
- set: (sm, v) => sm.setSteeringMode(v as "all" | "one-at-a-time"),
146
- },
147
- {
148
- id: "followUpMode",
149
- tab: "behavior",
150
- type: "enum",
151
- label: "Follow-up mode",
152
- description: "How to drain follow-up messages after a turn completes",
153
- values: ["one-at-a-time", "all"],
154
- get: sm => sm.getFollowUpMode(),
155
- set: (sm, v) => sm.setFollowUpMode(v as "one-at-a-time" | "all"),
156
- },
157
- {
158
- id: "interruptMode",
159
- tab: "behavior",
160
- type: "enum",
161
- label: "Interrupt mode",
162
- description: "When steering messages interrupt tool execution",
163
- values: ["immediate", "wait"],
164
- get: sm => sm.getInterruptMode(),
165
- set: (sm, v) => sm.setInterruptMode(v as "immediate" | "wait"),
166
- },
167
- {
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: "askTimeout",
195
- tab: "behavior",
196
- type: "enum",
197
- label: "Ask tool timeout",
198
- description: "Auto-select recommended option after timeout (disabled in plan mode)",
199
- values: ["off", "15", "30", "60", "120"],
200
- get: sm => {
201
- const timeout = sm.getAskTimeout();
202
- return timeout === null ? "off" : String(timeout / 1000);
203
- },
204
- set: (sm, v) => sm.setAskTimeout(v === "off" ? null : Number.parseInt(v, 10)),
205
- },
206
- {
207
- id: "askNotification",
208
- tab: "behavior",
209
- type: "enum",
210
- label: "Ask notification",
211
- description: "Notify when ask tool is waiting for input",
212
- values: ["auto", "bell", "osc99", "osc9", "off"],
213
- get: sm => sm.getAskNotification(),
214
- set: (sm, v) => sm.setAskNotification(v as NotificationMethod),
215
- },
216
- {
217
- id: "startupQuiet",
218
- tab: "behavior",
219
- type: "boolean",
220
- label: "Startup quiet",
221
- description: "Skip welcome screen and startup status messages",
222
- get: sm => sm.getStartupQuiet(),
223
- set: (sm, v) => sm.setStartupQuiet(v),
224
- },
225
- {
226
- id: "collapseChangelog",
227
- tab: "behavior",
228
- type: "boolean",
229
- label: "Collapse changelog",
230
- description: "Show condensed changelog after updates",
231
- get: sm => sm.getCollapseChangelog(),
232
- set: (sm, v) => sm.setCollapseChangelog(v),
233
- },
234
- {
235
- id: "normativeRewrite",
236
- tab: "behavior",
237
- type: "boolean",
238
- label: "Normative rewrite",
239
- description: "Rewrite tool call arguments to normalized format in session history",
240
- get: sm => sm.getNormativeRewrite(),
241
- set: (sm, v) => sm.setNormativeRewrite(v),
242
- },
243
- {
244
- id: "doubleEscapeAction",
245
- tab: "behavior",
246
- type: "enum",
247
- label: "Double-escape action",
248
- description: "Action when pressing Escape twice with empty editor",
249
- values: ["tree", "branch"],
250
- get: sm => sm.getDoubleEscapeAction(),
251
- set: (sm, v) => sm.setDoubleEscapeAction(v as "branch" | "tree"),
252
- },
158
+ // ═══════════════════════════════════════════════════════════════════════════
159
+ // Schema to UI Conversion
160
+ // ═══════════════════════════════════════════════════════════════════════════
253
161
 
254
- // ═══════════════════════════════════════════════════════════════════════════
255
- // Tools tab - Tool-specific settings
256
- // ═══════════════════════════════════════════════════════════════════════════
257
- {
258
- id: "bashInterceptor",
259
- tab: "tools",
260
- type: "boolean",
261
- label: "Bash interceptor",
262
- description: "Block shell commands that have dedicated tools (grep, cat, etc.)",
263
- get: sm => sm.getBashInterceptorEnabled(),
264
- set: (sm, v) => sm.setBashInterceptorEnabled(v),
265
- },
266
- {
267
- id: "shellForceBasic",
268
- tab: "tools",
269
- type: "boolean",
270
- label: "Force basic shell",
271
- description: "Use bash/sh even if your default shell is different",
272
- get: sm => sm.getShellForceBasic(),
273
- set: (sm, v) => sm.setShellForceBasic(v),
274
- },
275
- {
276
- id: "bashInterceptorSimpleLs",
277
- tab: "tools",
278
- type: "boolean",
279
- label: "Intercept simple ls",
280
- description: "Intercept bare ls commands (when bash interceptor is enabled)",
281
- get: sm => sm.getBashInterceptorSimpleLsEnabled(),
282
- set: (sm, v) => sm.setBashInterceptorSimpleLsEnabled(v),
283
- },
284
- {
285
- id: "pythonToolMode",
286
- tab: "tools",
287
- type: "enum",
288
- label: "Python tool mode",
289
- description: "How Python code is executed",
290
- values: ["ipy-only", "bash-only", "both"],
291
- get: sm => sm.getPythonToolMode(),
292
- set: (sm, v) => sm.setPythonToolMode(v as PythonToolMode),
293
- },
294
- {
295
- id: "pythonKernelMode",
296
- tab: "tools",
297
- type: "enum",
298
- label: "Python kernel mode",
299
- description: "Whether to keep IPython kernel alive across calls",
300
- values: ["session", "per-call"],
301
- get: sm => sm.getPythonKernelMode(),
302
- set: (sm, v) => sm.setPythonKernelMode(v as PythonKernelMode),
303
- },
304
- {
305
- id: "pythonSharedGateway",
306
- tab: "tools",
307
- type: "boolean",
308
- label: "Python shared gateway",
309
- description: "Share IPython kernel gateway across pi instances",
310
- get: sm => sm.getPythonSharedGateway(),
311
- set: (sm, v) => sm.setPythonSharedGateway(v),
312
- },
313
- {
314
- id: "editFuzzyMatch",
315
- tab: "tools",
316
- type: "boolean",
317
- label: "Edit fuzzy match",
318
- description: "Accept high-confidence fuzzy matches for whitespace/indentation differences",
319
- get: sm => sm.getEditFuzzyMatch(),
320
- set: (sm, v) => sm.setEditFuzzyMatch(v),
321
- },
322
- {
323
- id: "editFuzzyThreshold",
324
- tab: "tools",
325
- type: "submenu",
326
- label: "Edit fuzzy threshold",
327
- description: "Similarity threshold for fuzzy matches (higher = stricter)",
328
- get: sm => sm.getEditFuzzyThreshold().toFixed(2),
329
- set: (sm, v) => sm.setEditFuzzyThreshold(Number(v)),
330
- getOptions: () => [
331
- { value: "0.85", label: "0.85", description: "Lenient" },
332
- { value: "0.90", label: "0.90", description: "Moderate" },
333
- { value: "0.95", label: "0.95", description: "Default" },
334
- { value: "0.98", label: "0.98", description: "Strict" },
335
- ],
336
- },
337
- {
338
- id: "editPatchMode",
339
- tab: "tools",
340
- type: "boolean",
341
- label: "Edit patch mode",
342
- description: "Use codex-style apply-patch format instead of old_text/new_text for edits",
343
- get: sm => sm.getEditPatchMode(),
344
- set: (sm, v) => sm.setEditPatchMode(v),
345
- },
346
- {
347
- id: "editStreamingAbort",
348
- tab: "tools",
349
- type: "boolean",
350
- label: "Edit streaming abort",
351
- description: "Abort streaming edit tool calls when patch preview fails",
352
- get: sm => sm.getEditStreamingAbort(),
353
- set: (sm, v) => sm.setEditStreamingAbort(v),
354
- },
355
- {
356
- id: "readLineNumbers",
357
- tab: "tools",
358
- type: "boolean",
359
- label: "Read line numbers",
360
- description: "Prepend line numbers to read tool output by default",
361
- get: sm => sm.getReadLineNumbers(),
362
- set: (sm, v) => sm.setReadLineNumbers(v),
363
- },
364
- {
365
- id: "mcpProjectConfig",
366
- tab: "tools",
367
- type: "boolean",
368
- label: "MCP project config",
369
- description: "Load .mcp.json/mcp.json from project root",
370
- get: sm => sm.getMCPProjectConfigEnabled(),
371
- set: (sm, v) => sm.setMCPProjectConfigEnabled(v),
372
- },
373
- {
374
- id: "skillCommands",
375
- tab: "tools",
376
- type: "boolean",
377
- label: "Skill commands",
378
- description: "Register skills as /skill:name commands",
379
- get: sm => sm.getEnableSkillCommands(),
380
- set: (sm, v) => sm.setEnableSkillCommands(v),
381
- },
382
- {
383
- id: "claudeUserCommands",
384
- tab: "tools",
385
- type: "boolean",
386
- label: "Claude user commands",
387
- description: "Load commands from ~/.claude/commands/",
388
- get: sm => sm.getCommandsEnableClaudeUser(),
389
- set: (sm, v) => sm.setCommandsEnableClaudeUser(v),
390
- },
391
- {
392
- id: "claudeProjectCommands",
393
- tab: "tools",
394
- type: "boolean",
395
- label: "Claude project commands",
396
- description: "Load commands from .claude/commands/",
397
- get: sm => sm.getCommandsEnableClaudeProject(),
398
- set: (sm, v) => sm.setCommandsEnableClaudeProject(v),
399
- },
400
- {
401
- id: "webSearchProvider",
402
- tab: "tools",
403
- type: "submenu",
404
- label: "Web search provider",
405
- description: "Provider for web search tool",
406
- get: sm => sm.getWebSearchProvider(),
407
- set: (sm, v) => sm.setWebSearchProvider(v as WebSearchProviderOption),
408
- getOptions: () => [
409
- { value: "auto", label: "Auto", description: "Priority: Exa > Perplexity > Anthropic" },
410
- { value: "exa", label: "Exa", description: "Use Exa (requires EXA_API_KEY)" },
411
- { value: "perplexity", label: "Perplexity", description: "Use Perplexity (requires PERPLEXITY_API_KEY)" },
412
- { value: "anthropic", label: "Anthropic", description: "Use Anthropic web search" },
413
- ],
414
- },
415
- {
416
- id: "imageProvider",
417
- tab: "tools",
418
- type: "submenu",
419
- label: "Image provider",
420
- description: "Provider for image generation tool",
421
- get: sm => sm.getImageProvider(),
422
- set: (sm, v) => sm.setImageProvider(v as ImageProviderOption),
423
- getOptions: () => [
424
- { value: "auto", label: "Auto", description: "Priority: OpenRouter > Gemini" },
425
- { value: "gemini", label: "Gemini", description: "Use Gemini API directly (requires GEMINI_API_KEY)" },
426
- { value: "openrouter", label: "OpenRouter", description: "Use OpenRouter (requires OPENROUTER_API_KEY)" },
427
- ],
428
- },
429
- {
430
- id: "kimiApiFormat",
431
- tab: "tools",
432
- type: "submenu",
433
- label: "Kimi API format",
434
- description: "API format for Kimi Code provider",
435
- get: sm => sm.getKimiApiFormat(),
436
- set: (sm, v) => sm.setKimiApiFormat(v as KimiApiFormatOption),
437
- getOptions: () => [
438
- { value: "openai", label: "OpenAI", description: "Use OpenAI-compatible API (api.kimi.com)" },
439
- { value: "anthropic", label: "Anthropic", description: "Use Anthropic-compatible API (api.moonshot.ai)" },
440
- ],
441
- },
162
+ function pathToSettingDef(path: SettingPath): SettingDef | null {
163
+ const ui = getUi(path);
164
+ if (!ui) return null;
442
165
 
443
- // ═══════════════════════════════════════════════════════════════════════════
444
- // Display tab - Visual/UI settings
445
- // ═══════════════════════════════════════════════════════════════════════════
446
- {
447
- id: "theme",
448
- tab: "display",
449
- type: "submenu",
450
- label: "Theme",
451
- description: "Color theme for the interface",
452
- get: sm => sm.getTheme() ?? "dark",
453
- set: (sm, v) => sm.setTheme(v),
454
- getOptions: () => [], // Filled dynamically from context
455
- },
456
- {
457
- id: "symbolPreset",
458
- tab: "display",
459
- type: "submenu",
460
- label: "Symbol preset",
461
- description: "Icon/symbol style (overrides theme default)",
462
- get: sm => sm.getSymbolPreset() ?? "unicode",
463
- set: (sm, v) => sm.setSymbolPreset(v as SymbolPreset),
464
- getOptions: () => [
465
- { value: "unicode", label: "Unicode", description: "Standard Unicode symbols (default)" },
466
- { value: "nerd", label: "Nerd Font", description: "Nerd Font icons (requires Nerd Font)" },
467
- { value: "ascii", label: "ASCII", description: "ASCII-only characters (maximum compatibility)" },
468
- ],
469
- },
470
- {
471
- id: "colorBlindMode",
472
- tab: "display",
473
- type: "boolean",
474
- label: "Color blind mode",
475
- description: "Use blue instead of green for diff additions (red-green color blindness)",
476
- get: sm => sm.getColorBlindMode(),
477
- set: (sm, v) => sm.setColorBlindMode(v),
478
- },
479
- {
480
- id: "thinkingLevel",
481
- tab: "display",
482
- type: "submenu",
483
- label: "Thinking level",
484
- description: "Reasoning depth for thinking-capable models",
485
- get: sm => sm.getDefaultThinkingLevel() ?? "off",
486
- set: (sm, v) => sm.setDefaultThinkingLevel(v as ThinkingLevel),
487
- getOptions: () =>
488
- (["off", "minimal", "low", "medium", "high", "xhigh"] as ThinkingLevel[]).map(level => ({
489
- value: level,
490
- label: level,
491
- description: THINKING_DESCRIPTIONS[level],
492
- })),
493
- },
494
- {
495
- id: "hideThinking",
496
- tab: "display",
497
- type: "boolean",
498
- label: "Hide thinking",
499
- description: "Hide thinking blocks in assistant responses",
500
- get: sm => sm.getHideThinkingBlock(),
501
- set: (sm, v) => sm.setHideThinkingBlock(v),
502
- },
503
- {
504
- id: "showImages",
505
- tab: "display",
506
- type: "boolean",
507
- label: "Show images",
508
- description: "Render images inline in terminal",
509
- get: sm => sm.getShowImages(),
510
- set: (sm, v) => sm.setShowImages(v),
511
- condition: () => !!TERMINAL_INFO.imageProtocol,
512
- },
513
- {
514
- id: "autoResizeImages",
515
- tab: "display",
516
- type: "boolean",
517
- label: "Auto-resize images",
518
- description: "Resize large images to 2000x2000 max for better model compatibility",
519
- get: sm => sm.getImageAutoResize(),
520
- set: (sm, v) => sm.setImageAutoResize(v),
521
- },
522
- {
523
- id: "blockImages",
524
- tab: "display",
525
- type: "boolean",
526
- label: "Block images",
527
- description: "Prevent images from being sent to LLM providers",
528
- get: sm => sm.getBlockImages(),
529
- set: (sm, v) => sm.setBlockImages(v),
530
- },
531
- {
532
- id: "showHardwareCursor",
533
- tab: "display",
534
- type: "boolean",
535
- label: "Hardware cursor",
536
- description: "Show terminal cursor for IME support (default: on for Linux/macOS)",
537
- get: sm => sm.getShowHardwareCursor(),
538
- set: (sm, v) => sm.setShowHardwareCursor(v),
539
- },
166
+ const schemaType = getType(path);
167
+ const base = { path, label: ui.label, description: ui.description, tab: ui.tab };
540
168
 
541
- // ═══════════════════════════════════════════════════════════════════════════
542
- // TTSR tab - Time Traveling Stream Rules
543
- // ═══════════════════════════════════════════════════════════════════════════
544
- {
545
- id: "ttsrEnabled",
546
- tab: "ttsr",
547
- type: "boolean",
548
- label: "TTSR enabled",
549
- description: "Time Traveling Stream Rules: interrupt agent when output matches patterns",
550
- get: sm => sm.getTtsrEnabled(),
551
- set: (sm, v) => sm.setTtsrEnabled(v),
552
- },
553
- {
554
- id: "ttsrContextMode",
555
- tab: "ttsr",
556
- type: "enum",
557
- label: "TTSR context mode",
558
- description: "What to do with partial output when TTSR triggers",
559
- values: ["discard", "keep"],
560
- get: sm => sm.getTtsrContextMode(),
561
- set: (sm, v) => sm.setTtsrContextMode(v as "keep" | "discard"),
562
- },
563
- {
564
- id: "ttsrRepeatMode",
565
- tab: "ttsr",
566
- type: "enum",
567
- label: "TTSR repeat mode",
568
- description: "How rules can repeat: once per session or after a message gap",
569
- values: ["once", "after-gap"],
570
- get: sm => sm.getTtsrRepeatMode(),
571
- set: (sm, v) => sm.setTtsrRepeatMode(v as "once" | "after-gap"),
572
- },
573
- {
574
- id: "ttsrRepeatGap",
575
- tab: "ttsr",
576
- type: "submenu",
577
- label: "TTSR repeat gap",
578
- description: "Messages before a rule can trigger again (when repeat mode is after-gap)",
579
- get: sm => String(sm.getTtsrRepeatGap()),
580
- set: (sm, v) => sm.setTtsrRepeatGap(Number.parseInt(v, 10)),
581
- getOptions: () => [
582
- { value: "5", label: "5 messages" },
583
- { value: "10", label: "10 messages" },
584
- { value: "15", label: "15 messages" },
585
- { value: "20", label: "20 messages" },
586
- { value: "30", label: "30 messages" },
587
- ],
588
- },
169
+ // Check for condition
170
+ const condition = ui.condition ? CONDITIONS[ui.condition] : undefined;
589
171
 
590
- // ═══════════════════════════════════════════════════════════════════════════
591
- // Status tab - Status line configuration
592
- // ═══════════════════════════════════════════════════════════════════════════
593
- {
594
- id: "statusLinePreset",
595
- tab: "status",
596
- type: "submenu",
597
- label: "Preset",
598
- description: "Pre-built status line configurations",
599
- get: sm => sm.getStatusLinePreset(),
600
- set: (sm, v) => sm.setStatusLinePreset(v as StatusLinePreset),
601
- getOptions: () => [
602
- { value: "default", label: "Default", description: "Model, path, git, context, tokens, cost" },
603
- { value: "minimal", label: "Minimal", description: "Path and git only" },
604
- { value: "compact", label: "Compact", description: "Model, git, cost, context" },
605
- { value: "full", label: "Full", description: "All segments including time" },
606
- { value: "nerd", label: "Nerd", description: "Maximum info with Nerd Font icons" },
607
- { value: "ascii", label: "ASCII", description: "No special characters" },
608
- { value: "custom", label: "Custom", description: "User-defined segments" },
609
- ],
610
- },
611
- {
612
- id: "statusLineSeparator",
613
- tab: "status",
614
- type: "submenu",
615
- label: "Separator style",
616
- description: "Style of separators between segments",
617
- get: sm => {
618
- const settings = sm.getStatusLineSettings();
619
- if (settings.separator) return settings.separator;
620
- return getPreset(sm.getStatusLinePreset()).separator;
621
- },
622
- set: (sm, v) => sm.setStatusLineSeparator(v as StatusLineSeparatorStyle),
623
- getOptions: () => [
624
- { value: "powerline", label: "Powerline", description: "Solid arrows (requires Nerd Font)" },
625
- { value: "powerline-thin", label: "Thin chevron", description: "Thin arrows (requires Nerd Font)" },
626
- { value: "slash", label: "Slash", description: "Forward slashes" },
627
- { value: "pipe", label: "Pipe", description: "Vertical pipes" },
628
- { value: "block", label: "Block", description: "Solid blocks" },
629
- { value: "none", label: "None", description: "Space only" },
630
- { value: "ascii", label: "ASCII", description: "Greater-than signs" },
631
- ],
632
- },
633
- {
634
- id: "statusLineShowHooks",
635
- tab: "status",
636
- type: "boolean",
637
- label: "Show extension status",
638
- description: "Display hook status messages below status line",
639
- get: sm => sm.getStatusLineShowHookStatus(),
640
- set: (sm, v) => sm.setStatusLineShowHookStatus(v),
641
- },
642
- {
643
- id: "statusLineSegments",
644
- tab: "status",
645
- type: "submenu",
646
- label: "Configure segments",
647
- description: "Choose and arrange status line segments",
648
- get: () => "configure...",
649
- set: () => {},
650
- getOptions: () => [{ value: "open", label: "Open segment editor..." }],
651
- },
652
- {
653
- id: "statusLineModelThinking",
654
- tab: "status",
655
- type: "enum",
656
- label: "Model thinking level",
657
- description: "Show thinking level in the model segment",
658
- values: ["default", "on", "off"],
659
- get: sm => {
660
- const value = sm.getStatusLineSegmentOptions().model?.showThinkingLevel;
661
- if (value === undefined) return "default";
662
- return value ? "on" : "off";
663
- },
664
- set: (sm, v) => {
665
- if (v === "default") {
666
- sm.clearStatusLineSegmentOption("model", "showThinkingLevel");
667
- } else {
668
- sm.setStatusLineSegmentOption("model", "showThinkingLevel", v === "on");
669
- }
670
- },
671
- },
672
- {
673
- id: "statusLinePathAbbreviate",
674
- tab: "status",
675
- type: "enum",
676
- label: "Path abbreviate",
677
- description: "Use ~ and strip home prefix in path segment",
678
- values: ["default", "on", "off"],
679
- get: sm => {
680
- const value = sm.getStatusLineSegmentOptions().path?.abbreviate;
681
- if (value === undefined) return "default";
682
- return value ? "on" : "off";
683
- },
684
- set: (sm, v) => {
685
- if (v === "default") {
686
- sm.clearStatusLineSegmentOption("path", "abbreviate");
687
- } else {
688
- sm.setStatusLineSegmentOption("path", "abbreviate", v === "on");
689
- }
690
- },
691
- },
692
- {
693
- id: "statusLinePathMaxLength",
694
- tab: "status",
695
- type: "submenu",
696
- label: "Path max length",
697
- description: "Maximum length for displayed path",
698
- get: sm => {
699
- const value = sm.getStatusLineSegmentOptions().path?.maxLength;
700
- return typeof value === "number" ? String(value) : "default";
701
- },
702
- set: (sm, v) => {
703
- if (v === "default") {
704
- sm.clearStatusLineSegmentOption("path", "maxLength");
705
- } else {
706
- sm.setStatusLineSegmentOption("path", "maxLength", Number.parseInt(v, 10));
707
- }
708
- },
709
- getOptions: () => [
710
- { value: "default", label: "Preset default" },
711
- { value: "20", label: "20" },
712
- { value: "30", label: "30" },
713
- { value: "40", label: "40" },
714
- { value: "50", label: "50" },
715
- { value: "60", label: "60" },
716
- { value: "80", label: "80" },
717
- ],
718
- },
719
- {
720
- id: "statusLinePathStripWorkPrefix",
721
- tab: "status",
722
- type: "enum",
723
- label: "Path strip /work",
724
- description: "Strip /work prefix in path segment",
725
- values: ["default", "on", "off"],
726
- get: sm => {
727
- const value = sm.getStatusLineSegmentOptions().path?.stripWorkPrefix;
728
- if (value === undefined) return "default";
729
- return value ? "on" : "off";
730
- },
731
- set: (sm, v) => {
732
- if (v === "default") {
733
- sm.clearStatusLineSegmentOption("path", "stripWorkPrefix");
734
- } else {
735
- sm.setStatusLineSegmentOption("path", "stripWorkPrefix", v === "on");
736
- }
737
- },
738
- },
739
- {
740
- id: "statusLineGitShowBranch",
741
- tab: "status",
742
- type: "enum",
743
- label: "Git show branch",
744
- description: "Show branch name in git segment",
745
- values: ["default", "on", "off"],
746
- get: sm => {
747
- const value = sm.getStatusLineSegmentOptions().git?.showBranch;
748
- if (value === undefined) return "default";
749
- return value ? "on" : "off";
750
- },
751
- set: (sm, v) => {
752
- if (v === "default") {
753
- sm.clearStatusLineSegmentOption("git", "showBranch");
754
- } else {
755
- sm.setStatusLineSegmentOption("git", "showBranch", v === "on");
756
- }
757
- },
758
- },
759
- {
760
- id: "statusLineGitShowStaged",
761
- tab: "status",
762
- type: "enum",
763
- label: "Git show staged",
764
- description: "Show staged file count in git segment",
765
- values: ["default", "on", "off"],
766
- get: sm => {
767
- const value = sm.getStatusLineSegmentOptions().git?.showStaged;
768
- if (value === undefined) return "default";
769
- return value ? "on" : "off";
770
- },
771
- set: (sm, v) => {
772
- if (v === "default") {
773
- sm.clearStatusLineSegmentOption("git", "showStaged");
774
- } else {
775
- sm.setStatusLineSegmentOption("git", "showStaged", v === "on");
776
- }
777
- },
778
- },
779
- {
780
- id: "statusLineGitShowUnstaged",
781
- tab: "status",
782
- type: "enum",
783
- label: "Git show unstaged",
784
- description: "Show unstaged file count in git segment",
785
- values: ["default", "on", "off"],
786
- get: sm => {
787
- const value = sm.getStatusLineSegmentOptions().git?.showUnstaged;
788
- if (value === undefined) return "default";
789
- return value ? "on" : "off";
790
- },
791
- set: (sm, v) => {
792
- if (v === "default") {
793
- sm.clearStatusLineSegmentOption("git", "showUnstaged");
794
- } else {
795
- sm.setStatusLineSegmentOption("git", "showUnstaged", v === "on");
796
- }
797
- },
798
- },
799
- {
800
- id: "statusLineGitShowUntracked",
801
- tab: "status",
802
- type: "enum",
803
- label: "Git show untracked",
804
- description: "Show untracked file count in git segment",
805
- values: ["default", "on", "off"],
806
- get: sm => {
807
- const value = sm.getStatusLineSegmentOptions().git?.showUntracked;
808
- if (value === undefined) return "default";
809
- return value ? "on" : "off";
810
- },
811
- set: (sm, v) => {
812
- if (v === "default") {
813
- sm.clearStatusLineSegmentOption("git", "showUntracked");
814
- } else {
815
- sm.setStatusLineSegmentOption("git", "showUntracked", v === "on");
816
- }
817
- },
818
- },
819
- {
820
- id: "statusLineTimeFormat",
821
- tab: "status",
822
- type: "enum",
823
- label: "Time format",
824
- description: "Clock segment time format",
825
- values: ["default", "12h", "24h"],
826
- get: sm => sm.getStatusLineSegmentOptions().time?.format ?? "default",
827
- set: (sm, v) => {
828
- if (v === "default") {
829
- sm.clearStatusLineSegmentOption("time", "format");
830
- } else {
831
- sm.setStatusLineSegmentOption("time", "format", v);
832
- }
833
- },
834
- },
835
- {
836
- id: "statusLineTimeShowSeconds",
837
- tab: "status",
838
- type: "enum",
839
- label: "Time show seconds",
840
- description: "Include seconds in clock segment",
841
- values: ["default", "on", "off"],
842
- get: sm => {
843
- const value = sm.getStatusLineSegmentOptions().time?.showSeconds;
844
- if (value === undefined) return "default";
845
- return value ? "on" : "off";
846
- },
847
- set: (sm, v) => {
848
- if (v === "default") {
849
- sm.clearStatusLineSegmentOption("time", "showSeconds");
850
- } else {
851
- sm.setStatusLineSegmentOption("time", "showSeconds", v === "on");
852
- }
853
- },
854
- },
172
+ if (schemaType === "boolean") {
173
+ return { ...base, type: "boolean", condition };
174
+ }
855
175
 
856
- // ═══════════════════════════════════════════════════════════════════════════
857
- // LSP tab - LSP integration settings
858
- // ═══════════════════════════════════════════════════════════════════════════
859
- {
860
- id: "lspFormatOnWrite",
861
- tab: "lsp",
862
- type: "boolean",
863
- label: "Format on write",
864
- description: "Automatically format code files using LSP after writing",
865
- get: sm => sm.getLspFormatOnWrite(),
866
- set: (sm, v) => sm.setLspFormatOnWrite(v),
867
- },
868
- {
869
- id: "lspDiagnosticsOnWrite",
870
- tab: "lsp",
871
- type: "boolean",
872
- label: "Diagnostics on write",
873
- description: "Return LSP diagnostics (errors/warnings) after writing code files",
874
- get: sm => sm.getLspDiagnosticsOnWrite(),
875
- set: (sm, v) => sm.setLspDiagnosticsOnWrite(v),
876
- },
877
- {
878
- id: "lspDiagnosticsOnEdit",
879
- tab: "lsp",
880
- type: "boolean",
881
- label: "Diagnostics on edit",
882
- description: "Return LSP diagnostics (errors/warnings) after editing code files",
883
- get: sm => sm.getLspDiagnosticsOnEdit(),
884
- set: (sm, v) => sm.setLspDiagnosticsOnEdit(v),
885
- },
176
+ if (schemaType === "enum") {
177
+ const values = getEnumValues(path) ?? [];
886
178
 
887
- // ═══════════════════════════════════════════════════════════════════════════
888
- // Exa tab - Exa search tool settings
889
- // ═══════════════════════════════════════════════════════════════════════════
890
- {
891
- id: "exaEnabled",
892
- tab: "exa",
893
- type: "boolean",
894
- label: "Exa enabled",
895
- description: "Master toggle for all Exa search tools",
896
- get: sm => sm.getExaSettings().enabled,
897
- set: (sm, v) => sm.setExaEnabled(v),
898
- },
899
- {
900
- id: "exaSearch",
901
- tab: "exa",
902
- type: "boolean",
903
- label: "Exa search",
904
- description: "Basic search, deep search, code search, crawl",
905
- get: sm => sm.getExaSettings().enableSearch,
906
- set: (sm, v) => sm.setExaSearchEnabled(v),
907
- },
908
- {
909
- id: "exaLinkedin",
910
- tab: "exa",
911
- type: "boolean",
912
- label: "Exa LinkedIn",
913
- description: "Search LinkedIn for people and companies",
914
- get: sm => sm.getExaSettings().enableLinkedin,
915
- set: (sm, v) => sm.setExaLinkedinEnabled(v),
916
- },
917
- {
918
- id: "exaCompany",
919
- tab: "exa",
920
- type: "boolean",
921
- label: "Exa company",
922
- description: "Comprehensive company research tool",
923
- get: sm => sm.getExaSettings().enableCompany,
924
- set: (sm, v) => sm.setExaCompanyEnabled(v),
925
- },
926
- {
927
- id: "exaResearcher",
928
- tab: "exa",
929
- type: "boolean",
930
- label: "Exa researcher",
931
- description: "AI-powered deep research tasks",
932
- get: sm => sm.getExaSettings().enableResearcher,
933
- set: (sm, v) => sm.setExaResearcherEnabled(v),
934
- },
935
- {
936
- id: "exaWebsets",
937
- tab: "exa",
938
- type: "boolean",
939
- label: "Exa websets",
940
- description: "Webset management and enrichment tools",
941
- get: sm => sm.getExaSettings().enableWebsets,
942
- set: (sm, v) => sm.setExaWebsetsEnabled(v),
943
- },
944
- ];
179
+ // If marked as submenu, use submenu type
180
+ if (ui.submenu) {
181
+ const provider = OPTION_PROVIDERS[path];
182
+ return {
183
+ ...base,
184
+ type: "submenu",
185
+ getOptions:
186
+ provider ??
187
+ (() =>
188
+ values.map(v => ({
189
+ value: v,
190
+ label: v,
191
+ description: path === "defaultThinkingLevel" ? THINKING_DESCRIPTIONS[v] : undefined,
192
+ }))),
193
+ };
194
+ }
945
195
 
946
- /**
947
- * All settings. Discovery settings have been moved to /extensions dashboard.
948
- */
949
- function getAllSettings(): SettingDef[] {
950
- return SETTINGS_DEFS;
196
+ return { ...base, type: "enum", values };
197
+ }
198
+
199
+ if (schemaType === "number" && ui.submenu) {
200
+ const provider = OPTION_PROVIDERS[path];
201
+ if (provider) {
202
+ return { ...base, type: "submenu", getOptions: provider };
203
+ }
204
+ }
205
+
206
+ if (schemaType === "string" && ui.submenu) {
207
+ const provider = OPTION_PROVIDERS[path];
208
+ if (provider) {
209
+ return { ...base, type: "submenu", getOptions: provider };
210
+ }
211
+ // For theme etc, options will be injected at runtime
212
+ return { ...base, type: "submenu", getOptions: () => [] };
213
+ }
214
+
215
+ return null;
216
+ }
217
+
218
+ // ═══════════════════════════════════════════════════════════════════════════
219
+ // Public API
220
+ // ═══════════════════════════════════════════════════════════════════════════
221
+
222
+ /** Cache of generated definitions */
223
+ let cachedDefs: SettingDef[] | null = null;
224
+
225
+ /** Get all setting definitions with UI */
226
+ export function getAllSettingDefs(): SettingDef[] {
227
+ if (cachedDefs) return cachedDefs;
228
+
229
+ const defs: SettingDef[] = [];
230
+ for (const tab of ["behavior", "tools", "bash", "display", "ttsr", "status", "lsp", "exa"] as SettingTab[]) {
231
+ for (const path of getPathsForTab(tab)) {
232
+ const def = pathToSettingDef(path);
233
+ if (def) defs.push(def);
234
+ }
235
+ }
236
+ cachedDefs = defs;
237
+ return defs;
951
238
  }
952
239
 
953
240
  /** Get settings for a specific tab */
954
- export function getSettingsForTab(tab: string): SettingDef[] {
955
- return getAllSettings().filter(def => def.tab === tab);
241
+ export function getSettingsForTab(tab: SettingTab): SettingDef[] {
242
+ return getAllSettingDefs().filter(def => def.tab === tab);
243
+ }
244
+
245
+ /** Get a setting definition by path */
246
+ export function getSettingDef(path: SettingPath): SettingDef | undefined {
247
+ return getAllSettingDefs().find(def => def.path === path);
956
248
  }
957
249
 
958
- /** Get a setting definition by id */
959
- export function getSettingDef(id: string): SettingDef | undefined {
960
- return getAllSettings().find(def => def.id === id);
250
+ /** Get default value for display */
251
+ export function getDisplayDefault(path: SettingPath): string {
252
+ const value = getDefault(path);
253
+ if (value === undefined) return "";
254
+ if (typeof value === "boolean") return value ? "true" : "false";
255
+ return String(value);
961
256
  }