@oh-my-pi/pi-coding-agent 13.2.1 → 13.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +43 -2
  2. package/package.json +7 -7
  3. package/scripts/generate-docs-index.ts +2 -2
  4. package/src/cli/args.ts +2 -1
  5. package/src/cli/config-cli.ts +32 -20
  6. package/src/config/settings-schema.ts +96 -14
  7. package/src/config/settings.ts +10 -0
  8. package/src/discovery/claude.ts +24 -6
  9. package/src/discovery/helpers.ts +9 -2
  10. package/src/ipy/runtime.ts +1 -0
  11. package/src/mcp/config.ts +1 -1
  12. package/src/modes/components/settings-defs.ts +53 -1
  13. package/src/modes/components/status-line.ts +7 -5
  14. package/src/modes/controllers/mcp-command-controller.ts +4 -3
  15. package/src/modes/controllers/selector-controller.ts +46 -0
  16. package/src/modes/interactive-mode.ts +9 -0
  17. package/src/modes/oauth-manual-input.ts +42 -0
  18. package/src/modes/types.ts +2 -0
  19. package/src/patch/hashline.ts +19 -1
  20. package/src/patch/index.ts +7 -8
  21. package/src/prompts/system/commit-message-system.md +2 -0
  22. package/src/prompts/system/subagent-submit-reminder.md +3 -3
  23. package/src/prompts/system/subagent-system-prompt.md +4 -4
  24. package/src/prompts/system/system-prompt.md +13 -0
  25. package/src/prompts/tools/hashline.md +45 -1
  26. package/src/prompts/tools/task-summary.md +4 -4
  27. package/src/prompts/tools/task.md +1 -1
  28. package/src/sdk.ts +8 -0
  29. package/src/slash-commands/builtin-registry.ts +26 -1
  30. package/src/system-prompt.ts +4 -0
  31. package/src/task/index.ts +211 -70
  32. package/src/task/render.ts +44 -16
  33. package/src/task/types.ts +6 -1
  34. package/src/task/worktree.ts +394 -31
  35. package/src/tools/review.ts +50 -1
  36. package/src/tools/submit-result.ts +22 -23
  37. package/src/utils/commit-message-generator.ts +132 -0
  38. package/src/web/search/providers/exa.ts +41 -4
  39. package/src/web/search/providers/perplexity.ts +20 -8
package/CHANGELOG.md CHANGED
@@ -2,6 +2,46 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [13.3.1] - 2026-02-26
6
+
7
+ ### Added
8
+
9
+ - Added `topP` setting to control nucleus sampling cutoff for model output diversity
10
+ - Added `topK` setting to sample from top-K tokens for controlled generation
11
+ - Added `minP` setting to enforce minimum probability threshold for token selection
12
+ - Added `presencePenalty` setting to penalize introduction of already-present tokens
13
+ - Added `repetitionPenalty` setting to penalize repeated tokens in model output
14
+
15
+ ### Fixed
16
+
17
+ - Fixed skill discovery to continue loading project skills when user skills directory is missing
18
+
19
+ ## [13.3.0] - 2026-02-26
20
+
21
+ ### Breaking Changes
22
+
23
+ - Renamed `task.isolation.enabled` (boolean) setting to `task.isolation.mode` (enum: `none`, `worktree`, `fuse-overlay`). Existing `true`/`false` values are auto-migrated to `worktree`/`none`.
24
+
25
+ ### Added
26
+
27
+ - Added `PERPLEXITY_COOKIES` env var for Perplexity web search via session cookies extracted from desktop app
28
+ - Added `fuse-overlay` isolation mode for subagents using `fuse-overlayfs` (copy-on-write overlay, no baseline patch apply needed)
29
+ - Added `task.isolation.merge` setting (`patch` or `branch`) to control how isolated task changes are integrated back. `branch` mode commits each task to a temp branch and cherry-picks for clean commit history
30
+ - Added `task.isolation.commits` setting (`generic` or `ai`) for commit messages on isolated task branches and nested repos. `ai` mode uses a smol model to generate conventional commit messages from diffs
31
+ - Nested non-submodule git repos are now discovered and handled during task isolation (changes captured and applied independently from parent repo)
32
+ - Added `task.eager` setting to encourage the agent to delegate work to subagents by default
33
+ - Added manual OAuth login flow that lets users paste redirect URLs with /login for callback-server providers and prevents overlapping logins
34
+
35
+ ### Fixed
36
+
37
+ - Fixed nested repo changes being lost when tasks commit inside the isolation (baseline state is now committed before task runs, so delta correctly excludes it)
38
+ - Fixed nested repo patches conflicting when multiple tasks contribute to the same repo (baseline untracked files no longer leak into patches)
39
+ - Nested repo changes are now committed after patch application (previously left as untracked files)
40
+ - Failed tasks no longer create stale branches or capture garbage patches (gated on exit code)
41
+ - Merge failures (e.g. conflicting patches) are now non-fatal — agent output is preserved with `merge failed` status instead of `failed`
42
+ - Stale branches are cleaned up when `commitToBranch` fails
43
+ - Commit message generator filters lock files from diffs before AI summarization
44
+
5
45
  ## [13.2.1] - 2026-02-24
6
46
 
7
47
  ### Fixed
@@ -11,7 +51,6 @@
11
51
  ### Changed
12
52
 
13
53
  - Extracted non-interactive environment config from `bash-interactive.ts` into shared `non-interactive-env.ts` module, applied consistently to all bash execution paths
14
-
15
54
  ## [13.2.0] - 2026-02-23
16
55
  ### Breaking Changes
17
56
 
@@ -34,12 +73,14 @@
34
73
  - Removed unused SSH resource cleanup functions `closeAllConnections` and `unmountAll` from session imports
35
74
 
36
75
  ## [13.1.2] - 2026-02-23
37
- ### Breaking Changes
38
76
 
77
+ ### Breaking Changes
39
78
  - Removed `timeout` parameter from await tool—tool now waits indefinitely until jobs complete or the call is aborted
40
79
  - Renamed `job_ids` parameter to `jobs` in await tool schema
41
80
  - Removed `timedOut` field from await tool result details
42
81
 
82
+ ### Changed
83
+ - Resolved docs index generation paths using path.resolve relative to the script directory
43
84
  ## [13.1.1] - 2026-02-23
44
85
 
45
86
  ### Fixed
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "13.2.1",
4
+ "version": "13.3.1",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -41,12 +41,12 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@mozilla/readability": "^0.6",
44
- "@oh-my-pi/omp-stats": "13.2.1",
45
- "@oh-my-pi/pi-agent-core": "13.2.1",
46
- "@oh-my-pi/pi-ai": "13.2.1",
47
- "@oh-my-pi/pi-natives": "13.2.1",
48
- "@oh-my-pi/pi-tui": "13.2.1",
49
- "@oh-my-pi/pi-utils": "13.2.1",
44
+ "@oh-my-pi/omp-stats": "13.3.1",
45
+ "@oh-my-pi/pi-agent-core": "13.3.1",
46
+ "@oh-my-pi/pi-ai": "13.3.1",
47
+ "@oh-my-pi/pi-natives": "13.3.1",
48
+ "@oh-my-pi/pi-tui": "13.3.1",
49
+ "@oh-my-pi/pi-utils": "13.3.1",
50
50
  "@sinclair/typebox": "^0.34",
51
51
  "@xterm/headless": "^6.0",
52
52
  "ajv": "^8.18",
@@ -3,8 +3,8 @@
3
3
  import { Glob } from "bun";
4
4
  import * as path from "node:path";
5
5
 
6
- const docsDir = new URL("../../../docs/", import.meta.url).pathname;
7
- const outputPath = new URL("../src/internal-urls/docs-index.generated.ts", import.meta.url).pathname;
6
+ const docsDir = path.resolve(import.meta.dir, "../../../docs");
7
+ const outputPath = path.resolve(import.meta.dir, "../src/internal-urls/docs-index.generated.ts");
8
8
 
9
9
  const glob = new Glob("**/*.md");
10
10
  const entries: string[] = [];
package/src/cli/args.ts CHANGED
@@ -216,7 +216,8 @@ export function getExtraHelpText(): string {
216
216
  ${chalk.dim("# Search & Tools")}
217
217
  EXA_API_KEY - Exa web search
218
218
  BRAVE_API_KEY - Brave web search
219
- PERPLEXITY_API_KEY - Perplexity web search
219
+ PERPLEXITY_API_KEY - Perplexity web search (API)
220
+ PERPLEXITY_COOKIES - Perplexity web search (session cookie)
220
221
  ANTHROPIC_SEARCH_API_KEY - Anthropic search provider
221
222
 
222
223
  ${chalk.dim("# Configuration")}
@@ -2,7 +2,7 @@
2
2
  * Config CLI command handlers.
3
3
  *
4
4
  * Handles `omp config <command>` subcommands for managing settings.
5
- * Uses settings-defs as the source of truth for available settings.
5
+ * Uses the settings schema as the source of truth for available settings.
6
6
  */
7
7
 
8
8
  import { APP_NAME, getAgentDir } from "@oh-my-pi/pi-utils";
@@ -11,12 +11,13 @@ import {
11
11
  getDefault,
12
12
  getEnumValues,
13
13
  getType,
14
+ getUi,
14
15
  type SettingPath,
15
16
  Settings,
16
17
  type SettingValue,
17
18
  settings,
18
19
  } from "../config/settings";
19
- import { getAllSettingDefs, type SettingDef } from "../modes/components/settings-defs";
20
+ import { SETTINGS_SCHEMA } from "../config/settings-schema";
20
21
  import { theme } from "../modes/theme/theme";
21
22
 
22
23
  // =============================================================================
@@ -38,21 +39,32 @@ export interface ConfigCommandArgs {
38
39
  // Setting Filtering
39
40
  // =============================================================================
40
41
 
42
+ type CliSettingDef = {
43
+ path: SettingPath;
44
+ type: string;
45
+ description: string;
46
+ tab: string;
47
+ };
48
+
49
+ const ALL_SETTING_PATHS = Object.keys(SETTINGS_SCHEMA) as SettingPath[];
50
+
41
51
  /** Find setting definition by path */
42
- function findSettingDef(path: string): SettingDef | undefined {
43
- return getAllSettingDefs().find(def => def.path === path);
52
+ function findSettingDef(path: string): CliSettingDef | undefined {
53
+ if (!(path in SETTINGS_SCHEMA)) return undefined;
54
+ const key = path as SettingPath;
55
+ const ui = getUi(key);
56
+ return {
57
+ path: key,
58
+ type: getType(key),
59
+ description: ui?.description ?? "",
60
+ tab: ui?.tab ?? "internal",
61
+ };
44
62
  }
45
63
 
46
64
  /** Get available values for a setting */
47
- function getSettingValues(def: SettingDef): readonly string[] | undefined {
65
+ function getSettingValues(def: CliSettingDef): readonly string[] | undefined {
48
66
  if (def.type === "enum") {
49
- return def.values;
50
- }
51
- if (def.type === "submenu") {
52
- const options = def.options;
53
- if (options.length > 0) {
54
- return options.map(o => o.value);
55
- }
67
+ return getEnumValues(def.path);
56
68
  }
57
69
  return undefined;
58
70
  }
@@ -125,7 +137,7 @@ function formatValue(value: unknown): string {
125
137
  return chalk.yellow(String(value));
126
138
  }
127
139
 
128
- function getTypeDisplay(def: SettingDef): string {
140
+ function getTypeDisplay(def: CliSettingDef): string {
129
141
  if (def.type === "boolean") {
130
142
  return "(boolean)";
131
143
  }
@@ -199,13 +211,13 @@ export async function runConfigCommand(cmd: ConfigCommandArgs): Promise<void> {
199
211
  }
200
212
 
201
213
  function handleList(flags: { json?: boolean }): void {
202
- const defs = getAllSettingDefs();
214
+ const defs = ALL_SETTING_PATHS.map(path => findSettingDef(path)).filter((def): def is CliSettingDef => !!def);
203
215
 
204
216
  if (flags.json) {
205
217
  const result: Record<string, { value: unknown; type: string; description: string }> = {};
206
218
  for (const def of defs) {
207
219
  result[def.path] = {
208
- value: settings.get(def.path as SettingPath),
220
+ value: settings.get(def.path),
209
221
  type: def.type,
210
222
  description: def.description,
211
223
  };
@@ -216,7 +228,7 @@ function handleList(flags: { json?: boolean }): void {
216
228
 
217
229
  console.log(chalk.bold("Settings:\n"));
218
230
 
219
- const groups: Record<string, SettingDef[]> = {};
231
+ const groups: Record<string, CliSettingDef[]> = {};
220
232
  for (const def of defs) {
221
233
  if (!groups[def.tab]) {
222
234
  groups[def.tab] = [];
@@ -233,7 +245,7 @@ function handleList(flags: { json?: boolean }): void {
233
245
  for (const group of sortedGroups) {
234
246
  console.log(chalk.bold.blue(`[${group}]`));
235
247
  for (const def of groups[group]) {
236
- const value = settings.get(def.path as SettingPath);
248
+ const value = settings.get(def.path);
237
249
  const valueStr = formatValue(value);
238
250
  const typeStr = getTypeDisplay(def);
239
251
  console.log(` ${chalk.white(def.path)} = ${valueStr} ${chalk.dim(typeStr)}`);
@@ -256,7 +268,7 @@ function handleGet(key: string | undefined, flags: { json?: boolean }): void {
256
268
  process.exit(1);
257
269
  }
258
270
 
259
- const value = settings.get(def.path as SettingPath);
271
+ const value = settings.get(def.path);
260
272
 
261
273
  if (flags.json) {
262
274
  console.log(JSON.stringify({ key: def.path, value, type: def.type, description: def.description }, null, 2));
@@ -281,13 +293,13 @@ async function handleSet(key: string | undefined, value: string | undefined, fla
281
293
  }
282
294
 
283
295
  try {
284
- parseAndSetValue(def.path as SettingPath, value);
296
+ parseAndSetValue(def.path, value);
285
297
  } catch (err) {
286
298
  console.error(chalk.red(String(err)));
287
299
  process.exit(1);
288
300
  }
289
301
 
290
- const newValue = settings.get(def.path as SettingPath);
302
+ const newValue = settings.get(def.path);
291
303
 
292
304
  if (flags.json) {
293
305
  console.log(JSON.stringify({ key: def.path, value: newValue }));
@@ -197,16 +197,6 @@ export const SETTINGS_SCHEMA = {
197
197
  submenu: true,
198
198
  },
199
199
  },
200
- temperature: {
201
- type: "number",
202
- default: -1,
203
- ui: {
204
- tab: "agent",
205
- label: "Temperature",
206
- description: "Sampling temperature (0 = deterministic, 1 = creative, -1 = provider default)",
207
- submenu: true,
208
- },
209
- },
210
200
  hideThinkingBlock: {
211
201
  type: "boolean",
212
202
  default: false,
@@ -544,16 +534,48 @@ export const SETTINGS_SCHEMA = {
544
534
  // ─────────────────────────────────────────────────────────────────────────
545
535
  // Task tool settings
546
536
  // ─────────────────────────────────────────────────────────────────────────
547
- "task.isolation.enabled": {
548
- type: "boolean",
549
- default: false,
537
+ "task.isolation.mode": {
538
+ type: "enum",
539
+ values: ["none", "worktree", "fuse-overlay"] as const,
540
+ default: "none",
550
541
  ui: {
551
542
  tab: "tools",
552
543
  label: "Task isolation",
553
- description: "Run subagents in isolated git worktrees",
544
+ description: "Isolation mode for subagents (none, git worktree, or fuse-overlay)",
545
+ submenu: true,
546
+ },
547
+ },
548
+ "task.isolation.merge": {
549
+ type: "enum",
550
+ values: ["patch", "branch"] as const,
551
+ default: "patch",
552
+ ui: {
553
+ tab: "tools",
554
+ label: "Task isolation merge",
555
+ description: "How isolated task changes are integrated (patch apply or branch merge)",
554
556
  submenu: true,
555
557
  },
556
558
  },
559
+ "task.isolation.commits": {
560
+ type: "enum",
561
+ values: ["generic", "ai"] as const,
562
+ default: "generic",
563
+ ui: {
564
+ tab: "tools",
565
+ label: "Task isolation commits",
566
+ description: "Commit message style for nested repo changes (generic or AI-generated)",
567
+ submenu: true,
568
+ },
569
+ },
570
+ "task.eager": {
571
+ type: "boolean",
572
+ default: false,
573
+ ui: {
574
+ tab: "tools",
575
+ label: "Eager task delegation",
576
+ description: "Encourage the agent to delegate work to subagents unless changes are trivial",
577
+ },
578
+ },
557
579
  "task.maxConcurrency": {
558
580
  type: "number",
559
581
  default: 32,
@@ -1016,6 +1038,66 @@ export const SETTINGS_SCHEMA = {
1016
1038
  "statusLine.leftSegments": { type: "array", default: [] as StatusLineSegmentId[] },
1017
1039
  "statusLine.rightSegments": { type: "array", default: [] as StatusLineSegmentId[] },
1018
1040
  "statusLine.segmentOptions": { type: "record", default: {} as Record<string, unknown> },
1041
+ temperature: {
1042
+ type: "number",
1043
+ default: -1,
1044
+ ui: {
1045
+ tab: "agent",
1046
+ label: "Temperature",
1047
+ description: "Sampling temperature (0 = deterministic, 1 = creative, -1 = provider default)",
1048
+ submenu: true,
1049
+ },
1050
+ },
1051
+ topP: {
1052
+ type: "number",
1053
+ default: -1,
1054
+ ui: {
1055
+ tab: "agent",
1056
+ label: "Top P",
1057
+ description: "Nucleus sampling cutoff (0-1, -1 = provider default)",
1058
+ submenu: true,
1059
+ },
1060
+ },
1061
+ topK: {
1062
+ type: "number",
1063
+ default: -1,
1064
+ ui: {
1065
+ tab: "agent",
1066
+ label: "Top K",
1067
+ description: "Sample from top-K tokens (-1 = provider default)",
1068
+ submenu: true,
1069
+ },
1070
+ },
1071
+ minP: {
1072
+ type: "number",
1073
+ default: -1,
1074
+ ui: {
1075
+ tab: "agent",
1076
+ label: "Min P",
1077
+ description: "Minimum probability threshold (0-1, -1 = provider default)",
1078
+ submenu: true,
1079
+ },
1080
+ },
1081
+ presencePenalty: {
1082
+ type: "number",
1083
+ default: -1,
1084
+ ui: {
1085
+ tab: "agent",
1086
+ label: "Presence penalty",
1087
+ description: "Penalty for introducing already-present tokens (-1 = provider default)",
1088
+ submenu: true,
1089
+ },
1090
+ },
1091
+ repetitionPenalty: {
1092
+ type: "number",
1093
+ default: -1,
1094
+ ui: {
1095
+ tab: "agent",
1096
+ label: "Repetition penalty",
1097
+ description: "Penalty for repeated tokens (-1 = provider default)",
1098
+ submenu: true,
1099
+ },
1100
+ },
1019
1101
  } as const;
1020
1102
 
1021
1103
  // ═══════════════════════════════════════════════════════════════════════════
@@ -553,6 +553,16 @@ export class Settings {
553
553
  }
554
554
  }
555
555
 
556
+ // task.isolation.enabled (boolean) -> task.isolation.mode (enum)
557
+ const taskObj = raw.task as Record<string, unknown> | undefined;
558
+ const isolationObj = taskObj?.isolation as Record<string, unknown> | undefined;
559
+ if (isolationObj && "enabled" in isolationObj) {
560
+ if (typeof isolationObj.enabled === "boolean") {
561
+ isolationObj.mode = isolationObj.enabled ? "worktree" : "none";
562
+ }
563
+ delete isolationObj.enabled;
564
+ }
565
+
556
566
  return raw;
557
567
  }
558
568
 
@@ -5,7 +5,7 @@
5
5
  * Priority: 80 (tool-specific, below builtin but above shared standards)
6
6
  */
7
7
  import * as path from "node:path";
8
- import { tryParseJson } from "@oh-my-pi/pi-utils";
8
+ import { hasFsCode, tryParseJson } from "@oh-my-pi/pi-utils";
9
9
  import { registerProvider } from "../capability";
10
10
  import { type ContextFile, contextFileCapability } from "../capability/context-file";
11
11
  import { type ExtensionModule, extensionModuleCapability } from "../capability/extension-module";
@@ -47,6 +47,10 @@ function getProjectClaude(ctx: LoadContext): string {
47
47
  return path.join(ctx.cwd, CONFIG_DIR);
48
48
  }
49
49
 
50
+ function isMissingDirectoryError(error: unknown): boolean {
51
+ return hasFsCode(error, "ENOENT") || hasFsCode(error, "ENOTDIR");
52
+ }
53
+
50
54
  // =============================================================================
51
55
  // MCP Servers
52
56
  // =============================================================================
@@ -162,15 +166,29 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
162
166
  const userSkillsDir = path.join(getUserClaude(ctx), "skills");
163
167
  const projectSkillsDir = path.join(getProjectClaude(ctx), "skills");
164
168
 
165
- const results = await Promise.all([
169
+ const [userResult, projectResult] = await Promise.allSettled([
166
170
  scanSkillsFromDir(ctx, { dir: userSkillsDir, providerId: PROVIDER_ID, level: "user" }),
167
171
  scanSkillsFromDir(ctx, { dir: projectSkillsDir, providerId: PROVIDER_ID, level: "project" }),
168
172
  ]);
169
173
 
170
- return {
171
- items: results.flatMap(r => r.items),
172
- warnings: results.flatMap(r => r.warnings ?? []),
173
- };
174
+ const items: Skill[] = [];
175
+ const warnings: string[] = [];
176
+
177
+ if (userResult.status === "fulfilled") {
178
+ items.push(...userResult.value.items);
179
+ warnings.push(...(userResult.value.warnings ?? []));
180
+ } else if (!isMissingDirectoryError(userResult.reason)) {
181
+ warnings.push(`Failed to scan Claude user skills in ${userSkillsDir}: ${String(userResult.reason)}`);
182
+ }
183
+
184
+ if (projectResult.status === "fulfilled") {
185
+ items.push(...projectResult.value.items);
186
+ warnings.push(...(projectResult.value.warnings ?? []));
187
+ } else if (!isMissingDirectoryError(projectResult.reason)) {
188
+ warnings.push(`Failed to scan Claude project skills in ${projectSkillsDir}: ${String(projectResult.reason)}`);
189
+ }
190
+
191
+ return { items, warnings };
174
192
  }
175
193
 
176
194
  // =============================================================================
@@ -284,8 +284,15 @@ export async function scanSkillsFromDir(
284
284
  const warnings: string[] = [];
285
285
  const { dir, level, providerId, requireDescription = false } = options;
286
286
 
287
- const entries = await fs.promises.readdir(dir, { withFileTypes: true });
288
-
287
+ let entries: fs.Dirent[];
288
+ try {
289
+ entries = await fs.promises.readdir(dir, { withFileTypes: true });
290
+ } catch (error) {
291
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
292
+ warnings.push(`Failed to read skills directory: ${dir} (${String(error)})`);
293
+ }
294
+ return { items, warnings };
295
+ }
289
296
  const loadSkill = async (skillPath: string) => {
290
297
  try {
291
298
  const content = await readFile(skillPath);
@@ -72,6 +72,7 @@ const DEFAULT_ENV_DENYLIST = new Set([
72
72
  "GEMINI_API_KEY",
73
73
  "OPENROUTER_API_KEY",
74
74
  "PERPLEXITY_API_KEY",
75
+ "PERPLEXITY_COOKIES",
75
76
  "EXA_API_KEY",
76
77
  "AZURE_OPENAI_API_KEY",
77
78
  "MISTRAL_API_KEY",
package/src/mcp/config.ts CHANGED
@@ -110,7 +110,7 @@ export async function loadAllMCPConfigs(cwd: string, options?: LoadMCPConfigsOpt
110
110
  let sources: Record<string, SourceMeta> = {};
111
111
  for (const server of servers) {
112
112
  const config = convertToLegacyConfig(server);
113
- if (config.enabled === false || (server._source.level !== "user" && disabledServers.has(server.name))) {
113
+ if (config.enabled === false || disabledServers.has(server.name)) {
114
114
  continue;
115
115
  }
116
116
  configs[server.name] = config;
@@ -93,6 +93,22 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
93
93
  { value: "2", label: "Double" },
94
94
  { value: "3", label: "Triple" },
95
95
  ],
96
+ // Task isolation mode
97
+ "task.isolation.mode": [
98
+ { value: "none", label: "None", description: "No isolation" },
99
+ { value: "worktree", label: "Worktree", description: "Git worktree isolation" },
100
+ { value: "fuse-overlay", label: "Fuse Overlay", description: "COW overlay via fuse-overlayfs" },
101
+ ],
102
+ // Task isolation merge strategy
103
+ "task.isolation.merge": [
104
+ { value: "patch", label: "Patch", description: "Combine diffs and git apply" },
105
+ { value: "branch", label: "Branch", description: "Commit per task, merge with --no-ff" },
106
+ ],
107
+ // Task isolation commit messages
108
+ "task.isolation.commits": [
109
+ { value: "generic", label: "Generic", description: "Static commit message" },
110
+ { value: "ai", label: "AI", description: "AI-generated commit message from diff" },
111
+ ],
96
112
  // Todo max reminders
97
113
  "todo.reminders.max": [
98
114
  { value: "1", label: "1 reminder" },
@@ -166,7 +182,7 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
166
182
  { value: "brave", label: "Brave", description: "Requires BRAVE_API_KEY" },
167
183
  { value: "jina", label: "Jina", description: "Requires JINA_API_KEY" },
168
184
  { value: "kimi", label: "Kimi", description: "Requires MOONSHOT_SEARCH_API_KEY or MOONSHOT_API_KEY" },
169
- { value: "perplexity", label: "Perplexity", description: "Requires PERPLEXITY_API_KEY" },
185
+ { value: "perplexity", label: "Perplexity", description: "Requires PERPLEXITY_COOKIES or PERPLEXITY_API_KEY" },
170
186
  { value: "anthropic", label: "Anthropic", description: "Uses Anthropic web search" },
171
187
  { value: "zai", label: "Z.AI", description: "Calls Z.AI webSearchPrime MCP" },
172
188
  { value: "synthetic", label: "Synthetic", description: "Requires SYNTHETIC_API_KEY" },
@@ -198,6 +214,42 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
198
214
  { value: "0.7", label: "0.7", description: "Creative" },
199
215
  { value: "1", label: "1", description: "Maximum variety" },
200
216
  ],
217
+ topP: [
218
+ { value: "-1", label: "Default", description: "Use provider default" },
219
+ { value: "0.1", label: "0.1", description: "Very focused" },
220
+ { value: "0.3", label: "0.3", description: "Focused" },
221
+ { value: "0.5", label: "0.5", description: "Balanced" },
222
+ { value: "0.9", label: "0.9", description: "Broad" },
223
+ { value: "1", label: "1", description: "No nucleus filtering" },
224
+ ],
225
+ topK: [
226
+ { value: "-1", label: "Default", description: "Use provider default" },
227
+ { value: "1", label: "1", description: "Greedy top token" },
228
+ { value: "20", label: "20", description: "Focused" },
229
+ { value: "40", label: "40", description: "Balanced" },
230
+ { value: "100", label: "100", description: "Broad" },
231
+ ],
232
+ minP: [
233
+ { value: "-1", label: "Default", description: "Use provider default" },
234
+ { value: "0.01", label: "0.01", description: "Very permissive" },
235
+ { value: "0.05", label: "0.05", description: "Balanced" },
236
+ { value: "0.1", label: "0.1", description: "Strict" },
237
+ ],
238
+ presencePenalty: [
239
+ { value: "-1", label: "Default", description: "Use provider default" },
240
+ { value: "0", label: "0", description: "No penalty" },
241
+ { value: "0.5", label: "0.5", description: "Mild novelty" },
242
+ { value: "1", label: "1", description: "Encourage novelty" },
243
+ { value: "2", label: "2", description: "Strong novelty" },
244
+ ],
245
+ repetitionPenalty: [
246
+ { value: "-1", label: "Default", description: "Use provider default" },
247
+ { value: "0.8", label: "0.8", description: "Allow repetition" },
248
+ { value: "1", label: "1", description: "No penalty" },
249
+ { value: "1.1", label: "1.1", description: "Mild penalty" },
250
+ { value: "1.2", label: "1.2", description: "Balanced" },
251
+ { value: "1.5", label: "1.5", description: "Strong penalty" },
252
+ ],
201
253
  // Symbol preset
202
254
  symbolPreset: [
203
255
  { value: "unicode", label: "Unicode", description: "Standard symbols (default)" },
@@ -50,6 +50,7 @@ export class StatusLineComponent implements Component {
50
50
  // Git status caching (1s TTL)
51
51
  #cachedGitStatus: { staged: number; unstaged: number; untracked: number } | null = null;
52
52
  #gitStatusLastFetch = 0;
53
+ #gitStatusInFlight = false;
53
54
 
54
55
  constructor(private readonly session: AgentSession) {
55
56
  this.#settings = {
@@ -153,11 +154,12 @@ export class StatusLineComponent implements Component {
153
154
  }
154
155
 
155
156
  #getGitStatus(): { staged: number; unstaged: number; untracked: number } | null {
156
- const now = Date.now();
157
- if (now - this.#gitStatusLastFetch < 1000) {
157
+ if (this.#gitStatusInFlight || Date.now() - this.#gitStatusLastFetch < 1000) {
158
158
  return this.#cachedGitStatus;
159
159
  }
160
160
 
161
+ this.#gitStatusInFlight = true;
162
+
161
163
  // Fire async fetch, return cached value
162
164
  (async () => {
163
165
  try {
@@ -165,7 +167,6 @@ export class StatusLineComponent implements Component {
165
167
 
166
168
  if (result.exitCode !== 0) {
167
169
  this.#cachedGitStatus = null;
168
- this.#gitStatusLastFetch = now;
169
170
  return;
170
171
  }
171
172
 
@@ -195,10 +196,11 @@ export class StatusLineComponent implements Component {
195
196
  }
196
197
 
197
198
  this.#cachedGitStatus = { staged, unstaged, untracked };
198
- this.#gitStatusLastFetch = now;
199
199
  } catch {
200
200
  this.#cachedGitStatus = null;
201
- this.#gitStatusLastFetch = now;
201
+ } finally {
202
+ this.#gitStatusLastFetch = Date.now();
203
+ this.#gitStatusInFlight = false;
202
204
  }
203
205
  })();
204
206
 
@@ -739,10 +739,12 @@ export class MCPCommandController {
739
739
 
740
740
  // Collect runtime-discovered servers not in config files
741
741
  const configServerNames = new Set([...userServers, ...projectServers]);
742
+ const disabledServerNames = new Set(await readDisabledServers(userPath));
742
743
  const discoveredServers: { name: string; source: SourceMeta }[] = [];
743
744
  if (this.ctx.mcpManager) {
744
745
  for (const name of this.ctx.mcpManager.getAllServerNames()) {
745
746
  if (configServerNames.has(name)) continue;
747
+ if (disabledServerNames.has(name)) continue;
746
748
  const source = this.ctx.mcpManager.getSource(name);
747
749
  if (source) {
748
750
  discoveredServers.push({ name, source });
@@ -754,7 +756,7 @@ export class MCPCommandController {
754
756
  userServers.length === 0 &&
755
757
  projectServers.length === 0 &&
756
758
  discoveredServers.length === 0 &&
757
- (userConfig.disabledServers ?? []).length === 0
759
+ disabledServerNames.size === 0
758
760
  ) {
759
761
  this.#showMessage(
760
762
  [
@@ -851,8 +853,7 @@ export class MCPCommandController {
851
853
  }
852
854
 
853
855
  // Show servers disabled via /mcp disable (from third-party configs)
854
- const disabledServers = await readDisabledServers(userPath);
855
- const relevantDisabled = disabledServers.filter(n => !configServerNames.has(n));
856
+ const relevantDisabled = [...disabledServerNames].filter(n => !configServerNames.has(n));
856
857
  if (relevantDisabled.length > 0) {
857
858
  lines.push(theme.fg("accent", "Disabled") + theme.fg("muted", " (discovered servers):"));
858
859
  for (const name of relevantDisabled) {