@oh-my-pi/pi-coding-agent 1.337.1 → 1.340.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.
@@ -1,7 +1,6 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
2
  import {
3
3
  Container,
4
- getCapabilities,
5
4
  isArrowLeft,
6
5
  isArrowRight,
7
6
  isEscape,
@@ -17,55 +16,11 @@ import {
17
16
  type TabBarTheme,
18
17
  Text,
19
18
  } from "@oh-my-pi/pi-tui";
19
+ import type { SettingsManager } from "../../../core/settings-manager.js";
20
20
  import { getSelectListTheme, getSettingsListTheme, theme } from "../theme/theme.js";
21
21
  import { DynamicBorder } from "./dynamic-border.js";
22
22
  import { PluginSettingsComponent } from "./plugin-settings.js";
23
-
24
- const THINKING_DESCRIPTIONS: Record<ThinkingLevel, string> = {
25
- off: "No reasoning",
26
- minimal: "Very brief reasoning (~1k tokens)",
27
- low: "Light reasoning (~2k tokens)",
28
- medium: "Moderate reasoning (~8k tokens)",
29
- high: "Deep reasoning (~16k tokens)",
30
- xhigh: "Maximum reasoning (~32k tokens)",
31
- };
32
-
33
- export interface ExaToolsConfig {
34
- enabled: boolean;
35
- enableSearch: boolean;
36
- enableLinkedin: boolean;
37
- enableCompany: boolean;
38
- enableResearcher: boolean;
39
- enableWebsets: boolean;
40
- }
41
-
42
- export interface SettingsConfig {
43
- autoCompact: boolean;
44
- showImages: boolean;
45
- queueMode: "all" | "one-at-a-time";
46
- thinkingLevel: ThinkingLevel;
47
- availableThinkingLevels: ThinkingLevel[];
48
- currentTheme: string;
49
- availableThemes: string[];
50
- hideThinkingBlock: boolean;
51
- collapseChangelog: boolean;
52
- cwd: string;
53
- exa: ExaToolsConfig;
54
- }
55
-
56
- export interface SettingsCallbacks {
57
- onAutoCompactChange: (enabled: boolean) => void;
58
- onShowImagesChange: (enabled: boolean) => void;
59
- onQueueModeChange: (mode: "all" | "one-at-a-time") => void;
60
- onThinkingLevelChange: (level: ThinkingLevel) => void;
61
- onThemeChange: (theme: string) => void;
62
- onThemePreview?: (theme: string) => void;
63
- onHideThinkingBlockChange: (hidden: boolean) => void;
64
- onCollapseChangelogChange: (collapsed: boolean) => void;
65
- onPluginsChanged?: () => void;
66
- onExaSettingChange: (setting: keyof ExaToolsConfig, enabled: boolean) => void;
67
- onCancel: () => void;
68
- }
23
+ import { getSettingsForTab, type SettingDef } from "./settings-defs.js";
69
24
 
70
25
  function getTabBarTheme(): TabBarTheme {
71
26
  return {
@@ -146,8 +101,41 @@ const SETTINGS_TABS: Tab[] = [
146
101
  { id: "plugins", label: "Plugins" },
147
102
  ];
148
103
 
104
+ /**
105
+ * Dynamic context for settings that need runtime data.
106
+ * Some settings (like thinking level) are managed by the session, not SettingsManager.
107
+ */
108
+ export interface SettingsRuntimeContext {
109
+ /** Available thinking levels (from session) */
110
+ availableThinkingLevels: ThinkingLevel[];
111
+ /** Current thinking level (from session) */
112
+ thinkingLevel: ThinkingLevel;
113
+ /** Available themes */
114
+ availableThemes: string[];
115
+ /** Working directory for plugins tab */
116
+ cwd: string;
117
+ }
118
+
119
+ /**
120
+ * Callback when any setting changes.
121
+ * The handler should dispatch based on settingId.
122
+ */
123
+ export type SettingChangeHandler = (settingId: string, newValue: string | boolean) => void;
124
+
125
+ export interface SettingsCallbacks {
126
+ /** Called when any setting value changes */
127
+ onChange: SettingChangeHandler;
128
+ /** Called for theme preview while browsing */
129
+ onThemePreview?: (theme: string) => void;
130
+ /** Called when plugins change */
131
+ onPluginsChanged?: () => void;
132
+ /** Called when settings panel is closed */
133
+ onCancel: () => void;
134
+ }
135
+
149
136
  /**
150
137
  * Main tabbed settings selector component.
138
+ * Uses declarative settings definitions from settings-defs.ts.
151
139
  */
152
140
  export class SettingsSelectorComponent extends Container {
153
141
  private tabBar: TabBar;
@@ -155,13 +143,15 @@ export class SettingsSelectorComponent extends Container {
155
143
  private currentSubmenu: Container | null = null;
156
144
  private pluginComponent: PluginSettingsComponent | null = null;
157
145
 
158
- private config: SettingsConfig;
146
+ private settingsManager: SettingsManager;
147
+ private context: SettingsRuntimeContext;
159
148
  private callbacks: SettingsCallbacks;
160
149
 
161
- constructor(config: SettingsConfig, callbacks: SettingsCallbacks) {
150
+ constructor(settingsManager: SettingsManager, context: SettingsRuntimeContext, callbacks: SettingsCallbacks) {
162
151
  super();
163
152
 
164
- this.config = config;
153
+ this.settingsManager = settingsManager;
154
+ this.context = context;
165
155
  this.callbacks = callbacks;
166
156
 
167
157
  // Add top border
@@ -201,10 +191,8 @@ export class SettingsSelectorComponent extends Container {
201
191
 
202
192
  switch (tabId) {
203
193
  case "config":
204
- this.showConfigTab();
205
- break;
206
194
  case "exa":
207
- this.showExaTab();
195
+ this.showSettingsTab(tabId);
208
196
  break;
209
197
  case "plugins":
210
198
  this.showPluginsTab();
@@ -215,201 +203,135 @@ export class SettingsSelectorComponent extends Container {
215
203
  this.addChild(bottomBorder);
216
204
  }
217
205
 
218
- private showConfigTab(): void {
219
- const supportsImages = getCapabilities().images;
206
+ /**
207
+ * Convert a setting definition to a SettingItem for the UI.
208
+ */
209
+ private defToItem(def: SettingDef): SettingItem | null {
210
+ // Check condition
211
+ if (def.type === "boolean" && def.condition && !def.condition()) {
212
+ return null;
213
+ }
220
214
 
221
- const items: SettingItem[] = [
222
- {
223
- id: "autocompact",
224
- label: "Auto-compact",
225
- description: "Automatically compact context when it gets too large",
226
- currentValue: this.config.autoCompact ? "true" : "false",
227
- values: ["true", "false"],
228
- },
229
- {
230
- id: "queue-mode",
231
- label: "Queue mode",
232
- description: "How to process queued messages while agent is working",
233
- currentValue: this.config.queueMode,
234
- values: ["one-at-a-time", "all"],
235
- },
236
- {
237
- id: "hide-thinking",
238
- label: "Hide thinking",
239
- description: "Hide thinking blocks in assistant responses",
240
- currentValue: this.config.hideThinkingBlock ? "true" : "false",
241
- values: ["true", "false"],
242
- },
243
- {
244
- id: "collapse-changelog",
245
- label: "Collapse changelog",
246
- description: "Show condensed changelog after updates",
247
- currentValue: this.config.collapseChangelog ? "true" : "false",
248
- values: ["true", "false"],
249
- },
250
- {
251
- id: "thinking",
252
- label: "Thinking level",
253
- description: "Reasoning depth for thinking-capable models",
254
- currentValue: this.config.thinkingLevel,
255
- submenu: (currentValue, done) =>
256
- new SelectSubmenu(
257
- "Thinking Level",
258
- "Select reasoning depth for thinking-capable models",
259
- this.config.availableThinkingLevels.map((level) => ({
260
- value: level,
261
- label: level,
262
- description: THINKING_DESCRIPTIONS[level],
263
- })),
264
- currentValue,
265
- (value) => {
266
- this.callbacks.onThinkingLevelChange(value as ThinkingLevel);
267
- done(value);
268
- },
269
- () => done(),
270
- ),
271
- },
272
- {
273
- id: "theme",
274
- label: "Theme",
275
- description: "Color theme for the interface",
276
- currentValue: this.config.currentTheme,
277
- submenu: (currentValue, done) =>
278
- new SelectSubmenu(
279
- "Theme",
280
- "Select color theme",
281
- this.config.availableThemes.map((t) => ({
282
- value: t,
283
- label: t,
284
- })),
285
- currentValue,
286
- (value) => {
287
- this.callbacks.onThemeChange(value);
288
- done(value);
289
- },
290
- () => {
291
- this.callbacks.onThemePreview?.(currentValue);
292
- done();
293
- },
294
- (value) => {
295
- this.callbacks.onThemePreview?.(value);
296
- },
297
- ),
298
- },
299
- ];
300
-
301
- // Add image toggle if supported
302
- if (supportsImages) {
303
- items.splice(1, 0, {
304
- id: "show-images",
305
- label: "Show images",
306
- description: "Render images inline in terminal",
307
- currentValue: this.config.showImages ? "true" : "false",
308
- values: ["true", "false"],
215
+ const currentValue = this.getCurrentValue(def);
216
+
217
+ switch (def.type) {
218
+ case "boolean":
219
+ return {
220
+ id: def.id,
221
+ label: def.label,
222
+ description: def.description,
223
+ currentValue: currentValue ? "true" : "false",
224
+ values: ["true", "false"],
225
+ };
226
+
227
+ case "enum":
228
+ return {
229
+ id: def.id,
230
+ label: def.label,
231
+ description: def.description,
232
+ currentValue: currentValue as string,
233
+ values: [...def.values],
234
+ };
235
+
236
+ case "submenu":
237
+ return {
238
+ id: def.id,
239
+ label: def.label,
240
+ description: def.description,
241
+ currentValue: currentValue as string,
242
+ submenu: (cv, done) => this.createSubmenu(def, cv, done),
243
+ };
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Get the current value for a setting, using runtime context for special cases.
249
+ */
250
+ private getCurrentValue(def: SettingDef): string | boolean {
251
+ // Special cases that come from runtime context instead of SettingsManager
252
+ switch (def.id) {
253
+ case "thinkingLevel":
254
+ return this.context.thinkingLevel;
255
+ default:
256
+ return def.get(this.settingsManager);
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Create a submenu for a submenu-type setting.
262
+ */
263
+ private createSubmenu(
264
+ def: SettingDef & { type: "submenu" },
265
+ currentValue: string,
266
+ done: (value?: string) => void,
267
+ ): Container {
268
+ let options = def.getOptions(this.settingsManager);
269
+
270
+ // Special case: inject runtime options
271
+ if (def.id === "thinkingLevel") {
272
+ options = this.context.availableThinkingLevels.map((level) => {
273
+ const baseOpt = def.getOptions(this.settingsManager).find((o) => o.value === level);
274
+ return baseOpt || { value: level, label: level };
309
275
  });
276
+ } else if (def.id === "theme") {
277
+ options = this.context.availableThemes.map((t) => ({ value: t, label: t }));
310
278
  }
311
279
 
312
- this.currentList = new SettingsList(
313
- items,
314
- 10,
315
- getSettingsListTheme(),
316
- (id, newValue) => {
317
- switch (id) {
318
- case "autocompact":
319
- this.callbacks.onAutoCompactChange(newValue === "true");
320
- break;
321
- case "show-images":
322
- this.callbacks.onShowImagesChange(newValue === "true");
323
- break;
324
- case "queue-mode":
325
- this.callbacks.onQueueModeChange(newValue as "all" | "one-at-a-time");
326
- break;
327
- case "hide-thinking":
328
- this.callbacks.onHideThinkingBlockChange(newValue === "true");
329
- break;
330
- case "collapse-changelog":
331
- this.callbacks.onCollapseChangelogChange(newValue === "true");
332
- break;
333
- }
280
+ const onPreview = def.id === "theme" ? this.callbacks.onThemePreview : undefined;
281
+ const onPreviewCancel = def.id === "theme" ? () => this.callbacks.onThemePreview?.(currentValue) : undefined;
282
+
283
+ return new SelectSubmenu(
284
+ def.label,
285
+ def.description,
286
+ options,
287
+ currentValue,
288
+ (value) => {
289
+ // Persist to SettingsManager
290
+ def.set(this.settingsManager, value);
291
+ // Notify for side effects
292
+ this.callbacks.onChange(def.id, value);
293
+ done(value);
334
294
  },
335
- () => this.callbacks.onCancel(),
295
+ () => {
296
+ onPreviewCancel?.();
297
+ done();
298
+ },
299
+ onPreview,
336
300
  );
337
-
338
- this.addChild(this.currentList);
339
301
  }
340
302
 
341
- private showExaTab(): void {
342
- const items: SettingItem[] = [
343
- {
344
- id: "exa-enabled",
345
- label: "Exa enabled",
346
- description: "Master toggle for all Exa search tools",
347
- currentValue: this.config.exa.enabled ? "true" : "false",
348
- values: ["true", "false"],
349
- },
350
- {
351
- id: "exa-search",
352
- label: "Exa search",
353
- description: "Basic search, deep search, code search, crawl",
354
- currentValue: this.config.exa.enableSearch ? "true" : "false",
355
- values: ["true", "false"],
356
- },
357
- {
358
- id: "exa-linkedin",
359
- label: "Exa LinkedIn",
360
- description: "Search LinkedIn for people and companies",
361
- currentValue: this.config.exa.enableLinkedin ? "true" : "false",
362
- values: ["true", "false"],
363
- },
364
- {
365
- id: "exa-company",
366
- label: "Exa company",
367
- description: "Comprehensive company research tool",
368
- currentValue: this.config.exa.enableCompany ? "true" : "false",
369
- values: ["true", "false"],
370
- },
371
- {
372
- id: "exa-researcher",
373
- label: "Exa researcher",
374
- description: "AI-powered deep research tasks",
375
- currentValue: this.config.exa.enableResearcher ? "true" : "false",
376
- values: ["true", "false"],
377
- },
378
- {
379
- id: "exa-websets",
380
- label: "Exa websets",
381
- description: "Webset management and enrichment tools",
382
- currentValue: this.config.exa.enableWebsets ? "true" : "false",
383
- values: ["true", "false"],
384
- },
385
- ];
303
+ /**
304
+ * Show a settings tab (config or exa) using definitions.
305
+ */
306
+ private showSettingsTab(tabId: "config" | "exa"): void {
307
+ const defs = getSettingsForTab(tabId);
308
+ const items: SettingItem[] = [];
309
+
310
+ for (const def of defs) {
311
+ const item = this.defToItem(def);
312
+ if (item) {
313
+ items.push(item);
314
+ }
315
+ }
386
316
 
387
317
  this.currentList = new SettingsList(
388
318
  items,
389
319
  10,
390
320
  getSettingsListTheme(),
391
321
  (id, newValue) => {
392
- const enabled = newValue === "true";
393
- switch (id) {
394
- case "exa-enabled":
395
- this.callbacks.onExaSettingChange("enabled", enabled);
396
- break;
397
- case "exa-search":
398
- this.callbacks.onExaSettingChange("enableSearch", enabled);
399
- break;
400
- case "exa-linkedin":
401
- this.callbacks.onExaSettingChange("enableLinkedin", enabled);
402
- break;
403
- case "exa-company":
404
- this.callbacks.onExaSettingChange("enableCompany", enabled);
405
- break;
406
- case "exa-researcher":
407
- this.callbacks.onExaSettingChange("enableResearcher", enabled);
408
- break;
409
- case "exa-websets":
410
- this.callbacks.onExaSettingChange("enableWebsets", enabled);
411
- break;
322
+ const def = defs.find((d) => d.id === id);
323
+ if (!def) return;
324
+
325
+ // Persist to SettingsManager based on type
326
+ if (def.type === "boolean") {
327
+ const boolValue = newValue === "true";
328
+ def.set(this.settingsManager, boolValue);
329
+ this.callbacks.onChange(id, boolValue);
330
+ } else if (def.type === "enum") {
331
+ def.set(this.settingsManager, newValue);
332
+ this.callbacks.onChange(id, newValue);
412
333
  }
334
+ // Submenu types are handled in createSubmenu
413
335
  },
414
336
  () => this.callbacks.onCancel(),
415
337
  );
@@ -418,7 +340,7 @@ export class SettingsSelectorComponent extends Container {
418
340
  }
419
341
 
420
342
  private showPluginsTab(): void {
421
- this.pluginComponent = new PluginSettingsComponent(this.config.cwd, {
343
+ this.pluginComponent = new PluginSettingsComponent(this.context.cwd, {
422
344
  onClose: () => this.callbacks.onCancel(),
423
345
  onPluginChanged: () => this.callbacks.onPluginsChanged?.(),
424
346
  });
@@ -6,7 +6,7 @@
6
6
  import * as fs from "node:fs";
7
7
  import * as os from "node:os";
8
8
  import * as path from "node:path";
9
- import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
9
+ import type { AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
10
10
  import type { AssistantMessage, ImageContent, Message, OAuthProvider } from "@oh-my-pi/pi-ai";
11
11
  import type { SlashCommand } from "@oh-my-pi/pi-tui";
12
12
  import {
@@ -1597,48 +1597,15 @@ export class InteractiveMode {
1597
1597
  private showSettingsSelector(): void {
1598
1598
  this.showSelector((done) => {
1599
1599
  const selector = new SettingsSelectorComponent(
1600
+ this.settingsManager,
1600
1601
  {
1601
- autoCompact: this.session.autoCompactionEnabled,
1602
- showImages: this.settingsManager.getShowImages(),
1603
- queueMode: this.session.queueMode,
1604
- thinkingLevel: this.session.thinkingLevel,
1605
1602
  availableThinkingLevels: this.session.getAvailableThinkingLevels(),
1606
- currentTheme: this.settingsManager.getTheme() || "dark",
1603
+ thinkingLevel: this.session.thinkingLevel,
1607
1604
  availableThemes: getAvailableThemes(),
1608
- hideThinkingBlock: this.hideThinkingBlock,
1609
- collapseChangelog: this.settingsManager.getCollapseChangelog(),
1610
1605
  cwd: process.cwd(),
1611
- exa: this.settingsManager.getExaSettings(),
1612
1606
  },
1613
1607
  {
1614
- onAutoCompactChange: (enabled) => {
1615
- this.session.setAutoCompactionEnabled(enabled);
1616
- this.footer.setAutoCompactEnabled(enabled);
1617
- },
1618
- onShowImagesChange: (enabled) => {
1619
- this.settingsManager.setShowImages(enabled);
1620
- for (const child of this.chatContainer.children) {
1621
- if (child instanceof ToolExecutionComponent) {
1622
- child.setShowImages(enabled);
1623
- }
1624
- }
1625
- },
1626
- onQueueModeChange: (mode) => {
1627
- this.session.setQueueMode(mode);
1628
- },
1629
- onThinkingLevelChange: (level) => {
1630
- this.session.setThinkingLevel(level);
1631
- this.footer.invalidate();
1632
- this.updateEditorBorderColor();
1633
- },
1634
- onThemeChange: (themeName) => {
1635
- const result = setTheme(themeName, true);
1636
- this.settingsManager.setTheme(themeName);
1637
- this.ui.invalidate();
1638
- if (!result.success) {
1639
- this.showError(`Failed to load theme "${themeName}": ${result.error}\nFell back to dark theme.`);
1640
- }
1641
- },
1608
+ onChange: (id, value) => this.handleSettingChange(id, value),
1642
1609
  onThemePreview: (themeName) => {
1643
1610
  const result = setTheme(themeName, true);
1644
1611
  if (result.success) {
@@ -1646,46 +1613,9 @@ export class InteractiveMode {
1646
1613
  this.ui.requestRender();
1647
1614
  }
1648
1615
  },
1649
- onHideThinkingBlockChange: (hidden) => {
1650
- this.hideThinkingBlock = hidden;
1651
- this.settingsManager.setHideThinkingBlock(hidden);
1652
- for (const child of this.chatContainer.children) {
1653
- if (child instanceof AssistantMessageComponent) {
1654
- child.setHideThinkingBlock(hidden);
1655
- }
1656
- }
1657
- this.chatContainer.clear();
1658
- this.rebuildChatFromMessages();
1659
- },
1660
- onCollapseChangelogChange: (collapsed) => {
1661
- this.settingsManager.setCollapseChangelog(collapsed);
1662
- },
1663
1616
  onPluginsChanged: () => {
1664
- // Plugin config changed - could trigger reload if needed
1665
1617
  this.ui.requestRender();
1666
1618
  },
1667
- onExaSettingChange: (setting, enabled) => {
1668
- switch (setting) {
1669
- case "enabled":
1670
- this.settingsManager.setExaEnabled(enabled);
1671
- break;
1672
- case "enableSearch":
1673
- this.settingsManager.setExaSearchEnabled(enabled);
1674
- break;
1675
- case "enableLinkedin":
1676
- this.settingsManager.setExaLinkedinEnabled(enabled);
1677
- break;
1678
- case "enableCompany":
1679
- this.settingsManager.setExaCompanyEnabled(enabled);
1680
- break;
1681
- case "enableResearcher":
1682
- this.settingsManager.setExaResearcherEnabled(enabled);
1683
- break;
1684
- case "enableWebsets":
1685
- this.settingsManager.setExaWebsetsEnabled(enabled);
1686
- break;
1687
- }
1688
- },
1689
1619
  onCancel: () => {
1690
1620
  done();
1691
1621
  this.ui.requestRender();
@@ -1696,6 +1626,59 @@ export class InteractiveMode {
1696
1626
  });
1697
1627
  }
1698
1628
 
1629
+ /**
1630
+ * Handle setting changes from the settings selector.
1631
+ * Most settings are saved directly via SettingsManager in the definitions.
1632
+ * This handles side effects and session-specific settings.
1633
+ */
1634
+ private handleSettingChange(id: string, value: string | boolean): void {
1635
+ switch (id) {
1636
+ // Session-managed settings (not in SettingsManager)
1637
+ case "autoCompact":
1638
+ this.session.setAutoCompactionEnabled(value as boolean);
1639
+ this.footer.setAutoCompactEnabled(value as boolean);
1640
+ break;
1641
+ case "queueMode":
1642
+ this.session.setQueueMode(value as "all" | "one-at-a-time");
1643
+ break;
1644
+ case "thinkingLevel":
1645
+ this.session.setThinkingLevel(value as ThinkingLevel);
1646
+ this.footer.invalidate();
1647
+ this.updateEditorBorderColor();
1648
+ break;
1649
+
1650
+ // Settings with UI side effects
1651
+ case "showImages":
1652
+ for (const child of this.chatContainer.children) {
1653
+ if (child instanceof ToolExecutionComponent) {
1654
+ child.setShowImages(value as boolean);
1655
+ }
1656
+ }
1657
+ break;
1658
+ case "hideThinking":
1659
+ this.hideThinkingBlock = value as boolean;
1660
+ for (const child of this.chatContainer.children) {
1661
+ if (child instanceof AssistantMessageComponent) {
1662
+ child.setHideThinkingBlock(value as boolean);
1663
+ }
1664
+ }
1665
+ this.chatContainer.clear();
1666
+ this.rebuildChatFromMessages();
1667
+ break;
1668
+ case "theme": {
1669
+ const result = setTheme(value as string, true);
1670
+ this.ui.invalidate();
1671
+ if (!result.success) {
1672
+ this.showError(`Failed to load theme "${value}": ${result.error}\nFell back to dark theme.`);
1673
+ }
1674
+ break;
1675
+ }
1676
+
1677
+ // All other settings are handled by the definitions (get/set on SettingsManager)
1678
+ // No additional side effects needed
1679
+ }
1680
+ }
1681
+
1699
1682
  private showModelSelector(): void {
1700
1683
  this.showSelector((done) => {
1701
1684
  const selector = new ModelSelectorComponent(
@@ -2139,7 +2122,7 @@ export class InteractiveMode {
2139
2122
  }
2140
2123
 
2141
2124
  // Create the preview URL
2142
- const previewUrl = `https://shittycodingagent.ai/session?${gistId}`;
2125
+ const previewUrl = `https://gistpreview.github.io/?${gistId}`;
2143
2126
  this.showStatus(`Share URL: ${previewUrl}\nGist: ${gistUrl}`);
2144
2127
  } catch (error: unknown) {
2145
2128
  if (!loader.signal.aborted) {
@@ -83,13 +83,21 @@ export function getShellConfig(): { shell: string; args: string[] } {
83
83
  );
84
84
  }
85
85
 
86
- // Unix: prefer bash over sh
87
- if (existsSync("/bin/bash")) {
88
- cachedShellConfig = { shell: "/bin/bash", args: ["-c"] };
86
+ // Unix: prefer user's shell from $SHELL, fallback to bash, then sh
87
+ const userShell = process.env.SHELL;
88
+ if (userShell && existsSync(userShell)) {
89
+ cachedShellConfig = { shell: userShell, args: ["-c"] };
89
90
  return cachedShellConfig;
90
91
  }
91
92
 
92
- cachedShellConfig = { shell: "sh", args: ["-c"] };
93
+ const bashPath = Bun.which("bash");
94
+ if (bashPath) {
95
+ cachedShellConfig = { shell: bashPath, args: ["-c"] };
96
+ return cachedShellConfig;
97
+ }
98
+
99
+ const shPath = Bun.which("sh");
100
+ cachedShellConfig = { shell: shPath || "sh", args: ["-c"] };
93
101
  return cachedShellConfig;
94
102
  }
95
103