@oh-my-pi/pi-coding-agent 9.4.0 → 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.
- package/CHANGELOG.md +84 -0
- package/package.json +9 -8
- package/src/capability/index.ts +7 -9
- package/src/cli/config-cli.ts +86 -73
- package/src/cli/update-cli.ts +45 -3
- package/src/commit/agentic/agent.ts +4 -4
- package/src/commit/agentic/index.ts +6 -5
- package/src/commit/agentic/tools/analyze-file.ts +5 -7
- package/src/commit/agentic/tools/index.ts +3 -3
- package/src/commit/model-selection.ts +13 -17
- package/src/commit/pipeline.ts +5 -5
- package/src/config/model-registry.ts +7 -0
- package/src/config/settings-schema.ts +836 -0
- package/src/config/settings.ts +702 -0
- package/src/discovery/helpers.ts +55 -11
- package/src/exa/index.ts +1 -1
- package/src/exec/bash-executor.ts +13 -13
- package/src/exec/shell-session.ts +15 -3
- package/src/export/ttsr.ts +1 -1
- package/src/extensibility/skills.ts +40 -9
- package/src/index.ts +2 -10
- package/src/ipy/gateway-coordinator.ts +5 -143
- package/src/ipy/kernel.ts +6 -171
- package/src/ipy/runtime.ts +198 -0
- package/src/lsp/client.ts +14 -1
- package/src/lsp/defaults.json +0 -6
- package/src/lsp/index.ts +1 -1
- package/src/lsp/types.ts +2 -0
- package/src/main.ts +26 -48
- package/src/modes/components/extensions/extension-dashboard.ts +22 -11
- package/src/modes/components/index.ts +1 -1
- package/src/modes/components/model-selector.ts +7 -7
- package/src/modes/components/settings-defs.ts +210 -915
- package/src/modes/components/settings-selector.ts +80 -106
- package/src/modes/components/status-line/types.ts +2 -8
- package/src/modes/components/status-line-segment-editor.ts +1 -1
- package/src/modes/components/status-line.ts +26 -3
- package/src/modes/controllers/event-controller.ts +9 -8
- package/src/modes/controllers/input-controller.ts +19 -15
- package/src/modes/controllers/selector-controller.ts +30 -14
- package/src/modes/interactive-mode.ts +10 -10
- package/src/modes/rpc/rpc-mode.ts +10 -0
- package/src/modes/rpc/rpc-types.ts +3 -0
- package/src/modes/types.ts +2 -2
- package/src/modes/utils/ui-helpers.ts +4 -3
- package/src/patch/index.ts +7 -7
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/bash.md +12 -2
- package/src/prompts/tools/task.md +180 -73
- package/src/sdk.ts +38 -61
- package/src/session/agent-session.ts +66 -55
- package/src/session/agent-storage.ts +1 -1
- package/src/session/session-manager.ts +10 -10
- package/src/system-prompt.ts +2 -2
- package/src/task/executor.ts +9 -9
- package/src/task/index.ts +2 -2
- package/src/tools/ask.ts +5 -6
- package/src/tools/bash-interceptor.ts +39 -1
- package/src/tools/bash-normalize.ts +126 -0
- package/src/tools/bash.ts +31 -5
- package/src/tools/find.ts +51 -33
- package/src/tools/index.ts +5 -23
- package/src/tools/plan-mode-guard.ts +1 -6
- package/src/tools/python.ts +2 -2
- package/src/tools/read.ts +2 -2
- package/src/tools/write.ts +2 -2
- package/src/utils/ignore-files.ts +119 -0
- package/src/web/search/providers/perplexity.ts +1 -1
- package/examples/sdk/10-settings.ts +0 -37
- package/src/config/settings-manager.ts +0 -2015
|
@@ -1,2015 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import { isEnoent, logger, procmgr } from "@oh-my-pi/pi-utils";
|
|
4
|
-
import { YAML } from "bun";
|
|
5
|
-
import { type Settings as SettingsItem, settingsCapability } from "../capability/settings";
|
|
6
|
-
import { getAgentDbPath, getAgentDir } from "../config";
|
|
7
|
-
import { loadCapability } from "../discovery";
|
|
8
|
-
import type { SymbolPreset } from "../modes/theme/theme";
|
|
9
|
-
import { AgentStorage } from "../session/agent-storage";
|
|
10
|
-
import { withFileLock } from "./file-lock";
|
|
11
|
-
|
|
12
|
-
export interface CompactionSettings {
|
|
13
|
-
enabled?: boolean; // default: true
|
|
14
|
-
reserveTokens?: number; // default: 16384
|
|
15
|
-
keepRecentTokens?: number; // default: 20000
|
|
16
|
-
autoContinue?: boolean; // default: true
|
|
17
|
-
remoteEndpoint?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface BranchSummarySettings {
|
|
21
|
-
enabled?: boolean; // default: false (prompt user to summarize when leaving branch)
|
|
22
|
-
reserveTokens?: number; // default: 16384 (tokens reserved for prompt + LLM response)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface RetrySettings {
|
|
26
|
-
enabled?: boolean; // default: true
|
|
27
|
-
maxRetries?: number; // default: 3
|
|
28
|
-
baseDelayMs?: number; // default: 2000 (exponential backoff: 2s, 4s, 8s)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface SkillsSettings {
|
|
32
|
-
enabled?: boolean; // default: true
|
|
33
|
-
enableSkillCommands?: boolean; // default: true - register skills as /skill:name commands
|
|
34
|
-
enableCodexUser?: boolean; // default: true
|
|
35
|
-
enableClaudeUser?: boolean; // default: true
|
|
36
|
-
enableClaudeProject?: boolean; // default: true
|
|
37
|
-
enablePiUser?: boolean; // default: true
|
|
38
|
-
enablePiProject?: boolean; // default: true
|
|
39
|
-
customDirectories?: string[]; // default: []
|
|
40
|
-
ignoredSkills?: string[]; // default: [] (glob patterns to exclude; takes precedence over includeSkills)
|
|
41
|
-
includeSkills?: string[]; // default: [] (empty = include all; glob patterns to filter)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface CommandsSettings {
|
|
45
|
-
enableClaudeUser?: boolean; // default: true (load from ~/.claude/commands/)
|
|
46
|
-
enableClaudeProject?: boolean; // default: true (load from .claude/commands/)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface TerminalSettings {
|
|
50
|
-
showImages?: boolean; // default: true (only relevant if terminal supports images)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface StartupSettings {
|
|
54
|
-
quiet?: boolean; // default: false - suppress welcome screen and startup info
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface ImageSettings {
|
|
58
|
-
autoResize?: boolean; // default: true (resize images to 2000x2000 max for better model compatibility)
|
|
59
|
-
blockImages?: boolean; // default: false - when true, prevents all images from being sent to LLM providers
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface ThinkingBudgetsSettings {
|
|
63
|
-
minimal?: number;
|
|
64
|
-
low?: number;
|
|
65
|
-
medium?: number;
|
|
66
|
-
high?: number;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export type NotificationMethod = "bell" | "osc99" | "osc9" | "auto" | "off";
|
|
70
|
-
|
|
71
|
-
export interface NotificationSettings {
|
|
72
|
-
onComplete?: NotificationMethod; // default: "auto"
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export interface AskSettings {
|
|
76
|
-
/** Timeout in seconds for ask tool selections (0 or null to disable, default: 30) */
|
|
77
|
-
timeout?: number | null;
|
|
78
|
-
/** Notification method when ask tool is waiting for input (default: "auto") */
|
|
79
|
-
notification?: NotificationMethod;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export interface ExaSettings {
|
|
83
|
-
enabled?: boolean; // default: true (master toggle for all Exa tools)
|
|
84
|
-
enableSearch?: boolean; // default: true (search, deep, code, crawl)
|
|
85
|
-
enableLinkedin?: boolean; // default: false
|
|
86
|
-
enableCompany?: boolean; // default: false
|
|
87
|
-
enableResearcher?: boolean; // default: false
|
|
88
|
-
enableWebsets?: boolean; // default: false
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export type WebSearchProviderOption = "auto" | "exa" | "perplexity" | "anthropic";
|
|
92
|
-
export type ImageProviderOption = "auto" | "gemini" | "openrouter";
|
|
93
|
-
export type KimiApiFormatOption = "openai" | "anthropic";
|
|
94
|
-
|
|
95
|
-
export interface ProviderSettings {
|
|
96
|
-
webSearch?: WebSearchProviderOption; // default: "auto" (exa > perplexity > anthropic)
|
|
97
|
-
image?: ImageProviderOption; // default: "auto" (openrouter > gemini)
|
|
98
|
-
kimiApiFormat?: KimiApiFormatOption; // default: "anthropic" (use Anthropic-compatible API for Kimi, more stable)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export interface BashInterceptorRule {
|
|
102
|
-
pattern: string;
|
|
103
|
-
flags?: string;
|
|
104
|
-
tool: string;
|
|
105
|
-
message: string;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export interface BashInterceptorSettings {
|
|
109
|
-
enabled?: boolean; // default: false (blocks shell commands that have dedicated tools)
|
|
110
|
-
simpleLs?: boolean; // default: true (intercept bare ls commands)
|
|
111
|
-
patterns?: BashInterceptorRule[]; // default: built-in rules
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export interface MCPSettings {
|
|
115
|
-
enableProjectConfig?: boolean; // default: true (load .mcp.json from project root)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export interface LspSettings {
|
|
119
|
-
formatOnWrite?: boolean; // default: false (format files using LSP after write tool writes code files)
|
|
120
|
-
diagnosticsOnWrite?: boolean; // default: true (return LSP diagnostics after write tool writes code files)
|
|
121
|
-
diagnosticsOnEdit?: boolean; // default: false (return LSP diagnostics after edit tool edits code files)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export type PythonToolMode = "ipy-only" | "bash-only" | "both";
|
|
125
|
-
export type PythonKernelMode = "session" | "per-call";
|
|
126
|
-
|
|
127
|
-
export interface PythonSettings {
|
|
128
|
-
toolMode?: PythonToolMode;
|
|
129
|
-
kernelMode?: PythonKernelMode;
|
|
130
|
-
sharedGateway?: boolean;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export interface CommitSettings {
|
|
134
|
-
mapReduceEnabled?: boolean;
|
|
135
|
-
mapReduceMinFiles?: number;
|
|
136
|
-
mapReduceMaxFileTokens?: number;
|
|
137
|
-
mapReduceTimeoutMs?: number;
|
|
138
|
-
mapReduceMaxConcurrency?: number;
|
|
139
|
-
changelogMaxDiffChars?: number;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export interface EditSettings {
|
|
143
|
-
fuzzyMatch?: boolean; // default: true (accept high-confidence fuzzy matches for whitespace/indentation)
|
|
144
|
-
fuzzyThreshold?: number; // default: 0.95 (similarity threshold for fuzzy matching)
|
|
145
|
-
patchMode?: boolean; // default: true (use codex-style apply-patch format instead of old_text/new_text)
|
|
146
|
-
streamingAbort?: boolean; // default: false (abort streaming edit tool calls when patch preview fails)
|
|
147
|
-
/** Model-specific variant overrides. Keys are model pattern substrings (e.g., "kimi", "deepseek"). */
|
|
148
|
-
modelVariants?: Record<string, "patch" | "replace">;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export type { SymbolPreset };
|
|
152
|
-
|
|
153
|
-
export interface TtsrSettings {
|
|
154
|
-
enabled?: boolean; // default: true
|
|
155
|
-
/** What to do with partial output when TTSR triggers: "keep" shows interrupted attempt, "discard" removes it */
|
|
156
|
-
contextMode?: "keep" | "discard"; // default: "discard"
|
|
157
|
-
/** How TTSR rules repeat: "once" = only trigger once per session, "after-gap" = can repeat after N messages */
|
|
158
|
-
repeatMode?: "once" | "after-gap"; // default: "once"
|
|
159
|
-
/** Number of messages before a rule can trigger again (only used when repeatMode is "after-gap") */
|
|
160
|
-
repeatGap?: number; // default: 10
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export interface TodoCompletionSettings {
|
|
164
|
-
enabled?: boolean; // default: false - warn agent when it stops with incomplete todos
|
|
165
|
-
maxReminders?: number; // default: 3 - maximum reminders before giving up
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export type StatusLineSegmentId =
|
|
169
|
-
| "pi"
|
|
170
|
-
| "model"
|
|
171
|
-
| "plan_mode"
|
|
172
|
-
| "path"
|
|
173
|
-
| "git"
|
|
174
|
-
| "subagents"
|
|
175
|
-
| "token_in"
|
|
176
|
-
| "token_out"
|
|
177
|
-
| "token_total"
|
|
178
|
-
| "cost"
|
|
179
|
-
| "context_pct"
|
|
180
|
-
| "context_total"
|
|
181
|
-
| "time_spent"
|
|
182
|
-
| "time"
|
|
183
|
-
| "session"
|
|
184
|
-
| "hostname"
|
|
185
|
-
| "cache_read"
|
|
186
|
-
| "cache_write";
|
|
187
|
-
|
|
188
|
-
export type StatusLineSeparatorStyle = "powerline" | "powerline-thin" | "slash" | "pipe" | "block" | "none" | "ascii";
|
|
189
|
-
|
|
190
|
-
export type StatusLinePreset = "default" | "minimal" | "compact" | "full" | "nerd" | "ascii" | "custom";
|
|
191
|
-
|
|
192
|
-
export interface StatusLineSegmentOptions {
|
|
193
|
-
model?: { showThinkingLevel?: boolean };
|
|
194
|
-
path?: { abbreviate?: boolean; maxLength?: number; stripWorkPrefix?: boolean };
|
|
195
|
-
git?: { showBranch?: boolean; showStaged?: boolean; showUnstaged?: boolean; showUntracked?: boolean };
|
|
196
|
-
time?: { format?: "12h" | "24h"; showSeconds?: boolean };
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
export interface StatusLineSettings {
|
|
200
|
-
preset?: StatusLinePreset;
|
|
201
|
-
leftSegments?: StatusLineSegmentId[];
|
|
202
|
-
rightSegments?: StatusLineSegmentId[];
|
|
203
|
-
separator?: StatusLineSeparatorStyle;
|
|
204
|
-
segmentOptions?: StatusLineSegmentOptions;
|
|
205
|
-
showHookStatus?: boolean;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
export interface Settings {
|
|
209
|
-
lastChangelogVersion?: string;
|
|
210
|
-
/** Model roles map: { default: "provider/modelId", small: "provider/modelId", ... } */
|
|
211
|
-
modelRoles?: Record<string, string>;
|
|
212
|
-
defaultThinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
213
|
-
steeringMode?: "all" | "one-at-a-time";
|
|
214
|
-
followUpMode?: "all" | "one-at-a-time";
|
|
215
|
-
queueMode?: "all" | "one-at-a-time"; // legacy
|
|
216
|
-
interruptMode?: "immediate" | "wait";
|
|
217
|
-
theme?: string;
|
|
218
|
-
symbolPreset?: SymbolPreset; // default: uses theme's preset or "unicode"
|
|
219
|
-
colorBlindMode?: boolean; // default: false (use blue instead of green for diff additions)
|
|
220
|
-
compaction?: CompactionSettings;
|
|
221
|
-
branchSummary?: BranchSummarySettings;
|
|
222
|
-
retry?: RetrySettings;
|
|
223
|
-
hideThinkingBlock?: boolean;
|
|
224
|
-
shellPath?: string; // Custom shell path (e.g., for Cygwin users on Windows)
|
|
225
|
-
shellForceBasic?: boolean; // Force bash/sh even if user's default shell is different
|
|
226
|
-
collapseChangelog?: boolean; // Show condensed changelog after update (use /changelog for full)
|
|
227
|
-
startup?: StartupSettings;
|
|
228
|
-
doubleEscapeAction?: "branch" | "tree"; // Action for double-escape with empty editor (default: "tree")
|
|
229
|
-
thinkingBudgets?: ThinkingBudgetsSettings; // Custom token budgets for thinking levels
|
|
230
|
-
/** Environment variables to set automatically on startup */
|
|
231
|
-
env?: Record<string, string>;
|
|
232
|
-
extensions?: string[]; // Array of extension file paths
|
|
233
|
-
skills?: SkillsSettings;
|
|
234
|
-
commands?: CommandsSettings;
|
|
235
|
-
terminal?: TerminalSettings;
|
|
236
|
-
images?: ImageSettings;
|
|
237
|
-
notifications?: NotificationSettings;
|
|
238
|
-
enabledModels?: string[]; // Model patterns for cycling (same format as --models CLI flag)
|
|
239
|
-
exa?: ExaSettings;
|
|
240
|
-
bashInterceptor?: BashInterceptorSettings;
|
|
241
|
-
mcp?: MCPSettings;
|
|
242
|
-
lsp?: LspSettings;
|
|
243
|
-
python?: PythonSettings;
|
|
244
|
-
commit?: CommitSettings;
|
|
245
|
-
edit?: EditSettings;
|
|
246
|
-
ttsr?: TtsrSettings;
|
|
247
|
-
todoCompletion?: TodoCompletionSettings;
|
|
248
|
-
providers?: ProviderSettings;
|
|
249
|
-
disabledProviders?: string[]; // Discovery provider IDs that are disabled
|
|
250
|
-
disabledExtensions?: string[]; // Individual extension IDs that are disabled (e.g., "skill:commit")
|
|
251
|
-
statusLine?: StatusLineSettings; // Status line configuration
|
|
252
|
-
showHardwareCursor?: boolean; // Show terminal cursor while still positioning it for IME
|
|
253
|
-
normativeRewrite?: boolean; // default: false (rewrite tool call arguments to normalized format in session history)
|
|
254
|
-
readLineNumbers?: boolean; // default: false (prepend line numbers to read tool output by default)
|
|
255
|
-
ask?: AskSettings;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
export const DEFAULT_BASH_INTERCEPTOR_RULES: BashInterceptorRule[] = [
|
|
259
|
-
{
|
|
260
|
-
pattern: "^\\s*(cat|head|tail|less|more)\\s+",
|
|
261
|
-
tool: "read",
|
|
262
|
-
message: "Use the `read` tool instead of cat/head/tail. It provides better context and handles binary files.",
|
|
263
|
-
},
|
|
264
|
-
{
|
|
265
|
-
pattern: "^\\s*(grep|rg|ripgrep|ag|ack)\\s+",
|
|
266
|
-
tool: "grep",
|
|
267
|
-
message: "Use the `grep` tool instead of grep/rg. It respects .gitignore and provides structured output.",
|
|
268
|
-
},
|
|
269
|
-
{
|
|
270
|
-
pattern: "^\\s*(find|fd|locate)\\s+.*(-name|-iname|-type|--type|-glob)",
|
|
271
|
-
tool: "find",
|
|
272
|
-
message: "Use the `find` tool instead of find/fd. It respects .gitignore and is faster for glob patterns.",
|
|
273
|
-
},
|
|
274
|
-
{
|
|
275
|
-
pattern: "^\\s*sed\\s+(-i|--in-place)",
|
|
276
|
-
tool: "edit",
|
|
277
|
-
message: "Use the `edit` tool instead of sed -i. It provides diff preview and fuzzy matching.",
|
|
278
|
-
},
|
|
279
|
-
{
|
|
280
|
-
pattern: "^\\s*perl\\s+.*-[pn]?i",
|
|
281
|
-
tool: "edit",
|
|
282
|
-
message: "Use the `edit` tool instead of perl -i. It provides diff preview and fuzzy matching.",
|
|
283
|
-
},
|
|
284
|
-
{
|
|
285
|
-
pattern: "^\\s*awk\\s+.*-i\\s+inplace",
|
|
286
|
-
tool: "edit",
|
|
287
|
-
message: "Use the `edit` tool instead of awk -i inplace. It provides diff preview and fuzzy matching.",
|
|
288
|
-
},
|
|
289
|
-
{
|
|
290
|
-
pattern: "^\\s*(echo|printf|cat\\s*<<)\\s+.*[^|]>\\s*\\S",
|
|
291
|
-
tool: "write",
|
|
292
|
-
message: "Use the `write` tool instead of echo/cat redirection. It handles encoding and provides confirmation.",
|
|
293
|
-
},
|
|
294
|
-
];
|
|
295
|
-
|
|
296
|
-
const DEFAULT_BASH_INTERCEPTOR_SETTINGS: Required<BashInterceptorSettings> = {
|
|
297
|
-
enabled: false,
|
|
298
|
-
simpleLs: true,
|
|
299
|
-
patterns: DEFAULT_BASH_INTERCEPTOR_RULES,
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
const DEFAULT_SETTINGS: Settings = {
|
|
303
|
-
compaction: { enabled: true, reserveTokens: 16384, keepRecentTokens: 20000 },
|
|
304
|
-
branchSummary: { enabled: false, reserveTokens: 16384 },
|
|
305
|
-
retry: { enabled: true, maxRetries: 3, baseDelayMs: 2000 },
|
|
306
|
-
skills: {
|
|
307
|
-
enabled: true,
|
|
308
|
-
enableCodexUser: true,
|
|
309
|
-
enableClaudeUser: true,
|
|
310
|
-
enableClaudeProject: true,
|
|
311
|
-
enablePiUser: true,
|
|
312
|
-
enablePiProject: true,
|
|
313
|
-
customDirectories: [],
|
|
314
|
-
ignoredSkills: [],
|
|
315
|
-
includeSkills: [],
|
|
316
|
-
},
|
|
317
|
-
commands: { enableClaudeUser: true, enableClaudeProject: true },
|
|
318
|
-
terminal: { showImages: true },
|
|
319
|
-
images: { autoResize: true },
|
|
320
|
-
notifications: { onComplete: "auto" },
|
|
321
|
-
ask: { timeout: 30, notification: "auto" },
|
|
322
|
-
exa: {
|
|
323
|
-
enabled: true,
|
|
324
|
-
enableSearch: true,
|
|
325
|
-
enableLinkedin: false,
|
|
326
|
-
enableCompany: false,
|
|
327
|
-
enableResearcher: false,
|
|
328
|
-
enableWebsets: false,
|
|
329
|
-
},
|
|
330
|
-
bashInterceptor: DEFAULT_BASH_INTERCEPTOR_SETTINGS,
|
|
331
|
-
mcp: { enableProjectConfig: true },
|
|
332
|
-
lsp: { formatOnWrite: false, diagnosticsOnWrite: true, diagnosticsOnEdit: false },
|
|
333
|
-
python: { toolMode: "both", kernelMode: "session", sharedGateway: true },
|
|
334
|
-
edit: { fuzzyMatch: true, fuzzyThreshold: 0.95, streamingAbort: false },
|
|
335
|
-
ttsr: { enabled: true, contextMode: "discard", repeatMode: "once", repeatGap: 10 },
|
|
336
|
-
providers: { webSearch: "auto", image: "auto" },
|
|
337
|
-
} satisfies Settings;
|
|
338
|
-
|
|
339
|
-
function normalizeBashInterceptorRule(rule: unknown): BashInterceptorRule | null {
|
|
340
|
-
if (!rule || typeof rule !== "object" || Array.isArray(rule)) return null;
|
|
341
|
-
|
|
342
|
-
const candidate = rule as Record<string, unknown>;
|
|
343
|
-
const pattern = typeof candidate.pattern === "string" ? candidate.pattern : "";
|
|
344
|
-
const tool = typeof candidate.tool === "string" ? candidate.tool : "";
|
|
345
|
-
const message = typeof candidate.message === "string" ? candidate.message : "";
|
|
346
|
-
const flags = typeof candidate.flags === "string" && candidate.flags.length > 0 ? candidate.flags : undefined;
|
|
347
|
-
|
|
348
|
-
if (!pattern || !tool || !message) return null;
|
|
349
|
-
return { pattern, flags, tool, message };
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function normalizeBashInterceptorSettings(
|
|
353
|
-
settings: BashInterceptorSettings | undefined,
|
|
354
|
-
): Required<BashInterceptorSettings> {
|
|
355
|
-
const enabled = settings?.enabled ?? DEFAULT_BASH_INTERCEPTOR_SETTINGS.enabled;
|
|
356
|
-
const simpleLs = settings?.simpleLs ?? DEFAULT_BASH_INTERCEPTOR_SETTINGS.simpleLs;
|
|
357
|
-
const rawPatterns = settings?.patterns;
|
|
358
|
-
let patterns: BashInterceptorRule[];
|
|
359
|
-
if (rawPatterns === undefined) {
|
|
360
|
-
patterns = DEFAULT_BASH_INTERCEPTOR_RULES;
|
|
361
|
-
} else if (Array.isArray(rawPatterns)) {
|
|
362
|
-
patterns = rawPatterns
|
|
363
|
-
.map(rule => normalizeBashInterceptorRule(rule))
|
|
364
|
-
.filter((rule): rule is BashInterceptorRule => rule !== null);
|
|
365
|
-
} else {
|
|
366
|
-
patterns = DEFAULT_BASH_INTERCEPTOR_RULES;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
return { enabled, simpleLs, patterns };
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
let cachedNerdFonts: boolean | null = null;
|
|
373
|
-
|
|
374
|
-
function hasNerdFonts(): boolean {
|
|
375
|
-
if (cachedNerdFonts !== null) {
|
|
376
|
-
return cachedNerdFonts;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
const envOverride = process.env.NERD_FONTS;
|
|
380
|
-
if (envOverride === "1") {
|
|
381
|
-
cachedNerdFonts = true;
|
|
382
|
-
return true;
|
|
383
|
-
}
|
|
384
|
-
if (envOverride === "0") {
|
|
385
|
-
cachedNerdFonts = false;
|
|
386
|
-
return false;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
const termProgram = (process.env.TERM_PROGRAM || "").toLowerCase();
|
|
390
|
-
const term = (process.env.TERM || "").toLowerCase();
|
|
391
|
-
const nerdTerms = ["iterm", "wezterm", "kitty", "ghostty", "alacritty"];
|
|
392
|
-
cachedNerdFonts = nerdTerms.some(candidate => termProgram.includes(candidate) || term.includes(candidate));
|
|
393
|
-
return cachedNerdFonts;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function normalizeSettings(settings: Settings): Settings {
|
|
397
|
-
const merged = deepMergeSettings(DEFAULT_SETTINGS, settings);
|
|
398
|
-
const symbolPreset = merged.symbolPreset ?? (hasNerdFonts() ? "nerd" : "unicode");
|
|
399
|
-
return {
|
|
400
|
-
...merged,
|
|
401
|
-
symbolPreset,
|
|
402
|
-
bashInterceptor: normalizeBashInterceptorSettings(merged.bashInterceptor),
|
|
403
|
-
python: normalizePythonSettings(merged.python),
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
function normalizePythonSettings(settings: PythonSettings | undefined): PythonSettings {
|
|
408
|
-
const toolMode = settings?.toolMode;
|
|
409
|
-
const kernelMode = settings?.kernelMode;
|
|
410
|
-
const sharedGateway = settings?.sharedGateway;
|
|
411
|
-
return {
|
|
412
|
-
toolMode:
|
|
413
|
-
toolMode === "ipy-only" || toolMode === "bash-only" || toolMode === "both"
|
|
414
|
-
? toolMode
|
|
415
|
-
: (DEFAULT_SETTINGS.python?.toolMode ?? "both"),
|
|
416
|
-
kernelMode:
|
|
417
|
-
kernelMode === "session" || kernelMode === "per-call"
|
|
418
|
-
? kernelMode
|
|
419
|
-
: (DEFAULT_SETTINGS.python?.kernelMode ?? "session"),
|
|
420
|
-
sharedGateway:
|
|
421
|
-
typeof sharedGateway === "boolean" ? sharedGateway : (DEFAULT_SETTINGS.python?.sharedGateway ?? true),
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
/** Deep merge settings: project/overrides take precedence, nested objects merge recursively */
|
|
426
|
-
function deepMergeSettings(base: Settings, overrides: Settings): Settings {
|
|
427
|
-
const result: Settings = { ...base };
|
|
428
|
-
|
|
429
|
-
for (const key of Object.keys(overrides) as (keyof Settings)[]) {
|
|
430
|
-
const overrideValue = overrides[key];
|
|
431
|
-
const baseValue = base[key];
|
|
432
|
-
|
|
433
|
-
if (overrideValue === undefined) {
|
|
434
|
-
continue;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// For nested objects, merge recursively
|
|
438
|
-
if (
|
|
439
|
-
typeof overrideValue === "object" &&
|
|
440
|
-
overrideValue !== null &&
|
|
441
|
-
!Array.isArray(overrideValue) &&
|
|
442
|
-
typeof baseValue === "object" &&
|
|
443
|
-
baseValue !== null &&
|
|
444
|
-
!Array.isArray(baseValue)
|
|
445
|
-
) {
|
|
446
|
-
(result as Record<string, unknown>)[key] = { ...baseValue, ...overrideValue };
|
|
447
|
-
} else {
|
|
448
|
-
// For primitives and arrays, override value wins
|
|
449
|
-
(result as Record<string, unknown>)[key] = overrideValue;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
return result;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
export class SettingsManager {
|
|
457
|
-
/** SQLite storage for auth/cache (null for in-memory mode) */
|
|
458
|
-
private storage: AgentStorage | null;
|
|
459
|
-
/** Path to config.yml (null for in-memory mode) */
|
|
460
|
-
private configPath: string | null;
|
|
461
|
-
private cwd: string | null;
|
|
462
|
-
private globalSettings: Settings;
|
|
463
|
-
private overrides: Settings;
|
|
464
|
-
private settings!: Settings;
|
|
465
|
-
private persist: boolean;
|
|
466
|
-
private modifiedFields = new Set<keyof Settings>();
|
|
467
|
-
private modifiedNestedFields = new Map<keyof Settings, Set<string>>();
|
|
468
|
-
|
|
469
|
-
static #lastInstance: SettingsManager | null = null;
|
|
470
|
-
|
|
471
|
-
/**
|
|
472
|
-
* Private constructor - use static factory methods instead.
|
|
473
|
-
* @param storage - SQLite storage instance for auth/cache, or null for in-memory mode
|
|
474
|
-
* @param configPath - Path to config.yml for persistence, or null for in-memory mode
|
|
475
|
-
* @param cwd - Current working directory for project settings discovery
|
|
476
|
-
* @param initialSettings - Initial global settings to use
|
|
477
|
-
* @param persist - Whether to persist settings changes to storage
|
|
478
|
-
* @param projectSettings - Pre-loaded project settings (to avoid async in constructor)
|
|
479
|
-
*/
|
|
480
|
-
private constructor(
|
|
481
|
-
storage: AgentStorage | null,
|
|
482
|
-
configPath: string | null,
|
|
483
|
-
cwd: string | null,
|
|
484
|
-
initialSettings: Settings,
|
|
485
|
-
persist: boolean,
|
|
486
|
-
projectSettings: Settings,
|
|
487
|
-
private agentDir: string | null,
|
|
488
|
-
) {
|
|
489
|
-
this.storage = storage;
|
|
490
|
-
this.configPath = configPath;
|
|
491
|
-
this.cwd = cwd;
|
|
492
|
-
this.persist = persist;
|
|
493
|
-
this.globalSettings = initialSettings;
|
|
494
|
-
this.overrides = {};
|
|
495
|
-
this.rebuildSettings(projectSettings);
|
|
496
|
-
|
|
497
|
-
// Apply environment variables from settings
|
|
498
|
-
this.applyEnvironmentVariables();
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
/**
|
|
502
|
-
* Apply environment variables from settings to process.env
|
|
503
|
-
* Only sets variables that are not already set in the environment
|
|
504
|
-
*/
|
|
505
|
-
applyEnvironmentVariables(): void {
|
|
506
|
-
const envVars = this.settings.env;
|
|
507
|
-
if (!envVars || typeof envVars !== "object") {
|
|
508
|
-
return;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
for (const [key, value] of Object.entries(envVars)) {
|
|
512
|
-
if (typeof key === "string" && typeof value === "string") {
|
|
513
|
-
// Only set if not already present in environment (allow override with env vars)
|
|
514
|
-
if (!(key in process.env)) {
|
|
515
|
-
process.env[key] = value;
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
/**
|
|
522
|
-
* Create a SettingsManager that loads from persistent config.yml.
|
|
523
|
-
* @param cwd - Current working directory for project settings discovery
|
|
524
|
-
* @param agentDir - Agent directory containing config.yml
|
|
525
|
-
* @returns Configured SettingsManager with merged global and user settings
|
|
526
|
-
*/
|
|
527
|
-
static async create(cwd: string = process.cwd(), agentDir: string = getAgentDir()): Promise<SettingsManager> {
|
|
528
|
-
cwd = path.normalize(cwd);
|
|
529
|
-
agentDir = path.normalize(agentDir);
|
|
530
|
-
|
|
531
|
-
const configPath = path.join(agentDir, "config.yml");
|
|
532
|
-
const storage = await AgentStorage.open(getAgentDbPath(agentDir));
|
|
533
|
-
|
|
534
|
-
// Migrate from legacy storage if config.yml doesn't exist
|
|
535
|
-
await SettingsManager.migrateToYaml(storage, agentDir, configPath);
|
|
536
|
-
|
|
537
|
-
// Use capability API to load user-level settings from all providers
|
|
538
|
-
const result = await loadCapability(settingsCapability.id, { cwd });
|
|
539
|
-
|
|
540
|
-
// Merge all user-level settings
|
|
541
|
-
let globalSettings: Settings = {};
|
|
542
|
-
for (const item of result.items as SettingsItem[]) {
|
|
543
|
-
if (item.level === "user") {
|
|
544
|
-
globalSettings = deepMergeSettings(globalSettings, item.data as Settings);
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// Load persisted settings from config.yml
|
|
549
|
-
const storedSettings = await SettingsManager.loadFromYaml(configPath);
|
|
550
|
-
globalSettings = deepMergeSettings(globalSettings, storedSettings);
|
|
551
|
-
|
|
552
|
-
// Load project settings before construction (constructor is sync)
|
|
553
|
-
const projectSettings = await SettingsManager.loadProjectSettingsStatic(cwd);
|
|
554
|
-
|
|
555
|
-
const instance = new SettingsManager(storage, configPath, cwd, globalSettings, true, projectSettings, agentDir);
|
|
556
|
-
SettingsManager.#lastInstance = instance;
|
|
557
|
-
return instance;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
/**
|
|
561
|
-
* Create an in-memory SettingsManager without persistence.
|
|
562
|
-
* @param settings - Initial settings to use
|
|
563
|
-
* @returns SettingsManager that won't persist changes to disk
|
|
564
|
-
*/
|
|
565
|
-
static inMemory(settings: Partial<Settings> = {}): SettingsManager {
|
|
566
|
-
return new SettingsManager(null, null, null, settings, false, {}, null);
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
/**
|
|
570
|
-
* Serialize settings for passing to subagent workers.
|
|
571
|
-
* Returns the merged settings (global + project + overrides).
|
|
572
|
-
*/
|
|
573
|
-
serialize(): Settings {
|
|
574
|
-
return { ...this.settings };
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
getPlansDirectory(_cwd: string = this.cwd ?? process.cwd()): string {
|
|
578
|
-
return path.join(getAgentDir(), "plans");
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
/**
|
|
582
|
-
* Access the underlying agent storage (null for in-memory settings).
|
|
583
|
-
*/
|
|
584
|
-
getStorage(): AgentStorage | null {
|
|
585
|
-
return this.storage;
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
/**
|
|
589
|
-
* Load settings from config.yml, applying any schema migrations.
|
|
590
|
-
* @param configPath - Path to config.yml, or null for in-memory mode
|
|
591
|
-
* @returns Parsed and migrated settings, or empty object if file doesn't exist
|
|
592
|
-
*/
|
|
593
|
-
private static async loadFromYaml(configPath: string | null): Promise<Settings> {
|
|
594
|
-
if (!configPath) {
|
|
595
|
-
return {};
|
|
596
|
-
}
|
|
597
|
-
try {
|
|
598
|
-
const content = await Bun.file(configPath).text();
|
|
599
|
-
const parsed = YAML.parse(content);
|
|
600
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
601
|
-
return {};
|
|
602
|
-
}
|
|
603
|
-
return SettingsManager.migrateSettings(parsed as Record<string, unknown>);
|
|
604
|
-
} catch (error) {
|
|
605
|
-
if (isEnoent(error)) return {};
|
|
606
|
-
logger.warn("SettingsManager failed to load config.yml", { path: configPath, error: String(error) });
|
|
607
|
-
return {};
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
/**
|
|
612
|
-
* Migrate settings from legacy sources to config.yml.
|
|
613
|
-
* Migration order: settings.json -> agent.db -> config.yml
|
|
614
|
-
* Only migrates if config.yml doesn't exist.
|
|
615
|
-
*/
|
|
616
|
-
private static async migrateToYaml(storage: AgentStorage, agentDir: string, configPath: string): Promise<void> {
|
|
617
|
-
try {
|
|
618
|
-
await Bun.file(configPath).text();
|
|
619
|
-
return;
|
|
620
|
-
} catch (err) {
|
|
621
|
-
if (!isEnoent(err)) {
|
|
622
|
-
logger.warn("SettingsManager failed to check config.yml", { path: configPath, error: String(err) });
|
|
623
|
-
return;
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
let settings: Settings = {};
|
|
628
|
-
let migrated = false;
|
|
629
|
-
|
|
630
|
-
// 1. Try to migrate from settings.json (oldest legacy format)
|
|
631
|
-
const settingsJsonPath = path.join(agentDir, "settings.json");
|
|
632
|
-
try {
|
|
633
|
-
const parsed = JSON.parse(await Bun.file(settingsJsonPath).text());
|
|
634
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
635
|
-
settings = deepMergeSettings(settings, SettingsManager.migrateSettings(parsed));
|
|
636
|
-
migrated = true;
|
|
637
|
-
// Backup settings.json
|
|
638
|
-
try {
|
|
639
|
-
fs.renameSync(settingsJsonPath, `${settingsJsonPath}.bak`);
|
|
640
|
-
} catch (error) {
|
|
641
|
-
logger.warn("SettingsManager failed to backup settings.json", { error: String(error) });
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
} catch (error) {
|
|
645
|
-
if (!isEnoent(error)) {
|
|
646
|
-
logger.warn("SettingsManager failed to read settings.json", { error: String(error) });
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// 2. Migrate from agent.db settings table
|
|
651
|
-
try {
|
|
652
|
-
const dbSettings = storage.getSettings();
|
|
653
|
-
if (dbSettings) {
|
|
654
|
-
settings = deepMergeSettings(
|
|
655
|
-
settings,
|
|
656
|
-
SettingsManager.migrateSettings(dbSettings as Record<string, unknown>),
|
|
657
|
-
);
|
|
658
|
-
migrated = true;
|
|
659
|
-
}
|
|
660
|
-
} catch (error) {
|
|
661
|
-
logger.warn("SettingsManager failed to read agent.db settings", { error: String(error) });
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// 3. Write merged settings to config.yml if we found any
|
|
665
|
-
if (migrated && Object.keys(settings).length > 0) {
|
|
666
|
-
try {
|
|
667
|
-
await Bun.write(configPath, YAML.stringify(settings, null, 2));
|
|
668
|
-
logger.debug("SettingsManager migrated settings to config.yml", { path: configPath });
|
|
669
|
-
} catch (error) {
|
|
670
|
-
logger.warn("SettingsManager failed to write config.yml", { path: configPath, error: String(error) });
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
/** Migrate old settings format to new format */
|
|
676
|
-
private static migrateSettings(settings: Record<string, unknown>): Settings {
|
|
677
|
-
// Migrate queueMode -> steeringMode
|
|
678
|
-
if ("queueMode" in settings && !("steeringMode" in settings)) {
|
|
679
|
-
settings.steeringMode = settings.queueMode;
|
|
680
|
-
delete settings.queueMode;
|
|
681
|
-
}
|
|
682
|
-
return settings as Settings;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
/**
|
|
686
|
-
* Static helper to load project settings (used by create() before construction).
|
|
687
|
-
*/
|
|
688
|
-
private static async loadProjectSettingsStatic(cwd: string | null): Promise<Settings> {
|
|
689
|
-
if (!cwd) return {};
|
|
690
|
-
|
|
691
|
-
// Use capability API to discover settings from all providers
|
|
692
|
-
const result = await loadCapability(settingsCapability.id, { cwd });
|
|
693
|
-
|
|
694
|
-
// Merge only project-level settings (user-level settings are handled separately via globalSettings)
|
|
695
|
-
let merged: Settings = {};
|
|
696
|
-
for (const item of result.items as SettingsItem[]) {
|
|
697
|
-
if (item.level === "project") {
|
|
698
|
-
merged = deepMergeSettings(merged, item.data as Settings);
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
return SettingsManager.migrateSettings(merged as Record<string, unknown>);
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
private async loadProjectSettings(): Promise<Settings> {
|
|
706
|
-
return SettingsManager.loadProjectSettingsStatic(this.cwd);
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
private rebuildSettings(projectSettings: Settings): void {
|
|
710
|
-
this.settings = normalizeSettings(
|
|
711
|
-
deepMergeSettings(deepMergeSettings(this.globalSettings, projectSettings), this.overrides),
|
|
712
|
-
);
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
/** Apply additional overrides on top of current settings */
|
|
716
|
-
async applyOverrides(overrides: Partial<Settings>): Promise<void> {
|
|
717
|
-
this.overrides = deepMergeSettings(this.overrides, overrides);
|
|
718
|
-
const projectSettings = await this.loadProjectSettings();
|
|
719
|
-
this.rebuildSettings(projectSettings);
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
/** Mark a field as modified during this session */
|
|
723
|
-
private markModified(field: keyof Settings, nestedKey?: string): void {
|
|
724
|
-
this.modifiedFields.add(field);
|
|
725
|
-
if (nestedKey) {
|
|
726
|
-
if (!this.modifiedNestedFields.has(field)) {
|
|
727
|
-
this.modifiedNestedFields.set(field, new Set());
|
|
728
|
-
}
|
|
729
|
-
this.modifiedNestedFields.get(field)!.add(nestedKey);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
/**
|
|
734
|
-
* Persist current global settings to config.yml and rebuild merged settings.
|
|
735
|
-
* Uses file locking to prevent concurrent write races.
|
|
736
|
-
*/
|
|
737
|
-
private async save(): Promise<void> {
|
|
738
|
-
if (this.persist && this.configPath) {
|
|
739
|
-
const configPath = this.configPath;
|
|
740
|
-
try {
|
|
741
|
-
await withFileLock(configPath, async () => {
|
|
742
|
-
// Re-read current file to get latest external changes
|
|
743
|
-
const currentFileSettings = await SettingsManager.loadFromYaml(configPath);
|
|
744
|
-
|
|
745
|
-
// Start with file settings as base - preserves external edits
|
|
746
|
-
const mergedSettings: Settings = { ...currentFileSettings };
|
|
747
|
-
|
|
748
|
-
// Only override with in-memory values for fields that were explicitly modified during this session
|
|
749
|
-
for (const field of this.modifiedFields) {
|
|
750
|
-
const value = this.globalSettings[field];
|
|
751
|
-
|
|
752
|
-
// Handle nested objects specially - merge at nested level to preserve unmodified nested keys
|
|
753
|
-
if (this.modifiedNestedFields.has(field) && typeof value === "object" && value !== null) {
|
|
754
|
-
const nestedModified = this.modifiedNestedFields.get(field)!;
|
|
755
|
-
const baseNested = (currentFileSettings[field] as Record<string, unknown>) ?? {};
|
|
756
|
-
const inMemoryNested = value as Record<string, unknown>;
|
|
757
|
-
const mergedNested = { ...baseNested };
|
|
758
|
-
for (const nestedKey of nestedModified) {
|
|
759
|
-
mergedNested[nestedKey] = inMemoryNested[nestedKey];
|
|
760
|
-
}
|
|
761
|
-
(mergedSettings as Record<string, unknown>)[field] = mergedNested;
|
|
762
|
-
} else {
|
|
763
|
-
// For top-level primitives and arrays, use the modified value directly
|
|
764
|
-
(mergedSettings as Record<string, unknown>)[field] = value;
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
this.globalSettings = mergedSettings;
|
|
769
|
-
await Bun.write(configPath, YAML.stringify(this.globalSettings, null, 2));
|
|
770
|
-
});
|
|
771
|
-
} catch (error) {
|
|
772
|
-
logger.warn("SettingsManager save failed", { error: String(error) });
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
const projectSettings = await this.loadProjectSettings();
|
|
777
|
-
this.rebuildSettings(projectSettings);
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
getLastChangelogVersion(): string | undefined {
|
|
781
|
-
return this.settings.lastChangelogVersion;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
async setLastChangelogVersion(version: string): Promise<void> {
|
|
785
|
-
this.globalSettings.lastChangelogVersion = version;
|
|
786
|
-
this.markModified("lastChangelogVersion");
|
|
787
|
-
await this.save();
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
/**
|
|
791
|
-
* Get model for a role. Returns "provider/modelId" string or undefined.
|
|
792
|
-
*/
|
|
793
|
-
getModelRole(role: string): string | undefined {
|
|
794
|
-
return this.settings.modelRoles?.[role];
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
/**
|
|
798
|
-
* Set model for a role. Model should be "provider/modelId" format.
|
|
799
|
-
*/
|
|
800
|
-
async setModelRole(role: string, model: string): Promise<void> {
|
|
801
|
-
if (!this.globalSettings.modelRoles) {
|
|
802
|
-
this.globalSettings.modelRoles = {};
|
|
803
|
-
}
|
|
804
|
-
this.globalSettings.modelRoles[role] = model;
|
|
805
|
-
this.markModified("modelRoles", role);
|
|
806
|
-
|
|
807
|
-
if (this.overrides.modelRoles && this.overrides.modelRoles[role] !== undefined) {
|
|
808
|
-
this.overrides.modelRoles[role] = model;
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
await this.save();
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
/**
|
|
815
|
-
* Get all model roles.
|
|
816
|
-
*/
|
|
817
|
-
getModelRoles(): Record<string, string> {
|
|
818
|
-
return { ...this.settings.modelRoles };
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
getSteeringMode(): "all" | "one-at-a-time" {
|
|
822
|
-
return this.settings.steeringMode || "one-at-a-time";
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
async setSteeringMode(mode: "all" | "one-at-a-time"): Promise<void> {
|
|
826
|
-
this.globalSettings.steeringMode = mode;
|
|
827
|
-
this.markModified("steeringMode");
|
|
828
|
-
await this.save();
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
getFollowUpMode(): "all" | "one-at-a-time" {
|
|
832
|
-
return this.settings.followUpMode || "one-at-a-time";
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
async setFollowUpMode(mode: "all" | "one-at-a-time"): Promise<void> {
|
|
836
|
-
this.globalSettings.followUpMode = mode;
|
|
837
|
-
this.markModified("followUpMode");
|
|
838
|
-
await this.save();
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
getInterruptMode(): "immediate" | "wait" {
|
|
842
|
-
return this.settings.interruptMode || "immediate";
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
async setInterruptMode(mode: "immediate" | "wait"): Promise<void> {
|
|
846
|
-
this.globalSettings.interruptMode = mode;
|
|
847
|
-
this.markModified("interruptMode");
|
|
848
|
-
await this.save();
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
getTheme(): string | undefined {
|
|
852
|
-
return this.settings.theme;
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
async setTheme(theme: string): Promise<void> {
|
|
856
|
-
this.globalSettings.theme = theme;
|
|
857
|
-
this.markModified("theme");
|
|
858
|
-
await this.save();
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
getSymbolPreset(): SymbolPreset | undefined {
|
|
862
|
-
return this.settings.symbolPreset;
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
async setSymbolPreset(preset: SymbolPreset): Promise<void> {
|
|
866
|
-
this.globalSettings.symbolPreset = preset;
|
|
867
|
-
this.markModified("symbolPreset");
|
|
868
|
-
await this.save();
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
getColorBlindMode(): boolean {
|
|
872
|
-
return this.settings.colorBlindMode ?? false;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
async setColorBlindMode(enabled: boolean): Promise<void> {
|
|
876
|
-
this.globalSettings.colorBlindMode = enabled;
|
|
877
|
-
this.markModified("colorBlindMode");
|
|
878
|
-
await this.save();
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
getDefaultThinkingLevel(): "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | undefined {
|
|
882
|
-
return this.settings.defaultThinkingLevel;
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
async setDefaultThinkingLevel(level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"): Promise<void> {
|
|
886
|
-
this.globalSettings.defaultThinkingLevel = level;
|
|
887
|
-
this.markModified("defaultThinkingLevel");
|
|
888
|
-
await this.save();
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
getCompactionEnabled(): boolean {
|
|
892
|
-
return this.settings.compaction?.enabled ?? true;
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
async setCompactionEnabled(enabled: boolean): Promise<void> {
|
|
896
|
-
if (!this.globalSettings.compaction) {
|
|
897
|
-
this.globalSettings.compaction = {};
|
|
898
|
-
}
|
|
899
|
-
this.globalSettings.compaction.enabled = enabled;
|
|
900
|
-
this.markModified("compaction", "enabled");
|
|
901
|
-
await this.save();
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
getCompactionReserveTokens(): number {
|
|
905
|
-
return this.settings.compaction?.reserveTokens ?? 16384;
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
getCompactionKeepRecentTokens(): number {
|
|
909
|
-
return this.settings.compaction?.keepRecentTokens ?? 20000;
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
getCompactionAutoContinue(): boolean {
|
|
913
|
-
return this.settings.compaction?.autoContinue ?? true;
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
getCompactionRemoteEndpoint(): string | undefined {
|
|
917
|
-
return this.settings.compaction?.remoteEndpoint;
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
getCompactionSettings(): {
|
|
921
|
-
enabled: boolean;
|
|
922
|
-
reserveTokens: number;
|
|
923
|
-
keepRecentTokens: number;
|
|
924
|
-
autoContinue: boolean;
|
|
925
|
-
remoteEndpoint?: string;
|
|
926
|
-
} {
|
|
927
|
-
return {
|
|
928
|
-
enabled: this.getCompactionEnabled(),
|
|
929
|
-
reserveTokens: this.getCompactionReserveTokens(),
|
|
930
|
-
keepRecentTokens: this.getCompactionKeepRecentTokens(),
|
|
931
|
-
autoContinue: this.getCompactionAutoContinue(),
|
|
932
|
-
remoteEndpoint: this.getCompactionRemoteEndpoint(),
|
|
933
|
-
};
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
getBranchSummaryEnabled(): boolean {
|
|
937
|
-
return this.settings.branchSummary?.enabled ?? false;
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
async setBranchSummaryEnabled(enabled: boolean): Promise<void> {
|
|
941
|
-
if (!this.globalSettings.branchSummary) {
|
|
942
|
-
this.globalSettings.branchSummary = {};
|
|
943
|
-
}
|
|
944
|
-
this.globalSettings.branchSummary.enabled = enabled;
|
|
945
|
-
this.markModified("branchSummary", "enabled");
|
|
946
|
-
await this.save();
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
getBranchSummarySettings(): { enabled: boolean; reserveTokens: number } {
|
|
950
|
-
return {
|
|
951
|
-
enabled: this.getBranchSummaryEnabled(),
|
|
952
|
-
reserveTokens: this.settings.branchSummary?.reserveTokens ?? 16384,
|
|
953
|
-
};
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
getRetryEnabled(): boolean {
|
|
957
|
-
return this.settings.retry?.enabled ?? true;
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
async setRetryEnabled(enabled: boolean): Promise<void> {
|
|
961
|
-
if (!this.globalSettings.retry) {
|
|
962
|
-
this.globalSettings.retry = {};
|
|
963
|
-
}
|
|
964
|
-
this.globalSettings.retry.enabled = enabled;
|
|
965
|
-
this.markModified("retry", "enabled");
|
|
966
|
-
await this.save();
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
getRetrySettings(): { enabled: boolean; maxRetries: number; baseDelayMs: number } {
|
|
970
|
-
return {
|
|
971
|
-
enabled: this.getRetryEnabled(),
|
|
972
|
-
maxRetries: this.settings.retry?.maxRetries ?? 3,
|
|
973
|
-
baseDelayMs: this.settings.retry?.baseDelayMs ?? 2000,
|
|
974
|
-
};
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
getCommitSettings(): Required<CommitSettings> {
|
|
978
|
-
return {
|
|
979
|
-
mapReduceEnabled: this.settings.commit?.mapReduceEnabled ?? true,
|
|
980
|
-
mapReduceMinFiles: this.settings.commit?.mapReduceMinFiles ?? 4,
|
|
981
|
-
mapReduceMaxFileTokens: this.settings.commit?.mapReduceMaxFileTokens ?? 50_000,
|
|
982
|
-
mapReduceTimeoutMs: this.settings.commit?.mapReduceTimeoutMs ?? 120_000,
|
|
983
|
-
mapReduceMaxConcurrency: this.settings.commit?.mapReduceMaxConcurrency ?? 5,
|
|
984
|
-
changelogMaxDiffChars: this.settings.commit?.changelogMaxDiffChars ?? 120_000,
|
|
985
|
-
};
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
getRetryMaxRetries(): number {
|
|
989
|
-
return this.settings.retry?.maxRetries ?? 3;
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
async setRetryMaxRetries(maxRetries: number): Promise<void> {
|
|
993
|
-
if (!this.globalSettings.retry) {
|
|
994
|
-
this.globalSettings.retry = {};
|
|
995
|
-
}
|
|
996
|
-
this.globalSettings.retry.maxRetries = maxRetries;
|
|
997
|
-
this.markModified("retry", "maxRetries");
|
|
998
|
-
await this.save();
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
getRetryBaseDelayMs(): number {
|
|
1002
|
-
return this.settings.retry?.baseDelayMs ?? 2000;
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
async setRetryBaseDelayMs(baseDelayMs: number): Promise<void> {
|
|
1006
|
-
if (!this.globalSettings.retry) {
|
|
1007
|
-
this.globalSettings.retry = {};
|
|
1008
|
-
}
|
|
1009
|
-
this.globalSettings.retry.baseDelayMs = baseDelayMs;
|
|
1010
|
-
this.markModified("retry", "baseDelayMs");
|
|
1011
|
-
await this.save();
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
getTodoCompletionSettings(): { enabled: boolean; maxReminders: number } {
|
|
1015
|
-
return {
|
|
1016
|
-
enabled: this.settings.todoCompletion?.enabled ?? false,
|
|
1017
|
-
maxReminders: this.settings.todoCompletion?.maxReminders ?? 3,
|
|
1018
|
-
};
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
getTodoCompletionEnabled(): boolean {
|
|
1022
|
-
return this.settings.todoCompletion?.enabled ?? false;
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
async setTodoCompletionEnabled(enabled: boolean): Promise<void> {
|
|
1026
|
-
if (!this.globalSettings.todoCompletion) {
|
|
1027
|
-
this.globalSettings.todoCompletion = {};
|
|
1028
|
-
}
|
|
1029
|
-
this.globalSettings.todoCompletion.enabled = enabled;
|
|
1030
|
-
this.markModified("todoCompletion", "enabled");
|
|
1031
|
-
await this.save();
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
getTodoCompletionMaxReminders(): number {
|
|
1035
|
-
return this.settings.todoCompletion?.maxReminders ?? 3;
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
async setTodoCompletionMaxReminders(maxReminders: number): Promise<void> {
|
|
1039
|
-
if (!this.globalSettings.todoCompletion) {
|
|
1040
|
-
this.globalSettings.todoCompletion = {};
|
|
1041
|
-
}
|
|
1042
|
-
this.globalSettings.todoCompletion.maxReminders = maxReminders;
|
|
1043
|
-
this.markModified("todoCompletion", "maxReminders");
|
|
1044
|
-
await this.save();
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
getThinkingBudgets(): ThinkingBudgetsSettings | undefined {
|
|
1048
|
-
return this.settings.thinkingBudgets;
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
getHideThinkingBlock(): boolean {
|
|
1052
|
-
return this.settings.hideThinkingBlock ?? false;
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
async setHideThinkingBlock(hide: boolean): Promise<void> {
|
|
1056
|
-
this.globalSettings.hideThinkingBlock = hide;
|
|
1057
|
-
this.markModified("hideThinkingBlock");
|
|
1058
|
-
await this.save();
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
getShellPath(): string | undefined {
|
|
1062
|
-
return this.settings.shellPath;
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
getShellForceBasic(): boolean {
|
|
1066
|
-
return this.settings.shellForceBasic ?? true;
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
async setShellPath(path: string | undefined): Promise<void> {
|
|
1070
|
-
this.globalSettings.shellPath = path;
|
|
1071
|
-
this.markModified("shellPath");
|
|
1072
|
-
await this.save();
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
async setShellForceBasic(force: boolean): Promise<void> {
|
|
1076
|
-
this.globalSettings.shellForceBasic = force;
|
|
1077
|
-
this.markModified("shellForceBasic");
|
|
1078
|
-
await this.save();
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
getCollapseChangelog(): boolean {
|
|
1082
|
-
return this.settings.collapseChangelog ?? false;
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
async setCollapseChangelog(collapse: boolean): Promise<void> {
|
|
1086
|
-
this.globalSettings.collapseChangelog = collapse;
|
|
1087
|
-
this.markModified("collapseChangelog");
|
|
1088
|
-
await this.save();
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
getStartupQuiet(): boolean {
|
|
1092
|
-
return this.settings.startup?.quiet ?? false;
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
async setStartupQuiet(quiet: boolean): Promise<void> {
|
|
1096
|
-
if (!this.globalSettings.startup) {
|
|
1097
|
-
this.globalSettings.startup = {};
|
|
1098
|
-
}
|
|
1099
|
-
this.globalSettings.startup.quiet = quiet;
|
|
1100
|
-
this.markModified("startup", "quiet");
|
|
1101
|
-
await this.save();
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
getExtensionPaths(): string[] {
|
|
1105
|
-
return [...(this.settings.extensions ?? [])];
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
async setExtensionPaths(paths: string[]): Promise<void> {
|
|
1109
|
-
this.globalSettings.extensions = paths;
|
|
1110
|
-
this.markModified("extensions");
|
|
1111
|
-
await this.save();
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
getSkillsEnabled(): boolean {
|
|
1115
|
-
return this.settings.skills?.enabled ?? true;
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
async setSkillsEnabled(enabled: boolean): Promise<void> {
|
|
1119
|
-
if (!this.globalSettings.skills) {
|
|
1120
|
-
this.globalSettings.skills = {};
|
|
1121
|
-
}
|
|
1122
|
-
this.globalSettings.skills.enabled = enabled;
|
|
1123
|
-
this.markModified("skills", "enabled");
|
|
1124
|
-
await this.save();
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
getSkillsSettings(): Required<SkillsSettings> {
|
|
1128
|
-
return {
|
|
1129
|
-
enabled: this.settings.skills?.enabled ?? true,
|
|
1130
|
-
enableSkillCommands: this.settings.skills?.enableSkillCommands ?? true,
|
|
1131
|
-
enableCodexUser: this.settings.skills?.enableCodexUser ?? true,
|
|
1132
|
-
enableClaudeUser: this.settings.skills?.enableClaudeUser ?? true,
|
|
1133
|
-
enableClaudeProject: this.settings.skills?.enableClaudeProject ?? true,
|
|
1134
|
-
enablePiUser: this.settings.skills?.enablePiUser ?? true,
|
|
1135
|
-
enablePiProject: this.settings.skills?.enablePiProject ?? true,
|
|
1136
|
-
customDirectories: [...(this.settings.skills?.customDirectories ?? [])],
|
|
1137
|
-
ignoredSkills: [...(this.settings.skills?.ignoredSkills ?? [])],
|
|
1138
|
-
includeSkills: [...(this.settings.skills?.includeSkills ?? [])],
|
|
1139
|
-
};
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
getEnableSkillCommands(): boolean {
|
|
1143
|
-
return this.settings.skills?.enableSkillCommands ?? true;
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
async setEnableSkillCommands(enabled: boolean): Promise<void> {
|
|
1147
|
-
if (!this.globalSettings.skills) {
|
|
1148
|
-
this.globalSettings.skills = {};
|
|
1149
|
-
}
|
|
1150
|
-
this.globalSettings.skills.enableSkillCommands = enabled;
|
|
1151
|
-
this.markModified("skills", "enableSkillCommands");
|
|
1152
|
-
await this.save();
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
getCommandsSettings(): Required<CommandsSettings> {
|
|
1156
|
-
return {
|
|
1157
|
-
enableClaudeUser: this.settings.commands?.enableClaudeUser ?? true,
|
|
1158
|
-
enableClaudeProject: this.settings.commands?.enableClaudeProject ?? true,
|
|
1159
|
-
};
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
getCommandsEnableClaudeUser(): boolean {
|
|
1163
|
-
return this.settings.commands?.enableClaudeUser ?? true;
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
async setCommandsEnableClaudeUser(enabled: boolean): Promise<void> {
|
|
1167
|
-
if (!this.globalSettings.commands) {
|
|
1168
|
-
this.globalSettings.commands = {};
|
|
1169
|
-
}
|
|
1170
|
-
this.globalSettings.commands.enableClaudeUser = enabled;
|
|
1171
|
-
this.markModified("commands", "enableClaudeUser");
|
|
1172
|
-
await this.save();
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
getCommandsEnableClaudeProject(): boolean {
|
|
1176
|
-
return this.settings.commands?.enableClaudeProject ?? true;
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
async setCommandsEnableClaudeProject(enabled: boolean): Promise<void> {
|
|
1180
|
-
if (!this.globalSettings.commands) {
|
|
1181
|
-
this.globalSettings.commands = {};
|
|
1182
|
-
}
|
|
1183
|
-
this.globalSettings.commands.enableClaudeProject = enabled;
|
|
1184
|
-
this.markModified("commands", "enableClaudeProject");
|
|
1185
|
-
await this.save();
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
getShowImages(): boolean {
|
|
1189
|
-
return this.settings.terminal?.showImages ?? true;
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
async setShowImages(show: boolean): Promise<void> {
|
|
1193
|
-
if (!this.globalSettings.terminal) {
|
|
1194
|
-
this.globalSettings.terminal = {};
|
|
1195
|
-
}
|
|
1196
|
-
this.globalSettings.terminal.showImages = show;
|
|
1197
|
-
this.markModified("terminal", "showImages");
|
|
1198
|
-
await this.save();
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
getNotificationOnComplete(): NotificationMethod {
|
|
1202
|
-
return this.settings.notifications?.onComplete ?? "auto";
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
async setNotificationOnComplete(method: NotificationMethod): Promise<void> {
|
|
1206
|
-
if (!this.globalSettings.notifications) {
|
|
1207
|
-
this.globalSettings.notifications = {};
|
|
1208
|
-
}
|
|
1209
|
-
this.globalSettings.notifications.onComplete = method;
|
|
1210
|
-
this.markModified("notifications", "onComplete");
|
|
1211
|
-
await this.save();
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
/** Get ask tool timeout in milliseconds (0 or null = disabled) */
|
|
1215
|
-
getAskTimeout(): number | null {
|
|
1216
|
-
const timeout = this.settings.ask?.timeout;
|
|
1217
|
-
if (timeout === null || timeout === 0) return null;
|
|
1218
|
-
return (timeout ?? 30) * 1000;
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
async setAskTimeout(seconds: number | null): Promise<void> {
|
|
1222
|
-
if (!this.globalSettings.ask) {
|
|
1223
|
-
this.globalSettings.ask = {};
|
|
1224
|
-
}
|
|
1225
|
-
this.globalSettings.ask.timeout = seconds;
|
|
1226
|
-
this.markModified("ask", "timeout");
|
|
1227
|
-
await this.save();
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
getAskNotification(): NotificationMethod {
|
|
1231
|
-
return this.settings.ask?.notification ?? "auto";
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
async setAskNotification(method: NotificationMethod): Promise<void> {
|
|
1235
|
-
if (!this.globalSettings.ask) {
|
|
1236
|
-
this.globalSettings.ask = {};
|
|
1237
|
-
}
|
|
1238
|
-
this.globalSettings.ask.notification = method;
|
|
1239
|
-
this.markModified("ask", "notification");
|
|
1240
|
-
await this.save();
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
getImageAutoResize(): boolean {
|
|
1244
|
-
return this.settings.images?.autoResize ?? true;
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
async setImageAutoResize(enabled: boolean): Promise<void> {
|
|
1248
|
-
if (!this.globalSettings.images) {
|
|
1249
|
-
this.globalSettings.images = {};
|
|
1250
|
-
}
|
|
1251
|
-
this.globalSettings.images.autoResize = enabled;
|
|
1252
|
-
this.markModified("images", "autoResize");
|
|
1253
|
-
await this.save();
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
getBlockImages(): boolean {
|
|
1257
|
-
return this.settings.images?.blockImages ?? false;
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
async setBlockImages(blocked: boolean): Promise<void> {
|
|
1261
|
-
if (!this.globalSettings.images) {
|
|
1262
|
-
this.globalSettings.images = {};
|
|
1263
|
-
}
|
|
1264
|
-
this.globalSettings.images.blockImages = blocked;
|
|
1265
|
-
this.markModified("images", "blockImages");
|
|
1266
|
-
await this.save();
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
getEnabledModels(): string[] | undefined {
|
|
1270
|
-
return this.settings.enabledModels;
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
getExaSettings(): Required<ExaSettings> {
|
|
1274
|
-
return {
|
|
1275
|
-
enabled: this.settings.exa?.enabled ?? true,
|
|
1276
|
-
enableSearch: this.settings.exa?.enableSearch ?? true,
|
|
1277
|
-
enableLinkedin: this.settings.exa?.enableLinkedin ?? false,
|
|
1278
|
-
enableCompany: this.settings.exa?.enableCompany ?? false,
|
|
1279
|
-
enableResearcher: this.settings.exa?.enableResearcher ?? false,
|
|
1280
|
-
enableWebsets: this.settings.exa?.enableWebsets ?? false,
|
|
1281
|
-
};
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
async setExaEnabled(enabled: boolean): Promise<void> {
|
|
1285
|
-
if (!this.globalSettings.exa) {
|
|
1286
|
-
this.globalSettings.exa = {};
|
|
1287
|
-
}
|
|
1288
|
-
this.globalSettings.exa.enabled = enabled;
|
|
1289
|
-
this.markModified("exa", "enabled");
|
|
1290
|
-
await this.save();
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
async setExaSearchEnabled(enabled: boolean): Promise<void> {
|
|
1294
|
-
if (!this.globalSettings.exa) {
|
|
1295
|
-
this.globalSettings.exa = {};
|
|
1296
|
-
}
|
|
1297
|
-
this.globalSettings.exa.enableSearch = enabled;
|
|
1298
|
-
this.markModified("exa", "enableSearch");
|
|
1299
|
-
await this.save();
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
async setExaLinkedinEnabled(enabled: boolean): Promise<void> {
|
|
1303
|
-
if (!this.globalSettings.exa) {
|
|
1304
|
-
this.globalSettings.exa = {};
|
|
1305
|
-
}
|
|
1306
|
-
this.globalSettings.exa.enableLinkedin = enabled;
|
|
1307
|
-
this.markModified("exa", "enableLinkedin");
|
|
1308
|
-
await this.save();
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
async setExaCompanyEnabled(enabled: boolean): Promise<void> {
|
|
1312
|
-
if (!this.globalSettings.exa) {
|
|
1313
|
-
this.globalSettings.exa = {};
|
|
1314
|
-
}
|
|
1315
|
-
this.globalSettings.exa.enableCompany = enabled;
|
|
1316
|
-
this.markModified("exa", "enableCompany");
|
|
1317
|
-
await this.save();
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
async setExaResearcherEnabled(enabled: boolean): Promise<void> {
|
|
1321
|
-
if (!this.globalSettings.exa) {
|
|
1322
|
-
this.globalSettings.exa = {};
|
|
1323
|
-
}
|
|
1324
|
-
this.globalSettings.exa.enableResearcher = enabled;
|
|
1325
|
-
this.markModified("exa", "enableResearcher");
|
|
1326
|
-
await this.save();
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
async setExaWebsetsEnabled(enabled: boolean): Promise<void> {
|
|
1330
|
-
if (!this.globalSettings.exa) {
|
|
1331
|
-
this.globalSettings.exa = {};
|
|
1332
|
-
}
|
|
1333
|
-
this.globalSettings.exa.enableWebsets = enabled;
|
|
1334
|
-
this.markModified("exa", "enableWebsets");
|
|
1335
|
-
await this.save();
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
// Provider settings
|
|
1339
|
-
getWebSearchProvider(): WebSearchProviderOption {
|
|
1340
|
-
return this.settings.providers?.webSearch ?? "auto";
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
async setWebSearchProvider(provider: WebSearchProviderOption): Promise<void> {
|
|
1344
|
-
if (!this.globalSettings.providers) {
|
|
1345
|
-
this.globalSettings.providers = {};
|
|
1346
|
-
}
|
|
1347
|
-
this.globalSettings.providers.webSearch = provider;
|
|
1348
|
-
this.markModified("providers", "webSearch");
|
|
1349
|
-
await this.save();
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
getImageProvider(): ImageProviderOption {
|
|
1353
|
-
return this.settings.providers?.image ?? "auto";
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
async setImageProvider(provider: ImageProviderOption): Promise<void> {
|
|
1357
|
-
if (!this.globalSettings.providers) {
|
|
1358
|
-
this.globalSettings.providers = {};
|
|
1359
|
-
}
|
|
1360
|
-
this.globalSettings.providers.image = provider;
|
|
1361
|
-
this.markModified("providers", "image");
|
|
1362
|
-
await this.save();
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
getKimiApiFormat(): KimiApiFormatOption {
|
|
1366
|
-
return this.settings.providers?.kimiApiFormat ?? "anthropic";
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
async setKimiApiFormat(format: KimiApiFormatOption): Promise<void> {
|
|
1370
|
-
if (!this.globalSettings.providers) {
|
|
1371
|
-
this.globalSettings.providers = {};
|
|
1372
|
-
}
|
|
1373
|
-
this.globalSettings.providers.kimiApiFormat = format;
|
|
1374
|
-
this.markModified("providers", "kimiApiFormat");
|
|
1375
|
-
await this.save();
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
getBashInterceptorEnabled(): boolean {
|
|
1379
|
-
return this.settings.bashInterceptor?.enabled ?? DEFAULT_BASH_INTERCEPTOR_SETTINGS.enabled;
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
getBashInterceptorSimpleLsEnabled(): boolean {
|
|
1383
|
-
return this.settings.bashInterceptor?.simpleLs ?? DEFAULT_BASH_INTERCEPTOR_SETTINGS.simpleLs;
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
getBashInterceptorRules(): BashInterceptorRule[] {
|
|
1387
|
-
return [...(this.settings.bashInterceptor?.patterns ?? DEFAULT_BASH_INTERCEPTOR_RULES)];
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
async setBashInterceptorEnabled(enabled: boolean): Promise<void> {
|
|
1391
|
-
if (!this.globalSettings.bashInterceptor) {
|
|
1392
|
-
this.globalSettings.bashInterceptor = {};
|
|
1393
|
-
}
|
|
1394
|
-
this.globalSettings.bashInterceptor.enabled = enabled;
|
|
1395
|
-
this.markModified("bashInterceptor", "enabled");
|
|
1396
|
-
await this.save();
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
async setBashInterceptorSimpleLsEnabled(enabled: boolean): Promise<void> {
|
|
1400
|
-
if (!this.globalSettings.bashInterceptor) {
|
|
1401
|
-
this.globalSettings.bashInterceptor = {};
|
|
1402
|
-
}
|
|
1403
|
-
this.globalSettings.bashInterceptor.simpleLs = enabled;
|
|
1404
|
-
this.markModified("bashInterceptor", "simpleLs");
|
|
1405
|
-
await this.save();
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
getPythonToolMode(): PythonToolMode {
|
|
1409
|
-
return this.settings.python?.toolMode ?? "both";
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
async setPythonToolMode(mode: PythonToolMode): Promise<void> {
|
|
1413
|
-
if (!this.globalSettings.python) {
|
|
1414
|
-
this.globalSettings.python = {};
|
|
1415
|
-
}
|
|
1416
|
-
this.globalSettings.python.toolMode = mode;
|
|
1417
|
-
this.markModified("python", "toolMode");
|
|
1418
|
-
await this.save();
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
getPythonKernelMode(): PythonKernelMode {
|
|
1422
|
-
return this.settings.python?.kernelMode ?? "session";
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
async setPythonKernelMode(mode: PythonKernelMode): Promise<void> {
|
|
1426
|
-
if (!this.globalSettings.python) {
|
|
1427
|
-
this.globalSettings.python = {};
|
|
1428
|
-
}
|
|
1429
|
-
this.globalSettings.python.kernelMode = mode;
|
|
1430
|
-
this.markModified("python", "kernelMode");
|
|
1431
|
-
await this.save();
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
getPythonSharedGateway(): boolean {
|
|
1435
|
-
return this.settings.python?.sharedGateway ?? true;
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
async setPythonSharedGateway(enabled: boolean): Promise<void> {
|
|
1439
|
-
if (!this.globalSettings.python) {
|
|
1440
|
-
this.globalSettings.python = {};
|
|
1441
|
-
}
|
|
1442
|
-
this.globalSettings.python.sharedGateway = enabled;
|
|
1443
|
-
this.markModified("python", "sharedGateway");
|
|
1444
|
-
await this.save();
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
getMCPProjectConfigEnabled(): boolean {
|
|
1448
|
-
return this.settings.mcp?.enableProjectConfig ?? true;
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
|
-
async setMCPProjectConfigEnabled(enabled: boolean): Promise<void> {
|
|
1452
|
-
if (!this.globalSettings.mcp) {
|
|
1453
|
-
this.globalSettings.mcp = {};
|
|
1454
|
-
}
|
|
1455
|
-
this.globalSettings.mcp.enableProjectConfig = enabled;
|
|
1456
|
-
this.markModified("mcp", "enableProjectConfig");
|
|
1457
|
-
await this.save();
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
getLspFormatOnWrite(): boolean {
|
|
1461
|
-
return this.settings.lsp?.formatOnWrite ?? false;
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
async setLspFormatOnWrite(enabled: boolean): Promise<void> {
|
|
1465
|
-
if (!this.globalSettings.lsp) {
|
|
1466
|
-
this.globalSettings.lsp = {};
|
|
1467
|
-
}
|
|
1468
|
-
this.globalSettings.lsp.formatOnWrite = enabled;
|
|
1469
|
-
this.markModified("lsp", "formatOnWrite");
|
|
1470
|
-
await this.save();
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
getLspDiagnosticsOnWrite(): boolean {
|
|
1474
|
-
return this.settings.lsp?.diagnosticsOnWrite ?? true;
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
async setLspDiagnosticsOnWrite(enabled: boolean): Promise<void> {
|
|
1478
|
-
if (!this.globalSettings.lsp) {
|
|
1479
|
-
this.globalSettings.lsp = {};
|
|
1480
|
-
}
|
|
1481
|
-
this.globalSettings.lsp.diagnosticsOnWrite = enabled;
|
|
1482
|
-
this.markModified("lsp", "diagnosticsOnWrite");
|
|
1483
|
-
await this.save();
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
getLspDiagnosticsOnEdit(): boolean {
|
|
1487
|
-
return this.settings.lsp?.diagnosticsOnEdit ?? false;
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
async setLspDiagnosticsOnEdit(enabled: boolean): Promise<void> {
|
|
1491
|
-
if (!this.globalSettings.lsp) {
|
|
1492
|
-
this.globalSettings.lsp = {};
|
|
1493
|
-
}
|
|
1494
|
-
this.globalSettings.lsp.diagnosticsOnEdit = enabled;
|
|
1495
|
-
this.markModified("lsp", "diagnosticsOnEdit");
|
|
1496
|
-
await this.save();
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
getEditFuzzyMatch(): boolean {
|
|
1500
|
-
return this.settings.edit?.fuzzyMatch ?? true;
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
async setEditFuzzyMatch(enabled: boolean): Promise<void> {
|
|
1504
|
-
if (!this.globalSettings.edit) {
|
|
1505
|
-
this.globalSettings.edit = {};
|
|
1506
|
-
}
|
|
1507
|
-
this.globalSettings.edit.fuzzyMatch = enabled;
|
|
1508
|
-
this.markModified("edit", "fuzzyMatch");
|
|
1509
|
-
await this.save();
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
getEditFuzzyThreshold(): number {
|
|
1513
|
-
return this.settings.edit?.fuzzyThreshold ?? 0.95;
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
async setEditFuzzyThreshold(value: number): Promise<void> {
|
|
1517
|
-
if (!this.globalSettings.edit) {
|
|
1518
|
-
this.globalSettings.edit = {};
|
|
1519
|
-
}
|
|
1520
|
-
this.globalSettings.edit.fuzzyThreshold = value;
|
|
1521
|
-
this.markModified("edit", "fuzzyThreshold");
|
|
1522
|
-
await this.save();
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
getEditPatchMode(): boolean {
|
|
1526
|
-
return this.settings.edit?.patchMode ?? true;
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
async setEditPatchMode(enabled: boolean): Promise<void> {
|
|
1530
|
-
if (!this.globalSettings.edit) {
|
|
1531
|
-
this.globalSettings.edit = {};
|
|
1532
|
-
}
|
|
1533
|
-
this.globalSettings.edit.patchMode = enabled;
|
|
1534
|
-
this.markModified("edit", "patchMode");
|
|
1535
|
-
await this.save();
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
getEditStreamingAbort(): boolean {
|
|
1539
|
-
return this.settings.edit?.streamingAbort ?? false;
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
async setEditStreamingAbort(enabled: boolean): Promise<void> {
|
|
1543
|
-
if (!this.globalSettings.edit) {
|
|
1544
|
-
this.globalSettings.edit = {};
|
|
1545
|
-
}
|
|
1546
|
-
this.globalSettings.edit.streamingAbort = enabled;
|
|
1547
|
-
this.markModified("edit", "streamingAbort");
|
|
1548
|
-
await this.save();
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
/**
|
|
1552
|
-
* Default model patterns that should use replace mode instead of patch mode.
|
|
1553
|
-
* These are models known to struggle with unified diff format.
|
|
1554
|
-
*/
|
|
1555
|
-
static readonly DEFAULT_REPLACE_MODE_PATTERNS = ["kimi"];
|
|
1556
|
-
|
|
1557
|
-
/**
|
|
1558
|
-
* Get the edit variant for a specific model.
|
|
1559
|
-
* Returns "patch", "replace", or null (use global default).
|
|
1560
|
-
*/
|
|
1561
|
-
getEditVariantForModel(model: string | undefined): "patch" | "replace" | null {
|
|
1562
|
-
if (!model) return null;
|
|
1563
|
-
const modelLower = model.toLowerCase();
|
|
1564
|
-
|
|
1565
|
-
const userVariants = this.settings.edit?.modelVariants;
|
|
1566
|
-
if (userVariants) {
|
|
1567
|
-
for (const [pattern, variant] of Object.entries(userVariants)) {
|
|
1568
|
-
if (modelLower.includes(pattern.toLowerCase())) {
|
|
1569
|
-
return variant;
|
|
1570
|
-
}
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
|
|
1574
|
-
for (const pattern of SettingsManager.DEFAULT_REPLACE_MODE_PATTERNS) {
|
|
1575
|
-
if (modelLower.includes(pattern)) {
|
|
1576
|
-
return "replace";
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
return null;
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
getEditModelVariants(): Record<string, "patch" | "replace"> {
|
|
1584
|
-
return this.settings.edit?.modelVariants ?? {};
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
async setEditModelVariant(pattern: string, variant: "patch" | "replace" | null): Promise<void> {
|
|
1588
|
-
if (!this.globalSettings.edit) {
|
|
1589
|
-
this.globalSettings.edit = {};
|
|
1590
|
-
}
|
|
1591
|
-
if (!this.globalSettings.edit.modelVariants) {
|
|
1592
|
-
this.globalSettings.edit.modelVariants = {};
|
|
1593
|
-
}
|
|
1594
|
-
if (variant === null) {
|
|
1595
|
-
delete this.globalSettings.edit.modelVariants[pattern];
|
|
1596
|
-
} else {
|
|
1597
|
-
this.globalSettings.edit.modelVariants[pattern] = variant;
|
|
1598
|
-
}
|
|
1599
|
-
this.markModified("edit", "modelVariants");
|
|
1600
|
-
await this.save();
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
getNormativeRewrite(): boolean {
|
|
1604
|
-
return this.settings.normativeRewrite ?? false;
|
|
1605
|
-
}
|
|
1606
|
-
|
|
1607
|
-
async setNormativeRewrite(enabled: boolean): Promise<void> {
|
|
1608
|
-
this.globalSettings.normativeRewrite = enabled;
|
|
1609
|
-
this.markModified("normativeRewrite");
|
|
1610
|
-
await this.save();
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
getReadLineNumbers(): boolean {
|
|
1614
|
-
return this.settings.readLineNumbers ?? false;
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
async setReadLineNumbers(enabled: boolean): Promise<void> {
|
|
1618
|
-
this.globalSettings.readLineNumbers = enabled;
|
|
1619
|
-
this.markModified("readLineNumbers");
|
|
1620
|
-
await this.save();
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
getDisabledProviders(): string[] {
|
|
1624
|
-
return [...(this.settings.disabledProviders ?? [])];
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
async setDisabledProviders(providerIds: string[]): Promise<void> {
|
|
1628
|
-
this.globalSettings.disabledProviders = providerIds;
|
|
1629
|
-
this.markModified("disabledProviders");
|
|
1630
|
-
await this.save();
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
|
-
getDisabledExtensions(): string[] {
|
|
1634
|
-
return [...(this.settings.disabledExtensions ?? [])];
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
async setDisabledExtensions(extensionIds: string[]): Promise<void> {
|
|
1638
|
-
this.globalSettings.disabledExtensions = extensionIds;
|
|
1639
|
-
this.markModified("disabledExtensions");
|
|
1640
|
-
await this.save();
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
isExtensionEnabled(extensionId: string): boolean {
|
|
1644
|
-
return !(this.settings.disabledExtensions ?? []).includes(extensionId);
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
async enableExtension(extensionId: string): Promise<void> {
|
|
1648
|
-
const disabled = this.globalSettings.disabledExtensions ?? [];
|
|
1649
|
-
const index = disabled.indexOf(extensionId);
|
|
1650
|
-
if (index !== -1) {
|
|
1651
|
-
disabled.splice(index, 1);
|
|
1652
|
-
this.globalSettings.disabledExtensions = disabled;
|
|
1653
|
-
this.markModified("disabledExtensions");
|
|
1654
|
-
await this.save();
|
|
1655
|
-
}
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
async disableExtension(extensionId: string): Promise<void> {
|
|
1659
|
-
const disabled = this.globalSettings.disabledExtensions ?? [];
|
|
1660
|
-
if (!disabled.includes(extensionId)) {
|
|
1661
|
-
disabled.push(extensionId);
|
|
1662
|
-
this.globalSettings.disabledExtensions = disabled;
|
|
1663
|
-
this.markModified("disabledExtensions");
|
|
1664
|
-
await this.save();
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1667
|
-
|
|
1668
|
-
getTtsrSettings(): TtsrSettings {
|
|
1669
|
-
return this.settings.ttsr ?? {};
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
async setTtsrSettings(settings: TtsrSettings): Promise<void> {
|
|
1673
|
-
this.globalSettings.ttsr = { ...this.globalSettings.ttsr, ...settings };
|
|
1674
|
-
this.markModified("ttsr");
|
|
1675
|
-
await this.save();
|
|
1676
|
-
}
|
|
1677
|
-
|
|
1678
|
-
getTtsrEnabled(): boolean {
|
|
1679
|
-
return this.settings.ttsr?.enabled ?? true;
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
async setTtsrEnabled(enabled: boolean): Promise<void> {
|
|
1683
|
-
if (!this.globalSettings.ttsr) {
|
|
1684
|
-
this.globalSettings.ttsr = {};
|
|
1685
|
-
}
|
|
1686
|
-
this.globalSettings.ttsr.enabled = enabled;
|
|
1687
|
-
this.markModified("ttsr", "enabled");
|
|
1688
|
-
await this.save();
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
|
-
getTtsrContextMode(): "keep" | "discard" {
|
|
1692
|
-
return this.settings.ttsr?.contextMode ?? "discard";
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
async setTtsrContextMode(mode: "keep" | "discard"): Promise<void> {
|
|
1696
|
-
if (!this.globalSettings.ttsr) {
|
|
1697
|
-
this.globalSettings.ttsr = {};
|
|
1698
|
-
}
|
|
1699
|
-
this.globalSettings.ttsr.contextMode = mode;
|
|
1700
|
-
this.markModified("ttsr", "contextMode");
|
|
1701
|
-
await this.save();
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
|
-
getTtsrRepeatMode(): "once" | "after-gap" {
|
|
1705
|
-
return this.settings.ttsr?.repeatMode ?? "once";
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
async setTtsrRepeatMode(mode: "once" | "after-gap"): Promise<void> {
|
|
1709
|
-
if (!this.globalSettings.ttsr) {
|
|
1710
|
-
this.globalSettings.ttsr = {};
|
|
1711
|
-
}
|
|
1712
|
-
this.globalSettings.ttsr.repeatMode = mode;
|
|
1713
|
-
this.markModified("ttsr", "repeatMode");
|
|
1714
|
-
await this.save();
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
getTtsrRepeatGap(): number {
|
|
1718
|
-
return this.settings.ttsr?.repeatGap ?? 10;
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
|
-
async setTtsrRepeatGap(gap: number): Promise<void> {
|
|
1722
|
-
if (!this.globalSettings.ttsr) {
|
|
1723
|
-
this.globalSettings.ttsr = {};
|
|
1724
|
-
}
|
|
1725
|
-
this.globalSettings.ttsr.repeatGap = gap;
|
|
1726
|
-
this.markModified("ttsr", "repeatGap");
|
|
1727
|
-
await this.save();
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1731
|
-
// Status Line Settings
|
|
1732
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1733
|
-
|
|
1734
|
-
getStatusLineSettings(): StatusLineSettings {
|
|
1735
|
-
return this.settings.statusLine ? { ...this.settings.statusLine } : {};
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
getStatusLinePreset(): StatusLinePreset {
|
|
1739
|
-
return this.settings.statusLine?.preset ?? "default";
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
async setStatusLinePreset(preset: StatusLinePreset): Promise<void> {
|
|
1743
|
-
if (!this.globalSettings.statusLine) {
|
|
1744
|
-
this.globalSettings.statusLine = {};
|
|
1745
|
-
}
|
|
1746
|
-
if (preset !== "custom") {
|
|
1747
|
-
delete this.globalSettings.statusLine.leftSegments;
|
|
1748
|
-
delete this.globalSettings.statusLine.rightSegments;
|
|
1749
|
-
delete this.globalSettings.statusLine.segmentOptions;
|
|
1750
|
-
this.markModified("statusLine", "leftSegments");
|
|
1751
|
-
this.markModified("statusLine", "rightSegments");
|
|
1752
|
-
this.markModified("statusLine", "segmentOptions");
|
|
1753
|
-
}
|
|
1754
|
-
this.globalSettings.statusLine.preset = preset;
|
|
1755
|
-
this.markModified("statusLine", "preset");
|
|
1756
|
-
await this.save();
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
getStatusLineSeparator(): StatusLineSeparatorStyle {
|
|
1760
|
-
return this.settings.statusLine?.separator ?? "powerline-thin";
|
|
1761
|
-
}
|
|
1762
|
-
|
|
1763
|
-
async setStatusLineSeparator(separator: StatusLineSeparatorStyle): Promise<void> {
|
|
1764
|
-
if (!this.globalSettings.statusLine) {
|
|
1765
|
-
this.globalSettings.statusLine = {};
|
|
1766
|
-
}
|
|
1767
|
-
this.globalSettings.statusLine.separator = separator;
|
|
1768
|
-
this.markModified("statusLine", "separator");
|
|
1769
|
-
await this.save();
|
|
1770
|
-
}
|
|
1771
|
-
|
|
1772
|
-
getStatusLineLeftSegments(): StatusLineSegmentId[] {
|
|
1773
|
-
return [...(this.settings.statusLine?.leftSegments ?? [])];
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
async setStatusLineLeftSegments(segments: StatusLineSegmentId[]): Promise<void> {
|
|
1777
|
-
if (!this.globalSettings.statusLine) {
|
|
1778
|
-
this.globalSettings.statusLine = {};
|
|
1779
|
-
}
|
|
1780
|
-
this.globalSettings.statusLine.leftSegments = segments;
|
|
1781
|
-
this.markModified("statusLine", "leftSegments");
|
|
1782
|
-
// Setting segments explicitly implies custom preset
|
|
1783
|
-
if (this.globalSettings.statusLine.preset !== "custom") {
|
|
1784
|
-
this.globalSettings.statusLine.preset = "custom";
|
|
1785
|
-
this.markModified("statusLine", "preset");
|
|
1786
|
-
}
|
|
1787
|
-
await this.save();
|
|
1788
|
-
}
|
|
1789
|
-
|
|
1790
|
-
getStatusLineRightSegments(): StatusLineSegmentId[] {
|
|
1791
|
-
return [...(this.settings.statusLine?.rightSegments ?? [])];
|
|
1792
|
-
}
|
|
1793
|
-
|
|
1794
|
-
async setStatusLineRightSegments(segments: StatusLineSegmentId[]): Promise<void> {
|
|
1795
|
-
if (!this.globalSettings.statusLine) {
|
|
1796
|
-
this.globalSettings.statusLine = {};
|
|
1797
|
-
}
|
|
1798
|
-
this.globalSettings.statusLine.rightSegments = segments;
|
|
1799
|
-
this.markModified("statusLine", "rightSegments");
|
|
1800
|
-
// Setting segments explicitly implies custom preset
|
|
1801
|
-
if (this.globalSettings.statusLine.preset !== "custom") {
|
|
1802
|
-
this.globalSettings.statusLine.preset = "custom";
|
|
1803
|
-
this.markModified("statusLine", "preset");
|
|
1804
|
-
}
|
|
1805
|
-
await this.save();
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
|
-
getStatusLineSegmentOptions(): StatusLineSegmentOptions {
|
|
1809
|
-
return { ...this.settings.statusLine?.segmentOptions };
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
async setStatusLineSegmentOption<K extends keyof StatusLineSegmentOptions>(
|
|
1813
|
-
segment: K,
|
|
1814
|
-
option: keyof NonNullable<StatusLineSegmentOptions[K]>,
|
|
1815
|
-
value: boolean | number | string,
|
|
1816
|
-
): Promise<void> {
|
|
1817
|
-
if (!this.globalSettings.statusLine) {
|
|
1818
|
-
this.globalSettings.statusLine = {};
|
|
1819
|
-
}
|
|
1820
|
-
if (!this.globalSettings.statusLine.segmentOptions) {
|
|
1821
|
-
this.globalSettings.statusLine.segmentOptions = {};
|
|
1822
|
-
}
|
|
1823
|
-
if (!this.globalSettings.statusLine.segmentOptions[segment]) {
|
|
1824
|
-
this.globalSettings.statusLine.segmentOptions[segment] = {} as NonNullable<StatusLineSegmentOptions[K]>;
|
|
1825
|
-
}
|
|
1826
|
-
(this.globalSettings.statusLine.segmentOptions[segment] as Record<string, unknown>)[option as string] = value;
|
|
1827
|
-
this.markModified("statusLine", "segmentOptions");
|
|
1828
|
-
await this.save();
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
|
-
async clearStatusLineSegmentOption<K extends keyof StatusLineSegmentOptions>(
|
|
1832
|
-
segment: K,
|
|
1833
|
-
option: keyof NonNullable<StatusLineSegmentOptions[K]>,
|
|
1834
|
-
): Promise<void> {
|
|
1835
|
-
const segmentOptions = this.globalSettings.statusLine?.segmentOptions;
|
|
1836
|
-
if (!segmentOptions || !segmentOptions[segment]) {
|
|
1837
|
-
return;
|
|
1838
|
-
}
|
|
1839
|
-
delete (segmentOptions[segment] as Record<string, unknown>)[option as string];
|
|
1840
|
-
if (Object.keys(segmentOptions[segment] as Record<string, unknown>).length === 0) {
|
|
1841
|
-
delete segmentOptions[segment];
|
|
1842
|
-
}
|
|
1843
|
-
if (Object.keys(segmentOptions).length === 0) {
|
|
1844
|
-
delete this.globalSettings.statusLine?.segmentOptions;
|
|
1845
|
-
}
|
|
1846
|
-
this.markModified("statusLine", "segmentOptions");
|
|
1847
|
-
await this.save();
|
|
1848
|
-
}
|
|
1849
|
-
|
|
1850
|
-
getStatusLineShowHookStatus(): boolean {
|
|
1851
|
-
return this.settings.statusLine?.showHookStatus ?? true;
|
|
1852
|
-
}
|
|
1853
|
-
|
|
1854
|
-
async setStatusLineShowHookStatus(show: boolean): Promise<void> {
|
|
1855
|
-
if (!this.globalSettings.statusLine) {
|
|
1856
|
-
this.globalSettings.statusLine = {};
|
|
1857
|
-
}
|
|
1858
|
-
this.globalSettings.statusLine.showHookStatus = show;
|
|
1859
|
-
this.markModified("statusLine", "showHookStatus");
|
|
1860
|
-
await this.save();
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
getDoubleEscapeAction(): "branch" | "tree" {
|
|
1864
|
-
return this.settings.doubleEscapeAction ?? "tree";
|
|
1865
|
-
}
|
|
1866
|
-
|
|
1867
|
-
async setDoubleEscapeAction(action: "branch" | "tree"): Promise<void> {
|
|
1868
|
-
this.globalSettings.doubleEscapeAction = action;
|
|
1869
|
-
this.markModified("doubleEscapeAction");
|
|
1870
|
-
await this.save();
|
|
1871
|
-
}
|
|
1872
|
-
|
|
1873
|
-
getShowHardwareCursor(): boolean {
|
|
1874
|
-
// Check settings first
|
|
1875
|
-
if (this.settings.showHardwareCursor !== undefined) {
|
|
1876
|
-
return this.settings.showHardwareCursor;
|
|
1877
|
-
}
|
|
1878
|
-
// Check env var override
|
|
1879
|
-
const envVar = process.env.OMP_HARDWARE_CURSOR?.toLowerCase();
|
|
1880
|
-
if (envVar === "0" || envVar === "false") return false;
|
|
1881
|
-
if (envVar === "1" || envVar === "true") return true;
|
|
1882
|
-
// Default to true on Linux/macOS for IME support
|
|
1883
|
-
return process.platform === "linux" || process.platform === "darwin";
|
|
1884
|
-
}
|
|
1885
|
-
|
|
1886
|
-
async setShowHardwareCursor(show: boolean): Promise<void> {
|
|
1887
|
-
this.globalSettings.showHardwareCursor = show;
|
|
1888
|
-
this.markModified("showHardwareCursor");
|
|
1889
|
-
await this.save();
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1892
|
-
/**
|
|
1893
|
-
* Get environment variables from settings
|
|
1894
|
-
*/
|
|
1895
|
-
getEnvironmentVariables(): Record<string, string> {
|
|
1896
|
-
return { ...(this.settings.env ?? {}) };
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
/**
|
|
1900
|
-
* Set environment variables in settings (not process.env)
|
|
1901
|
-
* This will be applied on next startup or reload
|
|
1902
|
-
*/
|
|
1903
|
-
async setEnvironmentVariables(envVars: Record<string, string>): Promise<void> {
|
|
1904
|
-
this.globalSettings.env = { ...envVars };
|
|
1905
|
-
this.markModified("env");
|
|
1906
|
-
await this.save();
|
|
1907
|
-
}
|
|
1908
|
-
|
|
1909
|
-
/**
|
|
1910
|
-
* Clear all environment variables from settings
|
|
1911
|
-
*/
|
|
1912
|
-
async clearEnvironmentVariables(): Promise<void> {
|
|
1913
|
-
delete this.globalSettings.env;
|
|
1914
|
-
this.markModified("env");
|
|
1915
|
-
await this.save();
|
|
1916
|
-
}
|
|
1917
|
-
|
|
1918
|
-
/**
|
|
1919
|
-
* Set a single environment variable in settings
|
|
1920
|
-
*/
|
|
1921
|
-
async setEnvironmentVariable(key: string, value: string): Promise<void> {
|
|
1922
|
-
if (!this.globalSettings.env) {
|
|
1923
|
-
this.globalSettings.env = {};
|
|
1924
|
-
}
|
|
1925
|
-
this.globalSettings.env[key] = value;
|
|
1926
|
-
this.markModified("env", key);
|
|
1927
|
-
await this.save();
|
|
1928
|
-
}
|
|
1929
|
-
|
|
1930
|
-
/**
|
|
1931
|
-
* Remove a single environment variable from settings
|
|
1932
|
-
*/
|
|
1933
|
-
async removeEnvironmentVariable(key: string): Promise<void> {
|
|
1934
|
-
if (this.globalSettings.env) {
|
|
1935
|
-
delete this.globalSettings.env[key];
|
|
1936
|
-
this.markModified("env", key);
|
|
1937
|
-
await this.save();
|
|
1938
|
-
}
|
|
1939
|
-
}
|
|
1940
|
-
|
|
1941
|
-
_compareUniqueCtorKeys(cwd: string, agentDir: string): boolean {
|
|
1942
|
-
if (this.cwd !== cwd) {
|
|
1943
|
-
cwd = path.normalize(cwd);
|
|
1944
|
-
if (this.cwd !== cwd) {
|
|
1945
|
-
return false;
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
if (this.agentDir !== agentDir) {
|
|
1949
|
-
agentDir = path.normalize(agentDir);
|
|
1950
|
-
if (this.agentDir !== agentDir) {
|
|
1951
|
-
return false;
|
|
1952
|
-
}
|
|
1953
|
-
}
|
|
1954
|
-
return true;
|
|
1955
|
-
}
|
|
1956
|
-
|
|
1957
|
-
/**
|
|
1958
|
-
* Acquire the last created SettingsManager instance.
|
|
1959
|
-
* If no instance exists, create a new one.
|
|
1960
|
-
* @returns The SettingsManager instance
|
|
1961
|
-
*/
|
|
1962
|
-
static acquire(
|
|
1963
|
-
cwd: string = process.cwd(),
|
|
1964
|
-
agentDir: string = getAgentDir(),
|
|
1965
|
-
): SettingsManager | Promise<SettingsManager> {
|
|
1966
|
-
const prev = SettingsManager.#lastInstance;
|
|
1967
|
-
if (prev?._compareUniqueCtorKeys(cwd, agentDir)) {
|
|
1968
|
-
return prev;
|
|
1969
|
-
}
|
|
1970
|
-
return SettingsManager.create(cwd, agentDir);
|
|
1971
|
-
}
|
|
1972
|
-
|
|
1973
|
-
/**
|
|
1974
|
-
* Gets the shell configuration
|
|
1975
|
-
* @returns The shell configuration
|
|
1976
|
-
*/
|
|
1977
|
-
getShellConfig() {
|
|
1978
|
-
if (this.getShellForceBasic()) {
|
|
1979
|
-
const basicShell = resolveBasicShell();
|
|
1980
|
-
if (basicShell) {
|
|
1981
|
-
return procmgr.getShellConfig(basicShell);
|
|
1982
|
-
}
|
|
1983
|
-
}
|
|
1984
|
-
const shell = this.getShellPath();
|
|
1985
|
-
return procmgr.getShellConfig(shell);
|
|
1986
|
-
}
|
|
1987
|
-
|
|
1988
|
-
/**
|
|
1989
|
-
* Gets the shell configuration from the last created SettingsManager instance.
|
|
1990
|
-
* @returns The shell configuration
|
|
1991
|
-
*/
|
|
1992
|
-
static async getGlobalShellConfig() {
|
|
1993
|
-
const settings = await SettingsManager.acquire();
|
|
1994
|
-
return settings.getShellConfig();
|
|
1995
|
-
}
|
|
1996
|
-
}
|
|
1997
|
-
|
|
1998
|
-
function resolveBasicShell(): string | undefined {
|
|
1999
|
-
const searchPaths = ["/bin", "/usr/bin", "/usr/local/bin", "/opt/homebrew/bin"];
|
|
2000
|
-
const candidates = ["bash", "sh"];
|
|
2001
|
-
|
|
2002
|
-
for (const name of candidates) {
|
|
2003
|
-
for (const dir of searchPaths) {
|
|
2004
|
-
const fullPath = path.join(dir, name);
|
|
2005
|
-
if (fs.existsSync(fullPath)) return fullPath;
|
|
2006
|
-
}
|
|
2007
|
-
}
|
|
2008
|
-
|
|
2009
|
-
for (const name of ["bash", "bash.exe", "sh", "sh.exe"]) {
|
|
2010
|
-
const resolved = Bun.which(name);
|
|
2011
|
-
if (resolved) return resolved;
|
|
2012
|
-
}
|
|
2013
|
-
|
|
2014
|
-
return undefined;
|
|
2015
|
-
}
|