@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
@@ -12,13 +12,18 @@ import {
12
12
  type TabBarTheme,
13
13
  Text,
14
14
  } from "@oh-my-pi/pi-tui";
15
- import type { SettingsManager, StatusLineSettings } from "../../config/settings-manager";
15
+ import { type SettingPath, settings } from "../../config/settings";
16
+ import type {
17
+ SettingTab,
18
+ StatusLinePreset,
19
+ StatusLineSegmentId,
20
+ StatusLineSeparatorStyle,
21
+ } from "../../config/settings-schema";
16
22
  import { getSelectListTheme, getSettingsListTheme, theme } from "../../modes/theme/theme";
17
23
  import { DynamicBorder } from "./dynamic-border";
18
24
  import { PluginSettingsComponent } from "./plugin-settings";
19
25
  import { getSettingsForTab, type SettingDef } from "./settings-defs";
20
26
  import { getPreset } from "./status-line/presets";
21
- import { StatusLineSegmentEditorComponent } from "./status-line-segment-editor";
22
27
 
23
28
  function getTabBarTheme(): TabBarTheme {
24
29
  return {
@@ -111,11 +116,10 @@ class SelectSubmenu extends Container {
111
116
  }
112
117
  }
113
118
 
114
- type TabId = string;
115
-
116
119
  const SETTINGS_TABS: Tab[] = [
117
120
  { id: "behavior", label: "Behavior" },
118
121
  { id: "tools", label: "Tools" },
122
+ { id: "bash", label: "Bash" },
119
123
  { id: "display", label: "Display" },
120
124
  { id: "ttsr", label: "TTSR" },
121
125
  { id: "status", label: "Status" },
@@ -126,7 +130,7 @@ const SETTINGS_TABS: Tab[] = [
126
130
 
127
131
  /**
128
132
  * Dynamic context for settings that need runtime data.
129
- * Some settings (like thinking level) are managed by the session, not SettingsManager.
133
+ * Some settings (like thinking level) are managed by the session, not Settings.
130
134
  */
131
135
  export interface SettingsRuntimeContext {
132
136
  /** Available thinking levels (from session) */
@@ -139,19 +143,21 @@ export interface SettingsRuntimeContext {
139
143
  cwd: string;
140
144
  }
141
145
 
142
- /**
143
- * Callback when any setting changes.
144
- * The handler should dispatch based on settingId.
145
- */
146
- export type SettingChangeHandler = (settingId: string, newValue: string | boolean) => void;
146
+ /** Status line settings subset for preview */
147
+ export interface StatusLinePreviewSettings {
148
+ preset?: StatusLinePreset;
149
+ leftSegments?: StatusLineSegmentId[];
150
+ rightSegments?: StatusLineSegmentId[];
151
+ separator?: StatusLineSeparatorStyle;
152
+ }
147
153
 
148
154
  export interface SettingsCallbacks {
149
155
  /** Called when any setting value changes */
150
- onChange: SettingChangeHandler;
156
+ onChange: (path: SettingPath, newValue: unknown) => void;
151
157
  /** Called for theme preview while browsing */
152
158
  onThemePreview?: (theme: string) => void;
153
- /** Called for status line preview while configuring - updates actual status line */
154
- onStatusLinePreview?: (settings: Partial<StatusLineSettings>) => void;
159
+ /** Called for status line preview while configuring */
160
+ onStatusLinePreview?: (settings: StatusLinePreviewSettings) => void;
155
161
  /** Get current rendered status line for inline preview */
156
162
  getStatusLinePreview?: () => string;
157
163
  /** Called when plugins change */
@@ -171,16 +177,14 @@ export class SettingsSelectorComponent extends Container {
171
177
  private pluginComponent: PluginSettingsComponent | null = null;
172
178
  private statusPreviewContainer: Container | null = null;
173
179
  private statusPreviewText: Text | null = null;
174
- private currentTabId: TabId = "behavior";
180
+ private currentTabId: SettingTab | "plugins" = "behavior";
175
181
 
176
- private settingsManager: SettingsManager;
177
182
  private context: SettingsRuntimeContext;
178
183
  private callbacks: SettingsCallbacks;
179
184
 
180
- constructor(settingsManager: SettingsManager, context: SettingsRuntimeContext, callbacks: SettingsCallbacks) {
185
+ constructor(context: SettingsRuntimeContext, callbacks: SettingsCallbacks) {
181
186
  super();
182
187
 
183
- this.settingsManager = settingsManager;
184
188
  this.context = context;
185
189
  this.callbacks = callbacks;
186
190
 
@@ -190,7 +194,7 @@ export class SettingsSelectorComponent extends Container {
190
194
  // Tab bar
191
195
  this.tabBar = new TabBar("Settings", SETTINGS_TABS, getTabBarTheme());
192
196
  this.tabBar.onTabChange = () => {
193
- this.switchToTab(this.tabBar.getActiveTab().id as TabId);
197
+ this.switchToTab(this.tabBar.getActiveTab().id as SettingTab | "plugins");
194
198
  };
195
199
  this.addChild(this.tabBar);
196
200
 
@@ -204,7 +208,7 @@ export class SettingsSelectorComponent extends Container {
204
208
  this.addChild(new DynamicBorder());
205
209
  }
206
210
 
207
- private switchToTab(tabId: TabId): void {
211
+ private switchToTab(tabId: SettingTab | "plugins"): void {
208
212
  this.currentTabId = tabId;
209
213
 
210
214
  // Remove current content
@@ -250,7 +254,7 @@ export class SettingsSelectorComponent extends Container {
250
254
  switch (def.type) {
251
255
  case "boolean":
252
256
  return {
253
- id: def.id,
257
+ id: def.path,
254
258
  label: def.label,
255
259
  description: def.description,
256
260
  currentValue: currentValue ? "true" : "false",
@@ -259,7 +263,7 @@ export class SettingsSelectorComponent extends Container {
259
263
 
260
264
  case "enum":
261
265
  return {
262
- id: def.id,
266
+ id: def.path,
263
267
  label: def.label,
264
268
  description: def.description,
265
269
  currentValue: currentValue as string,
@@ -268,26 +272,24 @@ export class SettingsSelectorComponent extends Container {
268
272
 
269
273
  case "submenu":
270
274
  return {
271
- id: def.id,
275
+ id: def.path,
272
276
  label: def.label,
273
277
  description: def.description,
274
- currentValue: currentValue as string,
278
+ currentValue: String(currentValue ?? ""),
275
279
  submenu: (cv, done) => this.createSubmenu(def, cv, done),
276
280
  };
277
281
  }
278
282
  }
279
283
 
280
284
  /**
281
- * Get the current value for a setting, using runtime context for special cases.
285
+ * Get the current value for a setting.
282
286
  */
283
- private getCurrentValue(def: SettingDef): string | boolean {
284
- // Special cases that come from runtime context instead of SettingsManager
285
- switch (def.id) {
286
- case "thinkingLevel":
287
- return this.context.thinkingLevel;
288
- default:
289
- return def.get(this.settingsManager);
287
+ private getCurrentValue(def: SettingDef): unknown {
288
+ // Special case: thinking level comes from runtime context
289
+ if (def.path === "defaultThinkingLevel") {
290
+ return this.context.thinkingLevel;
290
291
  }
292
+ return settings.get(def.path);
291
293
  }
292
294
 
293
295
  /**
@@ -298,20 +300,15 @@ export class SettingsSelectorComponent extends Container {
298
300
  currentValue: string,
299
301
  done: (value?: string) => void,
300
302
  ): Container {
301
- // Special case: segment editor
302
- if (def.id === "statusLineSegments") {
303
- return this.createSegmentEditor(done);
304
- }
305
-
306
- let options = def.getOptions(this.settingsManager);
303
+ let options = def.getOptions();
307
304
 
308
- // Special case: inject runtime options
309
- if (def.id === "thinkingLevel") {
305
+ // Special case: inject runtime options for thinking level
306
+ if (def.path === "defaultThinkingLevel") {
310
307
  options = this.context.availableThinkingLevels.map(level => {
311
- const baseOpt = def.getOptions(this.settingsManager).find(o => o.value === level);
308
+ const baseOpt = def.getOptions().find(o => o.value === level);
312
309
  return baseOpt || { value: level, label: level };
313
310
  });
314
- } else if (def.id === "theme") {
311
+ } else if (def.path === "theme") {
315
312
  options = this.context.availableThemes.map(t => ({ value: t, label: t }));
316
313
  }
317
314
 
@@ -319,14 +316,16 @@ export class SettingsSelectorComponent extends Container {
319
316
  let onPreview: ((value: string) => void) | undefined;
320
317
  let onPreviewCancel: (() => void) | undefined;
321
318
 
322
- if (def.id === "theme") {
319
+ if (def.path === "theme") {
323
320
  onPreview = this.callbacks.onThemePreview;
324
321
  onPreviewCancel = () => this.callbacks.onThemePreview?.(currentValue);
325
- } else if (def.id === "statusLinePreset") {
322
+ } else if (def.path === "statusLine.preset") {
326
323
  onPreview = value => {
327
- const presetDef = getPreset((value as StatusLineSettings["preset"]) ?? "default");
324
+ const presetDef = getPreset(
325
+ value as "default" | "minimal" | "compact" | "full" | "nerd" | "ascii" | "custom",
326
+ );
328
327
  this.callbacks.onStatusLinePreview?.({
329
- preset: value as StatusLineSettings["preset"],
328
+ preset: value as StatusLinePreset,
330
329
  leftSegments: presetDef.leftSegments,
331
330
  rightSegments: presetDef.rightSegments,
332
331
  separator: presetDef.separator,
@@ -334,7 +333,7 @@ export class SettingsSelectorComponent extends Container {
334
333
  this.updateStatusPreview();
335
334
  };
336
335
  onPreviewCancel = () => {
337
- const currentPreset = this.settingsManager.getStatusLinePreset();
336
+ const currentPreset = settings.get("statusLine.preset");
338
337
  const presetDef = getPreset(currentPreset);
339
338
  this.callbacks.onStatusLinePreview?.({
340
339
  preset: currentPreset,
@@ -344,26 +343,20 @@ export class SettingsSelectorComponent extends Container {
344
343
  });
345
344
  this.updateStatusPreview();
346
345
  };
347
- } else if (def.id === "statusLineSeparator") {
346
+ } else if (def.path === "statusLine.separator") {
348
347
  onPreview = value => {
349
- this.callbacks.onStatusLinePreview?.({
350
- separator: value as StatusLineSettings["separator"],
351
- });
348
+ this.callbacks.onStatusLinePreview?.({ separator: value as StatusLineSeparatorStyle });
352
349
  this.updateStatusPreview();
353
350
  };
354
351
  onPreviewCancel = () => {
355
- const currentSettings = this.settingsManager.getStatusLineSettings();
356
- const separator =
357
- currentSettings.separator ?? getPreset(this.settingsManager.getStatusLinePreset()).separator;
358
- this.callbacks.onStatusLinePreview?.({
359
- separator,
360
- });
352
+ const separator = settings.get("statusLine.separator");
353
+ this.callbacks.onStatusLinePreview?.({ separator });
361
354
  this.updateStatusPreview();
362
355
  };
363
356
  }
364
357
 
365
358
  // Provide status line preview for theme selection
366
- const getPreview = def.id === "theme" ? this.callbacks.getStatusLinePreview : undefined;
359
+ const getPreview = def.path === "theme" ? this.callbacks.getStatusLinePreview : undefined;
367
360
 
368
361
  return new SelectSubmenu(
369
362
  def.label,
@@ -371,10 +364,10 @@ export class SettingsSelectorComponent extends Container {
371
364
  options,
372
365
  currentValue,
373
366
  value => {
374
- // Persist to SettingsManager
375
- def.set(this.settingsManager, value);
376
- // Notify for side effects
377
- this.callbacks.onChange(def.id, value);
367
+ // Persist
368
+ this.setSettingValue(def.path, value);
369
+ // Notify
370
+ this.callbacks.onChange(def.path, value);
378
371
  done(value);
379
372
  },
380
373
  () => {
@@ -387,47 +380,24 @@ export class SettingsSelectorComponent extends Container {
387
380
  }
388
381
 
389
382
  /**
390
- * Create the segment editor component.
383
+ * Set a setting value, handling type conversion.
391
384
  */
392
- private createSegmentEditor(done: (value?: string) => void): Container {
393
- const currentSettings = this.settingsManager.getStatusLineSettings();
394
- const preset = currentSettings.preset ?? "default";
395
- const presetDef = getPreset(preset);
396
-
397
- const leftSegments = currentSettings.leftSegments ?? presetDef.leftSegments;
398
- const rightSegments = currentSettings.rightSegments ?? presetDef.rightSegments;
399
-
400
- return new StatusLineSegmentEditorComponent(leftSegments, rightSegments, {
401
- onSave: (left, right) => {
402
- this.settingsManager.setStatusLineLeftSegments(left);
403
- this.settingsManager.setStatusLineRightSegments(right);
404
- this.callbacks.onChange("statusLineSegments", "saved");
405
- this.callbacks.onStatusLinePreview?.({ leftSegments: left, rightSegments: right });
406
- this.updateStatusPreview();
407
- done("saved");
408
- },
409
- onCancel: () => {
410
- // Restore preview to saved state
411
- const saved = this.settingsManager.getStatusLineSettings();
412
- const savedPreset = getPreset(saved.preset ?? "default");
413
- this.callbacks.onStatusLinePreview?.({
414
- leftSegments: saved.leftSegments ?? savedPreset.leftSegments,
415
- rightSegments: saved.rightSegments ?? savedPreset.rightSegments,
416
- });
417
- this.updateStatusPreview();
418
- done();
419
- },
420
- onPreview: (left, right) => {
421
- this.callbacks.onStatusLinePreview?.({ leftSegments: left, rightSegments: right });
422
- this.updateStatusPreview();
423
- },
424
- });
385
+ private setSettingValue(path: SettingPath, value: string): void {
386
+ // Handle number conversions
387
+ const currentValue = settings.get(path);
388
+ if (typeof currentValue === "number") {
389
+ settings.set(path, Number(value) as never);
390
+ } else if (typeof currentValue === "boolean") {
391
+ settings.set(path, (value === "true") as never);
392
+ } else {
393
+ settings.set(path, value as never);
394
+ }
425
395
  }
426
396
 
427
397
  /**
428
398
  * Show a settings tab using definitions.
429
399
  */
430
- private showSettingsTab(tabId: string): void {
400
+ private showSettingsTab(tabId: SettingTab): void {
431
401
  const defs = getSettingsForTab(tabId);
432
402
  const items: SettingItem[] = [];
433
403
 
@@ -454,22 +424,22 @@ export class SettingsSelectorComponent extends Container {
454
424
  10,
455
425
  getSettingsListTheme(),
456
426
  (id, newValue) => {
457
- const def = defs.find(d => d.id === id);
427
+ const def = defs.find(d => d.path === id);
458
428
  if (!def) return;
459
429
 
460
- // Persist to SettingsManager based on type
430
+ const path = def.path;
431
+
461
432
  if (def.type === "boolean") {
462
433
  const boolValue = newValue === "true";
463
- def.set(this.settingsManager, boolValue);
464
- this.callbacks.onChange(id, boolValue);
434
+ settings.set(path, boolValue as never);
435
+ this.callbacks.onChange(path, boolValue);
465
436
 
466
- // Trigger status line preview for status tab boolean settings
467
437
  if (tabId === "status") {
468
438
  this.triggerStatusLinePreview();
469
439
  }
470
440
  } else if (def.type === "enum") {
471
- def.set(this.settingsManager, newValue);
472
- this.callbacks.onChange(id, newValue);
441
+ settings.set(path, newValue as never);
442
+ this.callbacks.onChange(path, newValue);
473
443
  }
474
444
  // Submenu types are handled in createSubmenu
475
445
  },
@@ -493,9 +463,13 @@ export class SettingsSelectorComponent extends Container {
493
463
  * Trigger status line preview with current settings.
494
464
  */
495
465
  private triggerStatusLinePreview(): void {
496
- const settings = this.settingsManager.getStatusLineSettings();
497
- this.callbacks.onStatusLinePreview?.(settings);
498
- // Update inline preview
466
+ const statusLineSettings: StatusLinePreviewSettings = {
467
+ preset: settings.get("statusLine.preset"),
468
+ leftSegments: settings.get("statusLine.leftSegments"),
469
+ rightSegments: settings.get("statusLine.rightSegments"),
470
+ separator: settings.get("statusLine.separator"),
471
+ };
472
+ this.callbacks.onStatusLinePreview?.(statusLineSettings);
499
473
  this.updateStatusPreview();
500
474
  }
501
475
 
@@ -1,13 +1,7 @@
1
- import type {
2
- StatusLinePreset,
3
- StatusLineSegmentId,
4
- StatusLineSegmentOptions,
5
- StatusLineSeparatorStyle,
6
- StatusLineSettings,
7
- } from "../../../config/settings-manager";
1
+ import type { StatusLinePreset, StatusLineSegmentId, StatusLineSeparatorStyle } from "../../../config/settings-schema";
8
2
  import type { AgentSession } from "../../../session/agent-session";
3
+ import type { StatusLineSegmentOptions, StatusLineSettings } from "../status-line";
9
4
 
10
- // Re-export types from settings-manager (single source of truth)
11
5
  export type {
12
6
  StatusLinePreset,
13
7
  StatusLineSegmentId,
@@ -8,8 +8,8 @@
8
8
  * - Shift+J/K: Reorder segment within column
9
9
  * - Live preview shown in the actual status line above
10
10
  */
11
- import { Container, matchesKey } from "@oh-my-pi/pi-tui";
12
- import type { StatusLineSegmentId } from "../../config/settings-manager";
11
+ import { Container, matchesKey, padding } from "@oh-my-pi/pi-tui";
12
+ import type { StatusLineSegmentId } from "../../config/settings-schema";
13
13
  import { theme } from "../../modes/theme/theme";
14
14
  import { ALL_SEGMENT_IDS } from "./status-line/segments";
15
15
 
@@ -351,7 +351,7 @@ export class StatusLineSegmentEditorComponent extends Container {
351
351
  }
352
352
 
353
353
  // Pad to column width (accounting for ANSI codes)
354
- const padding = colWidth - label.length - 1;
355
- return text + " ".repeat(Math.max(0, padding));
354
+ const padSize = colWidth - label.length - 1;
355
+ return text + padding(Math.max(0, padSize));
356
356
  }
357
357
  }
@@ -1,15 +1,32 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { AssistantMessage } from "@oh-my-pi/pi-ai";
4
- import { type Component, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
4
+ import { type Component, padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
5
5
  import { $ } from "bun";
6
- import type { StatusLineSegmentOptions, StatusLineSettings } from "../../config/settings-manager";
6
+ import { settings } from "../../config/settings";
7
+ import type { StatusLinePreset, StatusLineSegmentId, StatusLineSeparatorStyle } from "../../config/settings-schema";
7
8
  import { theme } from "../../modes/theme/theme";
8
9
  import type { AgentSession } from "../../session/agent-session";
9
10
  import { getPreset } from "./status-line/presets";
10
11
  import { renderSegment, type SegmentContext } from "./status-line/segments";
11
12
  import { getSeparator } from "./status-line/separators";
12
13
 
14
+ export interface StatusLineSegmentOptions {
15
+ model?: { showThinkingLevel?: boolean };
16
+ path?: { abbreviate?: boolean; maxLength?: number; stripWorkPrefix?: boolean };
17
+ git?: { showBranch?: boolean; showStaged?: boolean; showUnstaged?: boolean; showUntracked?: boolean };
18
+ time?: { format?: "12h" | "24h"; showSeconds?: boolean };
19
+ }
20
+
21
+ export interface StatusLineSettings {
22
+ preset?: StatusLinePreset;
23
+ leftSegments?: StatusLineSegmentId[];
24
+ rightSegments?: StatusLineSegmentId[];
25
+ separator?: StatusLineSeparatorStyle;
26
+ segmentOptions?: StatusLineSegmentOptions;
27
+ showHookStatus?: boolean;
28
+ }
29
+
13
30
  // ═══════════════════════════════════════════════════════════════════════════
14
31
  // Rendering Helpers
15
32
  // ═══════════════════════════════════════════════════════════════════════════
@@ -60,8 +77,14 @@ export class StatusLineComponent implements Component {
60
77
 
61
78
  constructor(session: AgentSession) {
62
79
  this.session = session;
63
- // Load initial settings
64
- this.settings = session.settingsManager?.getStatusLineSettings() ?? {};
80
+ this.settings = {
81
+ preset: settings.get("statusLine.preset"),
82
+ leftSegments: settings.get("statusLine.leftSegments"),
83
+ rightSegments: settings.get("statusLine.rightSegments"),
84
+ separator: settings.get("statusLine.separator"),
85
+ showHookStatus: settings.get("statusLine.showHookStatus"),
86
+ segmentOptions: settings.getGroup("statusLine").segmentOptions,
87
+ };
65
88
  }
66
89
 
67
90
  updateSettings(settings: StatusLineSettings): void {
@@ -378,7 +401,7 @@ export class StatusLineComponent implements Component {
378
401
  leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
379
402
  rightWidth = groupWidth(right, rightCapWidth, rightSepWidth);
380
403
  const gapWidth = Math.max(1, topFillWidth - leftWidth - rightWidth);
381
- return leftGroup + " ".repeat(gapWidth) + rightGroup;
404
+ return leftGroup + padding(gapWidth) + rightGroup;
382
405
  }
383
406
 
384
407
  getTopBorder(width: number): { content: string; width: number } {
@@ -1,4 +1,4 @@
1
- import { type Component, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
1
+ import { type Component, padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
2
2
  import { APP_NAME } from "../../config";
3
3
  import { theme } from "../../modes/theme/theme";
4
4
 
@@ -169,7 +169,7 @@ export class WelcomeComponent implements Component {
169
169
  }
170
170
  const leftPad = Math.floor((width - visLen) / 2);
171
171
  const rightPad = width - visLen - leftPad;
172
- return " ".repeat(leftPad) + text + " ".repeat(rightPad);
172
+ return padding(leftPad) + text + padding(rightPad);
173
173
  }
174
174
 
175
175
  /** Apply magenta→cyan gradient to a string */
@@ -224,6 +224,6 @@ export class WelcomeComponent implements Component {
224
224
  }
225
225
  return `${truncated}${ellipsis}`;
226
226
  }
227
- return str + " ".repeat(width - visLen);
227
+ return str + padding(width - visLen);
228
228
  }
229
229
  }
@@ -2,7 +2,7 @@ import * as fs from "node:fs/promises";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import type { UsageLimit, UsageReport } from "@oh-my-pi/pi-ai";
5
- import { Loader, Markdown, Spacer, Text, visibleWidth } from "@oh-my-pi/pi-tui";
5
+ import { Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@oh-my-pi/pi-tui";
6
6
  import { $ } from "bun";
7
7
  import { nanoid } from "nanoid";
8
8
  import { loadCustomShare } from "../../export/custom-share";
@@ -773,7 +773,7 @@ function formatAccountHeader(limit: UsageLimit, report: UsageReport, index: numb
773
773
  function padColumn(text: string, width: number): string {
774
774
  const visible = visibleWidth(text);
775
775
  if (visible >= width) return text;
776
- return `${text}${" ".repeat(width - visible)}`;
776
+ return `${text}${padding(width - visible)}`;
777
777
  }
778
778
 
779
779
  function resolveAggregateStatus(limits: UsageLimit[]): UsageLimit["status"] {
@@ -1,4 +1,5 @@
1
1
  import { Loader, Text } from "@oh-my-pi/pi-tui";
2
+ import { settings } from "../../config/settings";
2
3
  import { AssistantMessageComponent } from "../../modes/components/assistant-message";
3
4
  import { ReadToolGroupComponent } from "../../modes/components/read-tool-group";
4
5
  import { TodoReminderComponent } from "../../modes/components/todo-reminder";
@@ -136,9 +137,9 @@ export class EventController {
136
137
  content.name,
137
138
  content.arguments,
138
139
  {
139
- showImages: this.ctx.settingsManager.getShowImages(),
140
- editFuzzyThreshold: this.ctx.settingsManager.getEditFuzzyThreshold(),
141
- editAllowFuzzy: this.ctx.settingsManager.getEditFuzzyMatch(),
140
+ showImages: settings.get("terminal.showImages"),
141
+ editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
142
+ editAllowFuzzy: settings.get("edit.fuzzyMatch"),
142
143
  },
143
144
  tool,
144
145
  this.ctx.ui,
@@ -210,9 +211,9 @@ export class EventController {
210
211
  event.toolName,
211
212
  event.args,
212
213
  {
213
- showImages: this.ctx.settingsManager.getShowImages(),
214
- editFuzzyThreshold: this.ctx.settingsManager.getEditFuzzyThreshold(),
215
- editAllowFuzzy: this.ctx.settingsManager.getEditFuzzyMatch(),
214
+ showImages: settings.get("terminal.showImages"),
215
+ editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
216
+ editAllowFuzzy: settings.get("edit.fuzzyMatch"),
216
217
  },
217
218
  tool,
218
219
  this.ctx.ui,
@@ -383,10 +384,10 @@ export class EventController {
383
384
  sendCompletionNotification(): void {
384
385
  if (this.ctx.isBackgrounded === false) return;
385
386
  if (isNotificationSuppressed()) return;
386
- const method = this.ctx.settingsManager.getNotificationOnComplete();
387
+ const method = settings.get("notifications.onComplete");
387
388
  if (method === "off") return;
388
389
  const protocol = method === "auto" ? detectNotificationProtocol() : method;
389
- const title = this.ctx.sessionManager.getSessionTitle();
390
+ const title = this.ctx.sessionManager.getSessionName();
390
391
  const message = title ? `${title}: Complete` : "Complete";
391
392
  sendNotification(protocol, message);
392
393
  }
@@ -4,6 +4,7 @@ import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
6
6
  import { nanoid } from "nanoid";
7
+ import { settings } from "../../config/settings";
7
8
  import { theme } from "../../modes/theme/theme";
8
9
  import type { InteractiveModeContext } from "../../modes/types";
9
10
  import type { AgentSessionEvent } from "../../session/agent-session";
@@ -40,17 +41,20 @@ export class InputController {
40
41
  this.ctx.isPythonMode = false;
41
42
  this.ctx.updateEditorBorderColor();
42
43
  } else if (!this.ctx.editor.getText().trim()) {
43
- // Double-escape with empty editor triggers /tree or /branch based on setting
44
- const now = Date.now();
45
- if (now - this.ctx.lastEscapeTime < 500) {
46
- if (this.ctx.settingsManager.getDoubleEscapeAction() === "tree") {
47
- this.ctx.showTreeSelector();
44
+ // Double-escape with empty editor triggers /tree, /branch, or nothing based on setting
45
+ const action = settings.get("doubleEscapeAction");
46
+ if (action !== "none") {
47
+ const now = Date.now();
48
+ if (now - this.ctx.lastEscapeTime < 500) {
49
+ if (action === "tree") {
50
+ this.ctx.showTreeSelector();
51
+ } else {
52
+ this.ctx.showUserMessageSelector();
53
+ }
54
+ this.ctx.lastEscapeTime = 0;
48
55
  } else {
49
- this.ctx.showUserMessageSelector();
56
+ this.ctx.lastEscapeTime = now;
50
57
  }
51
- this.ctx.lastEscapeTime = 0;
52
- } else {
53
- this.ctx.lastEscapeTime = now;
54
58
  }
55
59
  }
56
60
  };
@@ -242,7 +246,7 @@ export class InputController {
242
246
  return;
243
247
  }
244
248
  if (text === "/branch") {
245
- if (this.ctx.settingsManager.getDoubleEscapeAction() === "tree") {
249
+ if (settings.get("doubleEscapeAction") === "tree") {
246
250
  this.ctx.showTreeSelector();
247
251
  } else {
248
252
  this.ctx.showUserMessageSelector();
@@ -418,13 +422,13 @@ export class InputController {
418
422
 
419
423
  // Generate session title on first message
420
424
  const hasUserMessages = this.ctx.agent.state.messages.some((m: AgentMessage) => m.role === "user");
421
- if (!hasUserMessages && !this.ctx.sessionManager.getSessionTitle() && !process.env.OMP_NO_TITLE) {
425
+ if (!hasUserMessages && !this.ctx.sessionManager.getSessionName() && !process.env.OMP_NO_TITLE) {
422
426
  const registry = this.ctx.session.modelRegistry;
423
- const smolModel = this.ctx.settingsManager.getModelRole("smol");
427
+ const smolModel = this.ctx.settings.getModelRole("smol");
424
428
  generateSessionTitle(text, registry, smolModel, this.ctx.session.sessionId)
425
429
  .then(async title => {
426
430
  if (title) {
427
- await this.ctx.sessionManager.setSessionTitle(title);
431
+ await this.ctx.sessionManager.setSessionName(title);
428
432
  setTerminalTitle(`π: ${title}`);
429
433
  }
430
434
  })
@@ -560,7 +564,7 @@ export class InputController {
560
564
  const image = await readImageFromClipboard();
561
565
  if (image) {
562
566
  let imageData = image;
563
- if (this.ctx.settingsManager.getImageAutoResize()) {
567
+ if (settings.get("images.autoResize")) {
564
568
  try {
565
569
  const resized = await resizeImage({
566
570
  type: "image",
@@ -651,7 +655,7 @@ export class InputController {
651
655
 
652
656
  toggleThinkingBlockVisibility(): void {
653
657
  this.ctx.hideThinkingBlock = !this.ctx.hideThinkingBlock;
654
- this.ctx.settingsManager.setHideThinkingBlock(this.ctx.hideThinkingBlock);
658
+ settings.set("hideThinkingBlock", this.ctx.hideThinkingBlock);
655
659
 
656
660
  // Rebuild chat from session messages
657
661
  this.ctx.chatContainer.clear();