@oh-my-pi/pi-coding-agent 14.6.2 → 14.6.3

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 (58) hide show
  1. package/CHANGELOG.md +71 -2
  2. package/README.md +21 -0
  3. package/package.json +23 -7
  4. package/src/cli/grievances-cli.ts +89 -4
  5. package/src/commands/grievances.ts +33 -7
  6. package/src/config/prompt-templates.ts +14 -7
  7. package/src/config/settings-schema.ts +585 -100
  8. package/src/config/settings.ts +42 -0
  9. package/src/discovery/helpers.ts +13 -6
  10. package/src/edit/index.ts +3 -3
  11. package/src/edit/line-hash.ts +73 -25
  12. package/src/edit/modes/hashline.lark +10 -3
  13. package/src/edit/modes/hashline.ts +104 -38
  14. package/src/edit/renderer.ts +3 -3
  15. package/src/hindsight/backend.ts +444 -0
  16. package/src/hindsight/bank.ts +131 -0
  17. package/src/hindsight/client.ts +445 -0
  18. package/src/hindsight/config.ts +165 -0
  19. package/src/hindsight/content.ts +205 -0
  20. package/src/hindsight/index.ts +6 -0
  21. package/src/hindsight/retain-queue.ts +166 -0
  22. package/src/hindsight/transcript.ts +71 -0
  23. package/src/main.ts +7 -10
  24. package/src/memories/index.ts +1 -1
  25. package/src/memory-backend/index.ts +4 -0
  26. package/src/memory-backend/local-backend.ts +30 -0
  27. package/src/memory-backend/off-backend.ts +16 -0
  28. package/src/memory-backend/resolve.ts +24 -0
  29. package/src/memory-backend/types.ts +69 -0
  30. package/src/modes/components/settings-defs.ts +50 -451
  31. package/src/modes/components/settings-selector.ts +2 -2
  32. package/src/modes/components/status-line/presets.ts +1 -1
  33. package/src/modes/controllers/command-controller.ts +6 -5
  34. package/src/modes/controllers/event-controller.ts +12 -0
  35. package/src/modes/controllers/selector-controller.ts +3 -12
  36. package/src/modes/theme/theme.ts +4 -0
  37. package/src/prompts/tools/github.md +3 -0
  38. package/src/prompts/tools/hashline.md +20 -16
  39. package/src/prompts/tools/read.md +10 -6
  40. package/src/prompts/tools/recall.md +5 -0
  41. package/src/prompts/tools/reflect.md +5 -0
  42. package/src/prompts/tools/retain.md +5 -0
  43. package/src/prompts/tools/search.md +1 -1
  44. package/src/sdk.ts +12 -9
  45. package/src/session/agent-session.ts +75 -3
  46. package/src/slash-commands/builtin-registry.ts +2 -12
  47. package/src/tools/ast-edit.ts +14 -5
  48. package/src/tools/ast-grep.ts +12 -3
  49. package/src/tools/find.ts +47 -7
  50. package/src/tools/gh-renderer.ts +10 -1
  51. package/src/tools/gh.ts +233 -5
  52. package/src/tools/hindsight-recall.ts +70 -0
  53. package/src/tools/hindsight-reflect.ts +57 -0
  54. package/src/tools/hindsight-retain.ts +63 -0
  55. package/src/tools/index.ts +17 -0
  56. package/src/tools/path-utils.ts +55 -0
  57. package/src/tools/read.ts +1 -1
  58. package/src/tools/search.ts +45 -8
@@ -1,15 +1,17 @@
1
1
  /**
2
- * Declarative settings definitions for the UI.
2
+ * UI adapter over the schema. Reads `ui.options` declared inline in
3
+ * settings-schema.ts and produces typed widget definitions for the
4
+ * settings selector.
3
5
  *
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
6
+ * To add a new setting to the UI: declare it in `settings-schema.ts`
7
+ * with a `ui` block. If it needs a submenu, include `options: [...]`
8
+ * (or `options: "runtime"` for runtime-injected lists like themes).
8
9
  */
9
10
 
10
- import { THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
11
11
  import { TERMINAL } from "@oh-my-pi/pi-tui";
12
+ import { Settings } from "../../config/settings";
12
13
  import {
14
+ type AnyUiMetadata,
13
15
  getDefault,
14
16
  getEnumValues,
15
17
  getPathsForTab,
@@ -18,8 +20,8 @@ import {
18
20
  SETTING_TABS,
19
21
  type SettingPath,
20
22
  type SettingTab,
23
+ type SubmenuOption,
21
24
  } from "../../config/settings-schema";
22
- import { getThinkingLevelMetadata } from "../../thinking";
23
25
 
24
26
  // ═══════════════════════════════════════════════════════════════════════════
25
27
  // UI Definition Types
@@ -32,11 +34,16 @@ interface BaseSettingDef {
32
34
  label: string;
33
35
  description: string;
34
36
  tab: SettingTab;
37
+ /**
38
+ * Optional visibility predicate. When supplied and returning false, the
39
+ * setting is hidden from the UI. Applies to every variant — booleans,
40
+ * enums, submenus, and text inputs.
41
+ */
42
+ condition?: () => boolean;
35
43
  }
36
44
 
37
45
  export interface BooleanSettingDef extends BaseSettingDef {
38
46
  type: "boolean";
39
- condition?: () => boolean;
40
47
  }
41
48
 
42
49
  export interface EnumSettingDef extends BaseSettingDef {
@@ -44,9 +51,11 @@ export interface EnumSettingDef extends BaseSettingDef {
44
51
  values: readonly string[];
45
52
  }
46
53
 
54
+ type OptionList = ReadonlyArray<SubmenuOption>;
55
+
47
56
  export interface SubmenuSettingDef extends BaseSettingDef {
48
57
  type: "submenu";
49
- get options(): OptionList;
58
+ options: OptionList;
50
59
  onPreview?: (value: string) => void;
51
60
  onPreviewCancel?: (originalValue: string) => void;
52
61
  }
@@ -63,472 +72,62 @@ export type SettingDef = BooleanSettingDef | EnumSettingDef | SubmenuSettingDef
63
72
 
64
73
  const CONDITIONS: Record<string, () => boolean> = {
65
74
  hasImageProtocol: () => !!TERMINAL.imageProtocol,
75
+ hindsightActive: () => {
76
+ try {
77
+ return Settings.instance.get("memory.backend") === "hindsight";
78
+ } catch {
79
+ return false;
80
+ }
81
+ },
66
82
  };
67
83
 
68
84
  // ═══════════════════════════════════════════════════════════════════════════
69
- // Submenu Option Providers
85
+ // Schema to UI Conversion
70
86
  // ═══════════════════════════════════════════════════════════════════════════
71
87
 
72
- type OptionList = ReadonlyArray<{ value: string; label: string; description?: string }>;
73
- type OptionProvider = (() => OptionList) | OptionList;
74
-
75
- const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
76
- // Context maintenance strategy
77
- "compaction.strategy": [
78
- { value: "context-full", label: "Context-full", description: "Summarize in-place and keep the current session" },
79
- { value: "handoff", label: "Handoff", description: "Generate handoff and continue in a new session" },
80
- {
81
- value: "off",
82
- label: "Off",
83
- description: "Disable automatic context maintenance (same behavior as Auto-compact off)",
84
- },
85
- ],
86
- // Context maintenance threshold
87
- "compaction.thresholdPercent": [
88
- { value: "default", label: "Default", description: "Legacy reserve-based threshold" },
89
- { value: "10", label: "10%", description: "Extremely early maintenance" },
90
- { value: "20", label: "20%", description: "Very early maintenance" },
91
- { value: "30", label: "30%", description: "Early maintenance" },
92
- { value: "40", label: "40%", description: "Moderately early maintenance" },
93
- { value: "50", label: "50%", description: "Halfway point" },
94
- { value: "60", label: "60%", description: "Moderate context usage" },
95
- { value: "70", label: "70%", description: "Balanced" },
96
- { value: "75", label: "75%", description: "Slightly aggressive" },
97
- { value: "80", label: "80%", description: "Typical threshold" },
98
- { value: "85", label: "85%", description: "Aggressive context usage" },
99
- { value: "90", label: "90%", description: "Very aggressive" },
100
- { value: "95", label: "95%", description: "Near context limit" },
101
- ],
102
- "compaction.thresholdTokens": [
103
- { value: "default", label: "Default", description: "Use percentage-based threshold" },
104
- { value: "25000", label: "25K tokens", description: "Quarter of a 200K window" },
105
- { value: "50000", label: "50K tokens", description: "Half of a 200K window" },
106
- { value: "100000", label: "100K tokens", description: "Half of a 200K window" },
107
- { value: "150000", label: "150K tokens", description: "Three-quarters of a 200K window" },
108
- { value: "200000", label: "200K tokens", description: "Full standard context window" },
109
- { value: "300000", label: "300K tokens", description: "Large context window" },
110
- { value: "500000", label: "500K tokens", description: "Very large context window" },
111
- ],
112
- "compaction.idleThresholdTokens": [
113
- { value: "100000", label: "100K tokens" },
114
- { value: "200000", label: "200K tokens" },
115
- { value: "300000", label: "300K tokens" },
116
- { value: "400000", label: "400K tokens" },
117
- { value: "500000", label: "500K tokens" },
118
- { value: "600000", label: "600K tokens" },
119
- { value: "700000", label: "700K tokens" },
120
- { value: "800000", label: "800K tokens" },
121
- { value: "900000", label: "900K tokens" },
122
- ],
123
- "compaction.idleTimeoutSeconds": [
124
- { value: "60", label: "1 minute" },
125
- { value: "120", label: "2 minutes" },
126
- { value: "300", label: "5 minutes" },
127
- { value: "600", label: "10 minutes" },
128
- { value: "1800", label: "30 minutes" },
129
- { value: "3600", label: "1 hour" },
130
- ],
131
- // Retry max retries
132
- "retry.maxRetries": [
133
- { value: "1", label: "1 retry" },
134
- { value: "2", label: "2 retries" },
135
- { value: "3", label: "3 retries" },
136
- { value: "5", label: "5 retries" },
137
- { value: "10", label: "10 retries" },
138
- ],
139
- // Retry fallback revert policy
140
- "retry.fallbackRevertPolicy": [
141
- {
142
- value: "cooldown-expiry",
143
- label: "Cooldown expiry",
144
- description: "Return to the primary model after its suppression window ends",
145
- },
146
- { value: "never", label: "Never", description: "Stay on the fallback model until manually changed" },
147
- ],
148
- // Task input mode
149
- "task.simple": [
150
- {
151
- value: "default",
152
- label: "Default",
153
- description: "Shared context and custom task schema are available",
154
- },
155
- {
156
- value: "schema-free",
157
- label: "Schema-free",
158
- description: "Shared context stays available, but custom task schema is disabled",
159
- },
160
- {
161
- value: "independent",
162
- label: "Independent",
163
- description: "No shared context or custom task schema; each task must stand alone",
164
- },
165
- ],
166
- // Task max concurrency
167
- "task.maxConcurrency": [
168
- { value: "0", label: "Unlimited" },
169
- { value: "1", label: "1 task" },
170
- { value: "2", label: "2 tasks" },
171
- { value: "4", label: "4 tasks" },
172
- { value: "8", label: "8 tasks" },
173
- { value: "16", label: "16 tasks" },
174
- { value: "32", label: "32 tasks" },
175
- { value: "64", label: "64 tasks" },
176
- ],
177
- // Task max recursion depth
178
- "task.maxRecursionDepth": [
179
- { value: "-1", label: "Unlimited" },
180
- { value: "0", label: "None" },
181
- { value: "1", label: "Single" },
182
- { value: "2", label: "Double" },
183
- { value: "3", label: "Triple" },
184
- ],
185
- // Task isolation mode
186
- "task.isolation.mode": [
187
- { value: "none", label: "None", description: "No isolation" },
188
- { value: "worktree", label: "Worktree", description: "Git worktree isolation" },
189
- {
190
- value: "fuse-overlay",
191
- label: "Fuse Overlay",
192
- description: "COW overlay via fuse-overlayfs (Unix only)",
193
- },
194
- {
195
- value: "fuse-projfs",
196
- label: "Fuse ProjFS",
197
- description: "COW overlay via ProjFS (Windows only; falls back to worktree if unavailable)",
198
- },
199
- ],
200
- // Task isolation merge strategy
201
- "task.isolation.merge": [
202
- { value: "patch", label: "Patch", description: "Combine diffs and git apply" },
203
- { value: "branch", label: "Branch", description: "Commit per task, merge with --no-ff" },
204
- ],
205
- // Task isolation commit messages
206
- "task.isolation.commits": [
207
- { value: "generic", label: "Generic", description: "Static commit message" },
208
- { value: "ai", label: "AI", description: "AI-generated commit message from diff" },
209
- ],
210
- // Todo max reminders
211
- "todo.reminders.max": [
212
- { value: "1", label: "1 reminder" },
213
- { value: "2", label: "2 reminders" },
214
- { value: "3", label: "3 reminders" },
215
- { value: "5", label: "5 reminders" },
216
- ],
217
- // Search context
218
- "search.contextBefore": [
219
- { value: "0", label: "0 lines" },
220
- { value: "1", label: "1 line" },
221
- { value: "2", label: "2 lines" },
222
- { value: "3", label: "3 lines" },
223
- { value: "5", label: "5 lines" },
224
- ],
225
- "search.contextAfter": [
226
- { value: "0", label: "0 lines" },
227
- { value: "1", label: "1 line" },
228
- { value: "2", label: "2 lines" },
229
- { value: "3", label: "3 lines" },
230
- { value: "5", label: "5 lines" },
231
- { value: "10", label: "10 lines" },
232
- ],
233
- // Autocomplete max visible
234
- autocompleteMaxVisible: [
235
- { value: "3", label: "3 items" },
236
- { value: "5", label: "5 items" },
237
- { value: "7", label: "7 items" },
238
- { value: "10", label: "10 items" },
239
- { value: "15", label: "15 items" },
240
- { value: "20", label: "20 items" },
241
- ],
242
- // Ask timeout
243
- "ask.timeout": [
244
- { value: "0", label: "Disabled" },
245
- { value: "15", label: "15 seconds" },
246
- { value: "30", label: "30 seconds" },
247
- { value: "60", label: "60 seconds" },
248
- { value: "120", label: "120 seconds" },
249
- ],
250
- // Global tool timeout ceiling
251
- "tools.maxTimeout": [
252
- { value: "0", label: "No limit" },
253
- { value: "30", label: "30 seconds" },
254
- { value: "60", label: "60 seconds" },
255
- { value: "120", label: "120 seconds" },
256
- { value: "300", label: "5 minutes" },
257
- { value: "600", label: "10 minutes" },
258
- ],
259
- // Artifact spill settings
260
- "tools.artifactSpillThreshold": [
261
- { value: "1", label: "1 KB", description: "~250 tokens" },
262
- { value: "2.5", label: "2.5 KB", description: "~625 tokens" },
263
- { value: "5", label: "5 KB", description: "~1.25K tokens" },
264
- { value: "10", label: "10 KB", description: "~2.5K tokens" },
265
- { value: "20", label: "20 KB", description: "~5K tokens" },
266
- { value: "30", label: "30 KB", description: "~7.5K tokens" },
267
- { value: "50", label: "50 KB", description: "Default; ~12.5K tokens" },
268
- { value: "75", label: "75 KB", description: "~19K tokens" },
269
- { value: "100", label: "100 KB", description: "~25K tokens" },
270
- { value: "200", label: "200 KB", description: "~50K tokens" },
271
- { value: "500", label: "500 KB", description: "~125K tokens" },
272
- { value: "1000", label: "1 MB", description: "~250K tokens" },
273
- ],
274
- "tools.artifactTailBytes": [
275
- { value: "1", label: "1 KB", description: "~250 tokens" },
276
- { value: "2.5", label: "2.5 KB", description: "~625 tokens" },
277
- { value: "5", label: "5 KB", description: "~1.25K tokens" },
278
- { value: "10", label: "10 KB", description: "~2.5K tokens" },
279
- { value: "20", label: "20 KB", description: "Default; ~5K tokens" },
280
- { value: "50", label: "50 KB", description: "~12.5K tokens" },
281
- { value: "100", label: "100 KB", description: "~25K tokens" },
282
- { value: "200", label: "200 KB", description: "~50K tokens" },
283
- ],
284
- "tools.artifactTailLines": [
285
- { value: "50", label: "50 lines", description: "~250 tokens" },
286
- { value: "100", label: "100 lines", description: "~500 tokens" },
287
- { value: "250", label: "250 lines", description: "~1.25K tokens" },
288
- { value: "500", label: "500 lines", description: "Default; ~2.5K tokens" },
289
- { value: "1000", label: "1000 lines", description: "~5K tokens" },
290
- { value: "2000", label: "2000 lines", description: "~10K tokens" },
291
- { value: "5000", label: "5000 lines", description: "~25K tokens" },
292
- ],
293
- // Read line limit
294
- "read.defaultLimit": [
295
- { value: "200", label: "200 lines" },
296
- { value: "300", label: "300 lines" },
297
- { value: "500", label: "500 lines" },
298
- { value: "1000", label: "1000 lines" },
299
- { value: "5000", label: "5000 lines" },
300
- ],
301
- // Todo auto-clear delay
302
- "tasks.todoClearDelay": [
303
- { value: "0", label: "Instant" },
304
- { value: "60", label: "1 minute", description: "Default" },
305
- { value: "300", label: "5 minutes" },
306
- { value: "900", label: "15 minutes" },
307
- { value: "1800", label: "30 minutes" },
308
- { value: "3600", label: "1 hour" },
309
- { value: "-1", label: "Never" },
310
- ],
311
-
312
- // Edit fuzzy threshold
313
- "edit.fuzzyThreshold": [
314
- { value: "0.85", label: "0.85", description: "Lenient" },
315
- { value: "0.90", label: "0.90", description: "Moderate" },
316
- { value: "0.95", label: "0.95", description: "Default" },
317
- { value: "0.98", label: "0.98", description: "Strict" },
318
- ],
319
- // TTSR repeat gap
320
- "ttsr.repeatGap": [
321
- { value: "5", label: "5 messages" },
322
- { value: "10", label: "10 messages" },
323
- { value: "15", label: "15 messages" },
324
- { value: "20", label: "20 messages" },
325
- { value: "30", label: "30 messages" },
326
- ],
327
- "ttsr.interruptMode": [
328
- { value: "always", label: "always", description: "Interrupt on prose and tool streams" },
329
- { value: "prose-only", label: "prose-only", description: "Interrupt only on reply/thinking matches" },
330
- { value: "tool-only", label: "tool-only", description: "Interrupt only on tool-call argument matches" },
331
- { value: "never", label: "never", description: "Never interrupt; inject warning after completion" },
332
- ],
333
- // Provider options
334
- "providers.webSearch": [
335
- {
336
- value: "auto",
337
- label: "Auto",
338
- description: "Preferred web-search provider",
339
- },
340
- { value: "exa", label: "Exa", description: "Uses Exa API when EXA_API_KEY is set; falls back to Exa MCP" },
341
- { value: "brave", label: "Brave", description: "Requires BRAVE_API_KEY" },
342
- { value: "jina", label: "Jina", description: "Requires JINA_API_KEY" },
343
- { value: "kimi", label: "Kimi", description: "Requires MOONSHOT_SEARCH_API_KEY or MOONSHOT_API_KEY" },
344
- { value: "perplexity", label: "Perplexity", description: "Requires PERPLEXITY_COOKIES or PERPLEXITY_API_KEY" },
345
- { value: "anthropic", label: "Anthropic", description: "Uses Anthropic web search" },
346
- { value: "zai", label: "Z.AI", description: "Calls Z.AI webSearchPrime MCP" },
347
- { value: "tavily", label: "Tavily", description: "Requires TAVILY_API_KEY" },
348
- { value: "kagi", label: "Kagi", description: "Requires KAGI_API_KEY and Kagi Search API beta access" },
349
- { value: "synthetic", label: "Synthetic", description: "Requires SYNTHETIC_API_KEY" },
350
- { value: "parallel", label: "Parallel", description: "Requires PARALLEL_API_KEY" },
351
- { value: "searxng", label: "SearXNG", description: "Requires searxng.endpoint" },
352
- ],
353
- "providers.image": [
354
- {
355
- value: "auto",
356
- label: "Auto",
357
- description: "Priority: GPT model image tool > Antigravity > OpenRouter > Gemini",
358
- },
359
- { value: "openai", label: "OpenAI", description: "Uses the active GPT Responses/Codex model" },
360
- { value: "gemini", label: "Gemini", description: "Requires GEMINI_API_KEY" },
361
- { value: "openrouter", label: "OpenRouter", description: "Requires OPENROUTER_API_KEY" },
362
- ],
363
- "providers.kimiApiFormat": [
364
- { value: "openai", label: "OpenAI", description: "api.kimi.com" },
365
- { value: "anthropic", label: "Anthropic", description: "api.moonshot.ai" },
366
- ],
367
- "providers.openaiWebsockets": [
368
- { value: "auto", label: "Auto", description: "Use model/provider default websocket behavior" },
369
- { value: "off", label: "Off", description: "Disable websockets for OpenAI Codex models" },
370
- { value: "on", label: "On", description: "Force websockets for OpenAI Codex models" },
371
- ],
372
- // Default thinking level
373
- defaultThinkingLevel: [...THINKING_EFFORTS.map(getThinkingLevelMetadata)],
374
- // Temperature
375
- temperature: [
376
- { value: "-1", label: "Default", description: "Use provider default" },
377
- { value: "0", label: "0", description: "Deterministic" },
378
- { value: "0.2", label: "0.2", description: "Focused" },
379
- { value: "0.5", label: "0.5", description: "Balanced" },
380
- { value: "0.7", label: "0.7", description: "Creative" },
381
- { value: "1", label: "1", description: "Maximum variety" },
382
- ],
383
- topP: [
384
- { value: "-1", label: "Default", description: "Use provider default" },
385
- { value: "0.1", label: "0.1", description: "Very focused" },
386
- { value: "0.3", label: "0.3", description: "Focused" },
387
- { value: "0.5", label: "0.5", description: "Balanced" },
388
- { value: "0.9", label: "0.9", description: "Broad" },
389
- { value: "1", label: "1", description: "No nucleus filtering" },
390
- ],
391
- topK: [
392
- { value: "-1", label: "Default", description: "Use provider default" },
393
- { value: "1", label: "1", description: "Greedy top token" },
394
- { value: "20", label: "20", description: "Focused" },
395
- { value: "40", label: "40", description: "Balanced" },
396
- { value: "100", label: "100", description: "Broad" },
397
- ],
398
- minP: [
399
- { value: "-1", label: "Default", description: "Use provider default" },
400
- { value: "0.01", label: "0.01", description: "Very permissive" },
401
- { value: "0.05", label: "0.05", description: "Balanced" },
402
- { value: "0.1", label: "0.1", description: "Strict" },
403
- ],
404
- presencePenalty: [
405
- { value: "-1", label: "Default", description: "Use provider default" },
406
- { value: "0", label: "0", description: "No penalty" },
407
- { value: "0.5", label: "0.5", description: "Mild novelty" },
408
- { value: "1", label: "1", description: "Encourage novelty" },
409
- { value: "2", label: "2", description: "Strong novelty" },
410
- ],
411
- repetitionPenalty: [
412
- { value: "-1", label: "Default", description: "Use provider default" },
413
- { value: "0.8", label: "0.8", description: "Allow repetition" },
414
- { value: "1", label: "1", description: "No penalty" },
415
- { value: "1.1", label: "1.1", description: "Mild penalty" },
416
- { value: "1.2", label: "1.2", description: "Balanced" },
417
- { value: "1.5", label: "1.5", description: "Strong penalty" },
418
- ],
419
- serviceTier: [
420
- { value: "none", label: "None", description: "Omit service_tier parameter" },
421
- { value: "auto", label: "Auto", description: "Use provider default tier selection" },
422
- { value: "default", label: "Default", description: "Standard priority processing" },
423
- { value: "flex", label: "Flex", description: "Use flexible capacity tier when available" },
424
- { value: "scale", label: "Scale", description: "Use Scale Tier credits when available" },
425
- { value: "priority", label: "Priority", description: "Use Priority processing" },
426
- ],
427
- // Symbol preset
428
- symbolPreset: [
429
- { value: "unicode", label: "Unicode", description: "Standard symbols (default)" },
430
- { value: "nerd", label: "Nerd Font", description: "Requires Nerd Font" },
431
- { value: "ascii", label: "ASCII", description: "Maximum compatibility" },
432
- ],
433
- // Status line preset
434
- "statusLine.preset": [
435
- { value: "default", label: "Default", description: "Model, path, git, context, tokens, cost" },
436
- { value: "minimal", label: "Minimal", description: "Path and git only" },
437
- { value: "compact", label: "Compact", description: "Model, git, cost, context" },
438
- { value: "full", label: "Full", description: "All segments including time" },
439
- { value: "nerd", label: "Nerd", description: "Maximum info with Nerd Font icons" },
440
- { value: "ascii", label: "ASCII", description: "No special characters" },
441
- { value: "custom", label: "Custom", description: "User-defined segments" },
442
- ],
443
- // Status line separator
444
- "statusLine.separator": [
445
- { value: "powerline", label: "Powerline", description: "Solid arrows (Nerd Font)" },
446
- { value: "powerline-thin", label: "Thin chevron", description: "Thin arrows (Nerd Font)" },
447
- { value: "slash", label: "Slash", description: "Forward slashes" },
448
- { value: "pipe", label: "Pipe", description: "Vertical pipes" },
449
- { value: "block", label: "Block", description: "Solid blocks" },
450
- { value: "none", label: "None", description: "Space only" },
451
- { value: "ascii", label: "ASCII", description: "Greater-than signs" },
452
- ],
453
- // Loop mode
454
- "loop.mode": [
455
- {
456
- value: "prompt",
457
- label: "Prompt",
458
- description: "Re-submit the prompt as a follow-up message (current behavior)",
459
- },
460
- { value: "compact", label: "Compact", description: "Compact the session context, then re-submit the prompt" },
461
- { value: "reset", label: "Reset", description: "Start a new session, then re-submit the prompt" },
462
- ],
463
- };
464
-
465
- function createSubmenuSettingDef(base: Omit<SettingDef, "type" | "options">, provider: OptionProvider): SettingDef {
466
- if (typeof provider === "function") {
467
- return {
468
- ...base,
469
- type: "submenu",
470
- get options() {
471
- return provider();
472
- },
473
- };
474
- } else {
475
- return {
476
- ...base,
477
- type: "submenu",
478
- options: provider,
479
- };
480
- }
88
+ function resolveOptions(ui: AnyUiMetadata): OptionList | "runtime" | undefined {
89
+ if (!ui.options) return undefined;
90
+ if (ui.options === "runtime") return "runtime";
91
+ return ui.options;
481
92
  }
482
93
 
483
- // ═══════════════════════════════════════════════════════════════════════════
484
- // Schema to UI Conversion
485
- // ═══════════════════════════════════════════════════════════════════════════
486
-
487
94
  function pathToSettingDef(path: SettingPath): SettingDef | null {
488
95
  const ui = getUi(path);
489
96
  if (!ui) return null;
490
97
 
491
98
  const schemaType = getType(path);
492
- const base = { path, label: ui.label, description: ui.description, tab: ui.tab };
493
-
494
- // Check for condition
495
99
  const condition = ui.condition ? CONDITIONS[ui.condition] : undefined;
100
+ const base = { path, label: ui.label, description: ui.description, tab: ui.tab, condition };
496
101
 
497
102
  if (schemaType === "boolean") {
498
- return { ...base, type: "boolean", condition };
103
+ return { ...base, type: "boolean" };
499
104
  }
500
105
 
501
- if (schemaType === "enum") {
502
- const values = getEnumValues(path) ?? [];
503
-
504
- // If marked as submenu, use submenu type
505
- if (ui.submenu) {
506
- const provider = OPTION_PROVIDERS[path];
507
- if (!provider) return null;
508
- return createSubmenuSettingDef(base, provider);
509
- }
510
-
511
- return { ...base, type: "enum", values };
512
- }
106
+ const options = resolveOptions(ui);
513
107
 
514
- if (schemaType === "number" && ui.submenu) {
515
- const provider = OPTION_PROVIDERS[path];
516
- if (provider) {
517
- return createSubmenuSettingDef(base, provider);
108
+ if (schemaType === "enum") {
109
+ if (options === undefined) {
110
+ return { ...base, type: "enum", values: getEnumValues(path) ?? [] };
518
111
  }
112
+ // "runtime" is not a valid sentinel for enums — schema types prevent this,
113
+ // but treat defensively as an empty submenu.
114
+ return { ...base, type: "submenu", options: options === "runtime" ? [] : options };
519
115
  }
520
116
 
521
- if (schemaType === "string" && ui.submenu) {
522
- const provider = OPTION_PROVIDERS[path];
523
- if (provider) {
524
- return createSubmenuSettingDef(base, provider);
525
- }
526
- // For theme etc, options will be injected at runtime
527
- return createSubmenuSettingDef(base, []);
117
+ if (schemaType === "number") {
118
+ // Numbers without options are intentionally hidden from the UI.
119
+ if (!options || options === "runtime") return null;
120
+ return { ...base, type: "submenu", options };
528
121
  }
529
122
 
530
- // Plain string setting — free-text input field
531
123
  if (schemaType === "string") {
124
+ if (options === "runtime") {
125
+ // Empty list now; the selector layer (theme handling, etc.) injects choices.
126
+ return { ...base, type: "submenu", options: [] };
127
+ }
128
+ if (options) {
129
+ return { ...base, type: "submenu", options };
130
+ }
532
131
  return { ...base, type: "text" };
533
132
  }
534
133
 
@@ -288,8 +288,8 @@ export class SettingsSelectorComponent extends Container {
288
288
  * Convert a setting definition to a SettingItem for the UI.
289
289
  */
290
290
  #defToItem(def: SettingDef): SettingItem | null {
291
- // Check condition
292
- if (def.type === "boolean" && def.condition && !def.condition()) {
291
+ // Check condition: applies to every variant — booleans, enums, submenus, text inputs.
292
+ if (def.condition && !def.condition()) {
293
293
  return null;
294
294
  }
295
295
 
@@ -2,7 +2,7 @@ import type { PresetDef, StatusLinePreset } from "./types";
2
2
 
3
3
  export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
4
4
  default: {
5
- leftSegments: ["pi", "model", "mode", "path", "git", "pr", "context_pct", "token_total", "cost"],
5
+ leftSegments: ["pi", "model", "mode", "path", "git", "pr", "context_pct", "cost"],
6
6
  rightSegments: ["session_name"],
7
7
  separator: "powerline-thin",
8
8
  segmentOptions: {
@@ -17,7 +17,7 @@ import { clearClaudePluginRootsCache } from "../../discovery/helpers";
17
17
  import { getGatewayStatus } from "../../eval/py/gateway-coordinator";
18
18
  import { loadCustomShare } from "../../export/custom-share";
19
19
  import type { CompactOptions } from "../../extensibility/extensions/types";
20
- import { buildMemoryToolDeveloperInstructions, clearMemoryData, enqueueMemoryConsolidation } from "../../memories";
20
+ import { resolveMemoryBackend } from "../../memory-backend";
21
21
  import { BashExecutionComponent } from "../../modes/components/bash-execution";
22
22
  import { BorderedLoader } from "../../modes/components/bordered-loader";
23
23
  import { DynamicBorder } from "../../modes/components/dynamic-border";
@@ -570,11 +570,12 @@ export class CommandController {
570
570
  const argumentText = text.slice(7).trim();
571
571
  const action = argumentText.split(/\s+/, 1)[0]?.toLowerCase() || "view";
572
572
  const agentDir = this.ctx.settings.getAgentDir();
573
+ const backend = resolveMemoryBackend(this.ctx.settings);
573
574
 
574
575
  if (action === "view") {
575
- const payload = await buildMemoryToolDeveloperInstructions(agentDir, this.ctx.settings);
576
+ const payload = await backend.buildDeveloperInstructions(agentDir, this.ctx.settings);
576
577
  if (!payload) {
577
- this.ctx.showWarning("Memory payload is empty (memories disabled or no memory summary found).");
578
+ this.ctx.showWarning("Memory payload is empty (memory backend off, disabled, or no memory available).");
578
579
  return;
579
580
  }
580
581
  this.ctx.chatContainer.addChild(new Spacer(1));
@@ -589,7 +590,7 @@ export class CommandController {
589
590
 
590
591
  if (action === "reset" || action === "clear") {
591
592
  try {
592
- await clearMemoryData(agentDir, this.ctx.sessionManager.getCwd());
593
+ await backend.clear(agentDir, this.ctx.sessionManager.getCwd());
593
594
  await this.ctx.session.refreshBaseSystemPrompt();
594
595
  this.ctx.showStatus("Memory data cleared and system prompt refreshed.");
595
596
  } catch (error) {
@@ -600,7 +601,7 @@ export class CommandController {
600
601
 
601
602
  if (action === "enqueue" || action === "rebuild") {
602
603
  try {
603
- enqueueMemoryConsolidation(agentDir, this.ctx.sessionManager.getCwd());
604
+ await backend.enqueue(agentDir, this.ctx.sessionManager.getCwd());
604
605
  this.ctx.showStatus("Memory consolidation enqueued.");
605
606
  } catch (error) {
606
607
  this.ctx.showError(`Memory enqueue failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -53,6 +53,7 @@ export class EventController {
53
53
  todo_reminder: e => this.#handleTodoReminder(e),
54
54
  todo_auto_clear: e => this.#handleTodoAutoClear(e),
55
55
  irc_message: e => this.#handleIrcMessage(e),
56
+ notice: e => this.#handleNotice(e),
56
57
  } satisfies AgentSessionEventHandlers;
57
58
  }
58
59
 
@@ -223,6 +224,17 @@ export class EventController {
223
224
  this.ctx.ui.requestRender();
224
225
  }
225
226
 
227
+ async #handleNotice(event: Extract<AgentSessionEvent, { type: "notice" }>): Promise<void> {
228
+ const message = event.source ? `${event.source}: ${event.message}` : event.message;
229
+ if (event.level === "error") {
230
+ this.ctx.showError(message);
231
+ } else if (event.level === "warning") {
232
+ this.ctx.showWarning(message);
233
+ } else {
234
+ this.ctx.showStatus(message);
235
+ }
236
+ }
237
+
226
238
  async #handleMessageUpdate(event: Extract<AgentSessionEvent, { type: "message_update" }>): Promise<void> {
227
239
  if (this.ctx.streamingComponent && event.message.role === "assistant") {
228
240
  this.ctx.streamingMessage = event.message;