@oh-my-pi/pi-coding-agent 1.337.1 → 1.338.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.
- package/CHANGELOG.md +15 -1
- package/package.json +1 -1
- package/src/cli/args.ts +14 -8
- package/src/core/sdk.ts +4 -2
- package/src/core/settings-manager.ts +17 -0
- package/src/modes/interactive/components/settings-defs.ts +220 -0
- package/src/modes/interactive/components/settings-selector.ts +156 -234
- package/src/modes/interactive/interactive-mode.ts +57 -74
- package/src/utils/shell.ts +12 -4
- package/src/utils/tools-manager.ts +5 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [
|
|
3
|
+
## [1.338.0] - 2026-01-03
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Bash interceptor setting to block shell commands that have dedicated tools (disabled by default, enable via `/settings`)
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Refactored settings UI to declarative definitions for easier maintenance
|
|
12
|
+
- Shell detection now respects `$SHELL` environment variable before falling back to bash/sh
|
|
13
|
+
- Tool binary detection now uses `Bun.which()` instead of spawning processes
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- CLI help text now accurately lists all default tools
|
|
4
18
|
|
|
5
19
|
## [1.337.1] - 2026-01-02
|
|
6
20
|
|
package/package.json
CHANGED
package/src/cli/args.ts
CHANGED
|
@@ -234,13 +234,19 @@ ${chalk.bold("Environment Variables:")}
|
|
|
234
234
|
${chalk.dim("# Configuration")}
|
|
235
235
|
${ENV_AGENT_DIR.padEnd(23)} - Session storage directory (default: ~/${CONFIG_DIR_NAME}/agent)
|
|
236
236
|
|
|
237
|
-
${chalk.bold("Available Tools (
|
|
238
|
-
read
|
|
239
|
-
bash
|
|
240
|
-
edit
|
|
241
|
-
write
|
|
242
|
-
grep
|
|
243
|
-
find
|
|
244
|
-
ls
|
|
237
|
+
${chalk.bold("Available Tools (all enabled by default):")}
|
|
238
|
+
read - Read file contents
|
|
239
|
+
bash - Execute bash commands
|
|
240
|
+
edit - Edit files with find/replace
|
|
241
|
+
write - Write files (creates/overwrites)
|
|
242
|
+
grep - Search file contents
|
|
243
|
+
find - Find files by glob pattern
|
|
244
|
+
ls - List directory contents
|
|
245
|
+
lsp - Language server protocol (code intelligence)
|
|
246
|
+
notebook - Edit Jupyter notebooks
|
|
247
|
+
task - Launch sub-agents for parallel tasks
|
|
248
|
+
web_fetch - Fetch and process web pages
|
|
249
|
+
web_search - Search the web
|
|
250
|
+
ask - Ask user questions (interactive mode only)
|
|
245
251
|
`);
|
|
246
252
|
}
|
package/src/core/sdk.ts
CHANGED
|
@@ -664,8 +664,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
664
664
|
let allToolsArray: Tool[] = [...builtInTools, ...wrappedCustomTools];
|
|
665
665
|
time("combineTools");
|
|
666
666
|
|
|
667
|
-
// Apply bash interception to redirect common shell patterns to proper tools
|
|
668
|
-
|
|
667
|
+
// Apply bash interception to redirect common shell patterns to proper tools (if enabled)
|
|
668
|
+
if (settingsManager.getBashInterceptorEnabled()) {
|
|
669
|
+
allToolsArray = applyBashInterception(allToolsArray);
|
|
670
|
+
}
|
|
669
671
|
time("applyBashInterception");
|
|
670
672
|
|
|
671
673
|
if (hookRunner) {
|
|
@@ -43,6 +43,10 @@ export interface ExaSettings {
|
|
|
43
43
|
enableWebsets?: boolean; // default: false
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
export interface BashInterceptorSettings {
|
|
47
|
+
enabled?: boolean; // default: false (blocks shell commands that have dedicated tools)
|
|
48
|
+
}
|
|
49
|
+
|
|
46
50
|
export interface Settings {
|
|
47
51
|
lastChangelogVersion?: string;
|
|
48
52
|
defaultProvider?: string;
|
|
@@ -62,6 +66,7 @@ export interface Settings {
|
|
|
62
66
|
terminal?: TerminalSettings;
|
|
63
67
|
enabledModels?: string[]; // Model patterns for cycling (same format as --models CLI flag)
|
|
64
68
|
exa?: ExaSettings;
|
|
69
|
+
bashInterceptor?: BashInterceptorSettings;
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
/** Deep merge settings: project/overrides take precedence, nested objects merge recursively */
|
|
@@ -440,4 +445,16 @@ export class SettingsManager {
|
|
|
440
445
|
this.globalSettings.exa.enableWebsets = enabled;
|
|
441
446
|
this.save();
|
|
442
447
|
}
|
|
448
|
+
|
|
449
|
+
getBashInterceptorEnabled(): boolean {
|
|
450
|
+
return this.settings.bashInterceptor?.enabled ?? false;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
setBashInterceptorEnabled(enabled: boolean): void {
|
|
454
|
+
if (!this.globalSettings.bashInterceptor) {
|
|
455
|
+
this.globalSettings.bashInterceptor = {};
|
|
456
|
+
}
|
|
457
|
+
this.globalSettings.bashInterceptor.enabled = enabled;
|
|
458
|
+
this.save();
|
|
459
|
+
}
|
|
443
460
|
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative settings definitions.
|
|
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
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
12
|
+
import { getCapabilities } from "@oh-my-pi/pi-tui";
|
|
13
|
+
import type { SettingsManager } from "../../../core/settings-manager.js";
|
|
14
|
+
|
|
15
|
+
// Setting value types
|
|
16
|
+
export type SettingValue = boolean | string;
|
|
17
|
+
|
|
18
|
+
// Base definition for all settings
|
|
19
|
+
interface BaseSettingDef {
|
|
20
|
+
id: string;
|
|
21
|
+
label: string;
|
|
22
|
+
description: string;
|
|
23
|
+
tab: "config" | "exa";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Boolean toggle setting
|
|
27
|
+
export interface BooleanSettingDef extends BaseSettingDef {
|
|
28
|
+
type: "boolean";
|
|
29
|
+
get: (sm: SettingsManager) => boolean;
|
|
30
|
+
set: (sm: SettingsManager, value: boolean) => void;
|
|
31
|
+
/** If provided, setting is only shown when this returns true */
|
|
32
|
+
condition?: () => boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Enum setting (inline toggle between values)
|
|
36
|
+
export interface EnumSettingDef extends BaseSettingDef {
|
|
37
|
+
type: "enum";
|
|
38
|
+
values: readonly string[];
|
|
39
|
+
get: (sm: SettingsManager) => string;
|
|
40
|
+
set: (sm: SettingsManager, value: string) => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Submenu setting (opens a selection list)
|
|
44
|
+
export interface SubmenuSettingDef extends BaseSettingDef {
|
|
45
|
+
type: "submenu";
|
|
46
|
+
get: (sm: SettingsManager) => string;
|
|
47
|
+
set: (sm: SettingsManager, value: string) => void;
|
|
48
|
+
/** Get available options dynamically */
|
|
49
|
+
getOptions: (sm: SettingsManager) => Array<{ value: string; label: string; description?: string }>;
|
|
50
|
+
/** Called when selection changes (for preview) */
|
|
51
|
+
onPreview?: (value: string) => void;
|
|
52
|
+
/** Called when submenu is cancelled (to restore preview) */
|
|
53
|
+
onPreviewCancel?: (originalValue: string) => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type SettingDef = BooleanSettingDef | EnumSettingDef | SubmenuSettingDef;
|
|
57
|
+
|
|
58
|
+
const THINKING_DESCRIPTIONS: Record<ThinkingLevel, string> = {
|
|
59
|
+
off: "No reasoning",
|
|
60
|
+
minimal: "Very brief reasoning (~1k tokens)",
|
|
61
|
+
low: "Light reasoning (~2k tokens)",
|
|
62
|
+
medium: "Moderate reasoning (~8k tokens)",
|
|
63
|
+
high: "Deep reasoning (~16k tokens)",
|
|
64
|
+
xhigh: "Maximum reasoning (~32k tokens)",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* All settings definitions.
|
|
69
|
+
* Order determines display order within each tab.
|
|
70
|
+
*/
|
|
71
|
+
export const SETTINGS_DEFS: SettingDef[] = [
|
|
72
|
+
// Config tab
|
|
73
|
+
{
|
|
74
|
+
id: "autoCompact",
|
|
75
|
+
tab: "config",
|
|
76
|
+
type: "boolean",
|
|
77
|
+
label: "Auto-compact",
|
|
78
|
+
description: "Automatically compact context when it gets too large",
|
|
79
|
+
get: (sm) => sm.getCompactionEnabled(),
|
|
80
|
+
set: (sm, v) => sm.setCompactionEnabled(v), // Also handled in session
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: "showImages",
|
|
84
|
+
tab: "config",
|
|
85
|
+
type: "boolean",
|
|
86
|
+
label: "Show images",
|
|
87
|
+
description: "Render images inline in terminal",
|
|
88
|
+
get: (sm) => sm.getShowImages(),
|
|
89
|
+
set: (sm, v) => sm.setShowImages(v),
|
|
90
|
+
condition: () => !!getCapabilities().images,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "queueMode",
|
|
94
|
+
tab: "config",
|
|
95
|
+
type: "enum",
|
|
96
|
+
label: "Queue mode",
|
|
97
|
+
description: "How to process queued messages while agent is working",
|
|
98
|
+
values: ["one-at-a-time", "all"],
|
|
99
|
+
get: (sm) => sm.getQueueMode(),
|
|
100
|
+
set: (sm, v) => sm.setQueueMode(v as "all" | "one-at-a-time"), // Also handled in session
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: "hideThinking",
|
|
104
|
+
tab: "config",
|
|
105
|
+
type: "boolean",
|
|
106
|
+
label: "Hide thinking",
|
|
107
|
+
description: "Hide thinking blocks in assistant responses",
|
|
108
|
+
get: (sm) => sm.getHideThinkingBlock(),
|
|
109
|
+
set: (sm, v) => sm.setHideThinkingBlock(v),
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "collapseChangelog",
|
|
113
|
+
tab: "config",
|
|
114
|
+
type: "boolean",
|
|
115
|
+
label: "Collapse changelog",
|
|
116
|
+
description: "Show condensed changelog after updates",
|
|
117
|
+
get: (sm) => sm.getCollapseChangelog(),
|
|
118
|
+
set: (sm, v) => sm.setCollapseChangelog(v),
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: "bashInterceptor",
|
|
122
|
+
tab: "config",
|
|
123
|
+
type: "boolean",
|
|
124
|
+
label: "Bash interceptor",
|
|
125
|
+
description: "Block shell commands that have dedicated tools (grep, cat, etc.)",
|
|
126
|
+
get: (sm) => sm.getBashInterceptorEnabled(),
|
|
127
|
+
set: (sm, v) => sm.setBashInterceptorEnabled(v),
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: "thinkingLevel",
|
|
131
|
+
tab: "config",
|
|
132
|
+
type: "submenu",
|
|
133
|
+
label: "Thinking level",
|
|
134
|
+
description: "Reasoning depth for thinking-capable models",
|
|
135
|
+
get: (sm) => sm.getDefaultThinkingLevel() ?? "off",
|
|
136
|
+
set: (sm, v) => sm.setDefaultThinkingLevel(v as ThinkingLevel), // Also handled in session
|
|
137
|
+
getOptions: () =>
|
|
138
|
+
(["off", "minimal", "low", "medium", "high", "xhigh"] as ThinkingLevel[]).map((level) => ({
|
|
139
|
+
value: level,
|
|
140
|
+
label: level,
|
|
141
|
+
description: THINKING_DESCRIPTIONS[level],
|
|
142
|
+
})),
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: "theme",
|
|
146
|
+
tab: "config",
|
|
147
|
+
type: "submenu",
|
|
148
|
+
label: "Theme",
|
|
149
|
+
description: "Color theme for the interface",
|
|
150
|
+
get: (sm) => sm.getTheme() ?? "dark",
|
|
151
|
+
set: (sm, v) => sm.setTheme(v),
|
|
152
|
+
getOptions: () => [], // Filled dynamically from context
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
// Exa tab
|
|
156
|
+
{
|
|
157
|
+
id: "exaEnabled",
|
|
158
|
+
tab: "exa",
|
|
159
|
+
type: "boolean",
|
|
160
|
+
label: "Exa enabled",
|
|
161
|
+
description: "Master toggle for all Exa search tools",
|
|
162
|
+
get: (sm) => sm.getExaSettings().enabled,
|
|
163
|
+
set: (sm, v) => sm.setExaEnabled(v),
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: "exaSearch",
|
|
167
|
+
tab: "exa",
|
|
168
|
+
type: "boolean",
|
|
169
|
+
label: "Exa search",
|
|
170
|
+
description: "Basic search, deep search, code search, crawl",
|
|
171
|
+
get: (sm) => sm.getExaSettings().enableSearch,
|
|
172
|
+
set: (sm, v) => sm.setExaSearchEnabled(v),
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: "exaLinkedin",
|
|
176
|
+
tab: "exa",
|
|
177
|
+
type: "boolean",
|
|
178
|
+
label: "Exa LinkedIn",
|
|
179
|
+
description: "Search LinkedIn for people and companies",
|
|
180
|
+
get: (sm) => sm.getExaSettings().enableLinkedin,
|
|
181
|
+
set: (sm, v) => sm.setExaLinkedinEnabled(v),
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: "exaCompany",
|
|
185
|
+
tab: "exa",
|
|
186
|
+
type: "boolean",
|
|
187
|
+
label: "Exa company",
|
|
188
|
+
description: "Comprehensive company research tool",
|
|
189
|
+
get: (sm) => sm.getExaSettings().enableCompany,
|
|
190
|
+
set: (sm, v) => sm.setExaCompanyEnabled(v),
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: "exaResearcher",
|
|
194
|
+
tab: "exa",
|
|
195
|
+
type: "boolean",
|
|
196
|
+
label: "Exa researcher",
|
|
197
|
+
description: "AI-powered deep research tasks",
|
|
198
|
+
get: (sm) => sm.getExaSettings().enableResearcher,
|
|
199
|
+
set: (sm, v) => sm.setExaResearcherEnabled(v),
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: "exaWebsets",
|
|
203
|
+
tab: "exa",
|
|
204
|
+
type: "boolean",
|
|
205
|
+
label: "Exa websets",
|
|
206
|
+
description: "Webset management and enrichment tools",
|
|
207
|
+
get: (sm) => sm.getExaSettings().enableWebsets,
|
|
208
|
+
set: (sm, v) => sm.setExaWebsetsEnabled(v),
|
|
209
|
+
},
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
/** Get settings for a specific tab */
|
|
213
|
+
export function getSettingsForTab(tab: "config" | "exa"): SettingDef[] {
|
|
214
|
+
return SETTINGS_DEFS.filter((def) => def.tab === tab);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Get a setting definition by id */
|
|
218
|
+
export function getSettingDef(id: string): SettingDef | undefined {
|
|
219
|
+
return SETTINGS_DEFS.find((def) => def.id === id);
|
|
220
|
+
}
|
|
@@ -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
|
|
146
|
+
private settingsManager: SettingsManager;
|
|
147
|
+
private context: SettingsRuntimeContext;
|
|
159
148
|
private callbacks: SettingsCallbacks;
|
|
160
149
|
|
|
161
|
-
constructor(
|
|
150
|
+
constructor(settingsManager: SettingsManager, context: SettingsRuntimeContext, callbacks: SettingsCallbacks) {
|
|
162
151
|
super();
|
|
163
152
|
|
|
164
|
-
this.
|
|
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.
|
|
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
|
-
|
|
219
|
-
|
|
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
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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.
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
() =>
|
|
295
|
+
() => {
|
|
296
|
+
onPreviewCancel?.();
|
|
297
|
+
done();
|
|
298
|
+
},
|
|
299
|
+
onPreview,
|
|
336
300
|
);
|
|
337
|
-
|
|
338
|
-
this.addChild(this.currentList);
|
|
339
301
|
}
|
|
340
302
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
{
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
package/src/utils/shell.ts
CHANGED
|
@@ -83,13 +83,21 @@ export function getShellConfig(): { shell: string; args: string[] } {
|
|
|
83
83
|
);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
// Unix: prefer bash
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -95,14 +95,9 @@ const TOOLS: Record<string, ToolConfig> = {
|
|
|
95
95
|
},
|
|
96
96
|
};
|
|
97
97
|
|
|
98
|
-
// Check if a command exists in PATH
|
|
99
|
-
function commandExists(cmd: string):
|
|
100
|
-
|
|
101
|
-
const proc = Bun.spawnSync([cmd, "--version"], { stdin: "ignore", stdout: "pipe", stderr: "pipe" });
|
|
102
|
-
return proc.exitCode !== null;
|
|
103
|
-
} catch {
|
|
104
|
-
return false;
|
|
105
|
-
}
|
|
98
|
+
// Check if a command exists in PATH
|
|
99
|
+
function commandExists(cmd: string): string | null {
|
|
100
|
+
return Bun.which(cmd);
|
|
106
101
|
}
|
|
107
102
|
|
|
108
103
|
// Get the path to a tool (system-wide or in our tools dir)
|
|
@@ -116,12 +111,8 @@ export function getToolPath(tool: "fd" | "rg" | "sd" | "sg"): string | null {
|
|
|
116
111
|
return localPath;
|
|
117
112
|
}
|
|
118
113
|
|
|
119
|
-
// Check system PATH
|
|
120
|
-
|
|
121
|
-
return config.binaryName;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return null;
|
|
114
|
+
// Check system PATH
|
|
115
|
+
return commandExists(config.binaryName);
|
|
125
116
|
}
|
|
126
117
|
|
|
127
118
|
// Fetch latest release version from GitHub
|