@kolisachint/hoocode-agent 0.4.149 → 0.4.151
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 +29 -0
- package/dist/core/agent-session-services.d.ts +1 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +22 -5
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +6 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/context-gc.d.ts +5 -0
- package/dist/core/context-gc.d.ts.map +1 -1
- package/dist/core/context-gc.js +19 -18
- package/dist/core/context-gc.js.map +1 -1
- package/dist/core/settings-defaults.d.ts +2 -0
- package/dist/core/settings-defaults.d.ts.map +1 -1
- package/dist/core/settings-defaults.js +3 -1
- package/dist/core/settings-defaults.js.map +1 -1
- package/dist/core/settings-manager.d.ts +13 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +62 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/settings-types.d.ts +3 -0
- package/dist/core/settings-types.d.ts.map +1 -1
- package/dist/core/settings-types.js.map +1 -1
- package/dist/core/tools/read-dedup.d.ts +78 -0
- package/dist/core/tools/read-dedup.d.ts.map +1 -0
- package/dist/core/tools/read-dedup.js +219 -0
- package/dist/core/tools/read-dedup.js.map +1 -0
- package/dist/core/tools/read.d.ts +7 -0
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +18 -2
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/truncate.d.ts +3 -3
- package/dist/core/tools/truncate.d.ts.map +1 -1
- package/dist/core/tools/truncate.js +2 -2
- package/dist/core/tools/truncate.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +8 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +46 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +304 -3
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +13 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +24 -4
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +125 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
|
@@ -24,6 +24,11 @@
|
|
|
24
24
|
* Deliberately conservative: paths are matched after `path.resolve`, so two
|
|
25
25
|
* different files never collide, and any ambiguity results in NOT evicting.
|
|
26
26
|
*
|
|
27
|
+
* This post-hoc pass is complemented by an at-call-time guard in the `read`
|
|
28
|
+
* tool (see `tools/read-dedup.ts`), which short-circuits a redundant re-read
|
|
29
|
+
* before it fetches. Both share the range math in that module, and this pass
|
|
30
|
+
* skips the guard's pointer results so they never supersede the read they name.
|
|
31
|
+
*
|
|
27
32
|
* Bash-output eviction is additionally gated on token-budget pressure
|
|
28
33
|
* (`options.budgetPressure`, the fraction of the model's context window in
|
|
29
34
|
* use). At 0 pressure — the default — behaviour is identical to read-only GC.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-gc.d.ts","sourceRoot":"","sources":["../../src/core/context-gc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAoBpE,MAAM,WAAW,gBAAgB;IAChC,sEAAsE;IACtE,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAoDD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,gBAAgB,GAAG,YAAY,EAAE,CAyGxG","sourcesContent":["/**\n * Context garbage collection.\n *\n * The whole transcript is re-sent on every turn, so a tool result that has\n * become useless keeps costing tokens for the rest of the session. This pass\n * runs on the OUTGOING message copy (via the agent's `transformContext` hook),\n * never on the persisted session, and replaces provably-dead `read` results\n * with a short stub. Nothing is lost: the persisted history is untouched and\n * the file is re-readable on demand.\n *\n * Current rule — superseded reads only (the read-then-edit / re-read pattern,\n * which dominates coding sessions):\n *\n * A `read` result for a path P is stale once, later in the transcript, the\n * same path is edited/written (its on-disk content changed) or read again\n * over an overlapping line range (the newer read reflects that region's newer\n * state). Reads of disjoint regions of the same file coexist — a later read of\n * lines 200-260 does not evict an earlier read of lines 1-40 — so paginating\n * through a large file never makes the model re-fetch a region it already has.\n * A read is only evicted when a *successful* later event supersedes it — so a\n * read whose edit failed (and which the model still needs to retry) is never\n * touched.\n *\n * Deliberately conservative: paths are matched after `path.resolve`, so two\n * different files never collide, and any ambiguity results in NOT evicting.\n *\n * Bash-output eviction is additionally gated on token-budget pressure\n * (`options.budgetPressure`, the fraction of the model's context window in\n * use). At 0 pressure — the default — behaviour is identical to read-only GC.\n * Bash output is not always recoverable, so it carries its own safeguards: a\n * command that looks side-effecting is never elided (re-running it, as the\n * stub invites, could repeat a destructive action), and below 80% pressure\n * only large outputs are elided.\n */\n\nimport { resolve } from \"node:path\";\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\n\n/** Tool names whose result injects file contents into the transcript. */\nconst READ_TOOLS = new Set([\"read\"]);\n/** Tool names that change a file, making a prior read of that path stale. */\nconst MUTATE_TOOLS = new Set([\"edit\", \"write\"]);\n\n/**\n * Commands whose bash output must never be elided, because the stub invites\n * the model to re-run the command and re-running could repeat a destructive or\n * state-changing action. Tested against the whole command string, and\n * deliberately over-broad: a false positive merely keeps an output (safe),\n * while the common verbose *read* commands (cat/grep/ls/find/git log/diff,\n * test runners) intentionally do NOT match, so they stay evictable.\n */\nconst BASH_SIDE_EFFECT_PATTERN =\n\t/(\\b(write|insert|delete|update|curl|wget|psql|mysql|sqlite3|migrate|drop|truncate|rm|rmdir|mv|cp|dd|kill|tee|chmod|chown|ln|mkdir|touch)\\b|\\bgit\\s+(commit|push|reset|checkout|rebase|clean|apply|merge)\\b|\\bsed\\b[^|]*-i|>>?)/i;\n/** Below 80% pressure, only bash outputs larger than this (chars) are elided. */\nconst BASH_EVICTION_CHAR_THRESHOLD = 2000;\n\nexport interface ContextGcOptions {\n\t/** Working directory used to resolve relative tool path arguments. */\n\tcwd: string;\n\t/**\n\t * Token-budget pressure in [0, 1] — the fraction of the model's context\n\t * window currently in use. Absent or 0 reproduces read-only GC behaviour.\n\t * Bash-output eviction begins at 0.6 and becomes unconditional at 0.8.\n\t */\n\tbudgetPressure?: number;\n}\n\ninterface AssistantLike {\n\trole: \"assistant\";\n\tcontent: Array<{ type: string; id?: string; name?: string; arguments?: Record<string, unknown> }>;\n}\n\ninterface ToolResultLike {\n\trole: \"toolResult\";\n\ttoolCallId: string;\n\ttoolName: string;\n\tcontent: Array<{ type: string; text?: string }>;\n\tisError: boolean;\n}\n\nfunction isAssistant(m: AgentMessage): m is AgentMessage & AssistantLike {\n\treturn (m as { role?: string }).role === \"assistant\" && Array.isArray((m as AssistantLike).content);\n}\n\nfunction isToolResult(m: AgentMessage): m is AgentMessage & ToolResultLike {\n\treturn (m as { role?: string }).role === \"toolResult\";\n}\n\n/** Half-open line interval [start, end) a read call covers; end is exclusive. */\ninterface ReadRange {\n\tstart: number;\n\tend: number;\n}\n\n/**\n * Derive the line range a read call covers from its `offset`/`limit` args.\n * Missing offset means \"from line 1\"; missing limit means \"to end of file\"\n * (open-ended, so it overlaps any later read of the same file).\n */\nfunction readRangeFromArgs(args: Record<string, unknown> | undefined): ReadRange {\n\tconst offsetRaw = args?.offset;\n\tconst limitRaw = args?.limit;\n\tconst offset = typeof offsetRaw === \"number\" && Number.isFinite(offsetRaw) && offsetRaw > 0 ? offsetRaw : 1;\n\tconst end =\n\t\ttypeof limitRaw === \"number\" && Number.isFinite(limitRaw)\n\t\t\t? offset + Math.max(0, limitRaw)\n\t\t\t: Number.POSITIVE_INFINITY;\n\treturn { start: offset, end };\n}\n\n/** Whether two half-open line ranges intersect. */\nfunction rangesOverlap(a: ReadRange, b: ReadRange): boolean {\n\treturn a.start < b.end && b.start < a.end;\n}\n\nconst WHOLE_FILE_RANGE: ReadRange = { start: 1, end: Number.POSITIVE_INFINITY };\n\n/**\n * Return a message array with superseded `read` results stubbed out. Returns the\n * original array reference unchanged when there is nothing to evict, so a\n * no-op turn does not needlessly perturb the outgoing context.\n */\nexport function evictSupersededReads(messages: AgentMessage[], options: ContextGcOptions): AgentMessage[] {\n\t// Map each tool call id -> the resolved path it operated on, plus a friendly\n\t// display path (the original argument) for the stub text.\n\tconst resolvedPathByCallId = new Map<string, string>();\n\tconst displayPathByResolved = new Map<string, string>();\n\t// Bash calls carry a `command`, not a `path`; capture it so the eviction\n\t// pass can consult the side-effect guard by tool call id.\n\tconst bashCommandByCallId = new Map<string, string>();\n\t// Read calls carry `offset`/`limit`; capture the line range each covers so a\n\t// later read only supersedes an earlier one when their ranges overlap.\n\tconst rangeByCallId = new Map<string, ReadRange>();\n\tfor (const m of messages) {\n\t\tif (!isAssistant(m)) continue;\n\t\tfor (const block of m.content) {\n\t\t\tif (block.type !== \"toolCall\" || !block.id) continue;\n\t\t\tif (block.name === \"bash\") {\n\t\t\t\tconst cmd = block.arguments?.command;\n\t\t\t\tif (typeof cmd === \"string\" && cmd.length > 0) bashCommandByCallId.set(block.id, cmd);\n\t\t\t}\n\t\t\tconst rawPath = block.arguments?.path;\n\t\t\tif (typeof rawPath !== \"string\" || rawPath.length === 0) continue;\n\t\t\tconst resolved = resolve(options.cwd, rawPath);\n\t\t\tresolvedPathByCallId.set(block.id, resolved);\n\t\t\tif (block.name && READ_TOOLS.has(block.name)) rangeByCallId.set(block.id, readRangeFromArgs(block.arguments));\n\t\t\tif (!displayPathByResolved.has(resolved)) displayPathByResolved.set(resolved, rawPath);\n\t\t}\n\t}\n\n\t// First pass: per path, collect every successful read (index + line range)\n\t// and the last successful mutate. A read at index i is superseded when a later\n\t// successful mutate exists (index > i, whole file changed) or a later read of\n\t// an overlapping range exists (index > i) for the same path.\n\tconst readsByPath = new Map<string, Array<{ index: number; range: ReadRange }>>();\n\tconst lastMutateIndex = new Map<string, number>();\n\tmessages.forEach((m, i) => {\n\t\tif (!isToolResult(m) || m.isError) return;\n\t\tconst path = resolvedPathByCallId.get(m.toolCallId);\n\t\tif (!path) return;\n\t\tif (READ_TOOLS.has(m.toolName)) {\n\t\t\tconst range = rangeByCallId.get(m.toolCallId) ?? WHOLE_FILE_RANGE;\n\t\t\tconst list = readsByPath.get(path);\n\t\t\tif (list) list.push({ index: i, range });\n\t\t\telse readsByPath.set(path, [{ index: i, range }]);\n\t\t} else if (MUTATE_TOOLS.has(m.toolName)) {\n\t\t\tlastMutateIndex.set(path, i);\n\t\t}\n\t});\n\n\t// Second pass: build the output, stubbing evicted reads. Keep every other\n\t// message by reference; clone only the ones we rewrite so the persisted\n\t// history (which may share these objects) is never mutated.\n\tconst pressure = options.budgetPressure ?? 0;\n\tlet changed = false;\n\tconst out = messages.map((m, i) => {\n\t\tif (!isToolResult(m) || m.isError) return m;\n\n\t\t// Superseded-read eviction — always on, pressure-independent.\n\t\tif (READ_TOOLS.has(m.toolName)) {\n\t\t\tconst path = resolvedPathByCallId.get(m.toolCallId);\n\t\t\tif (!path) return m;\n\t\t\tconst range = rangeByCallId.get(m.toolCallId) ?? WHOLE_FILE_RANGE;\n\t\t\tconst laterMutate = (lastMutateIndex.get(path) ?? -1) > i;\n\t\t\tconst laterOverlappingRead = (readsByPath.get(path) ?? []).some(\n\t\t\t\t(r) => r.index > i && rangesOverlap(r.range, range),\n\t\t\t);\n\t\t\tif (!laterOverlappingRead && !laterMutate) return m;\n\t\t\tchanged = true;\n\t\t\tconst display = displayPathByResolved.get(path) ?? path;\n\t\t\tconst reason = laterMutate ? \"the file was modified after this read\" : \"the file was read again later\";\n\t\t\treturn {\n\t\t\t\t...m,\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\ttext: `[Superseded read of ${display} elided to save context — ${reason}. Re-read the file if you need its current contents.]`,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t};\n\t\t}\n\n\t\t// Bash-output eviction — only under token-budget pressure (>= 0.6).\n\t\tif (pressure >= 0.6 && m.toolName === \"bash\") {\n\t\t\tconst cmd = bashCommandByCallId.get(m.toolCallId) ?? \"\";\n\t\t\tif (BASH_SIDE_EFFECT_PATTERN.test(cmd)) return m;\n\t\t\tconst textLen = m.content.filter((c) => c.type === \"text\").reduce((sum, c) => sum + (c.text?.length ?? 0), 0);\n\t\t\t// 60–79%: elide only large outputs. 80%+: elide unconditionally.\n\t\t\tconst shouldEvict = pressure >= 0.8 || textLen > BASH_EVICTION_CHAR_THRESHOLD;\n\t\t\tif (shouldEvict) {\n\t\t\t\tchanged = true;\n\t\t\t\treturn {\n\t\t\t\t\t...m,\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `[Bash output elided at ${Math.round(pressure * 100)}% token budget — re-run if needed.]`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\treturn m;\n\t});\n\n\treturn changed ? out : messages;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"context-gc.d.ts","sourceRoot":"","sources":["../../src/core/context-gc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AA2BpE,MAAM,WAAW,gBAAgB;IAChC,sEAAsE;IACtE,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AA+BD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,gBAAgB,GAAG,YAAY,EAAE,CA8GxG","sourcesContent":["/**\n * Context garbage collection.\n *\n * The whole transcript is re-sent on every turn, so a tool result that has\n * become useless keeps costing tokens for the rest of the session. This pass\n * runs on the OUTGOING message copy (via the agent's `transformContext` hook),\n * never on the persisted session, and replaces provably-dead `read` results\n * with a short stub. Nothing is lost: the persisted history is untouched and\n * the file is re-readable on demand.\n *\n * Current rule — superseded reads only (the read-then-edit / re-read pattern,\n * which dominates coding sessions):\n *\n * A `read` result for a path P is stale once, later in the transcript, the\n * same path is edited/written (its on-disk content changed) or read again\n * over an overlapping line range (the newer read reflects that region's newer\n * state). Reads of disjoint regions of the same file coexist — a later read of\n * lines 200-260 does not evict an earlier read of lines 1-40 — so paginating\n * through a large file never makes the model re-fetch a region it already has.\n * A read is only evicted when a *successful* later event supersedes it — so a\n * read whose edit failed (and which the model still needs to retry) is never\n * touched.\n *\n * Deliberately conservative: paths are matched after `path.resolve`, so two\n * different files never collide, and any ambiguity results in NOT evicting.\n *\n * This post-hoc pass is complemented by an at-call-time guard in the `read`\n * tool (see `tools/read-dedup.ts`), which short-circuits a redundant re-read\n * before it fetches. Both share the range math in that module, and this pass\n * skips the guard's pointer results so they never supersede the read they name.\n *\n * Bash-output eviction is additionally gated on token-budget pressure\n * (`options.budgetPressure`, the fraction of the model's context window in\n * use). At 0 pressure — the default — behaviour is identical to read-only GC.\n * Bash output is not always recoverable, so it carries its own safeguards: a\n * command that looks side-effecting is never elided (re-running it, as the\n * stub invites, could repeat a destructive action), and below 80% pressure\n * only large outputs are elided.\n */\n\nimport { resolve } from \"node:path\";\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport {\n\tisDedupPointerText,\n\ttype ReadRange,\n\trangesOverlap,\n\treadRangeFromArgs,\n\tWHOLE_FILE_RANGE,\n} from \"./tools/read-dedup.js\";\n\n/** Tool names whose result injects file contents into the transcript. */\nconst READ_TOOLS = new Set([\"read\"]);\n/** Tool names that change a file, making a prior read of that path stale. */\nconst MUTATE_TOOLS = new Set([\"edit\", \"write\"]);\n\n/**\n * Commands whose bash output must never be elided, because the stub invites\n * the model to re-run the command and re-running could repeat a destructive or\n * state-changing action. Tested against the whole command string, and\n * deliberately over-broad: a false positive merely keeps an output (safe),\n * while the common verbose *read* commands (cat/grep/ls/find/git log/diff,\n * test runners) intentionally do NOT match, so they stay evictable.\n */\nconst BASH_SIDE_EFFECT_PATTERN =\n\t/(\\b(write|insert|delete|update|curl|wget|psql|mysql|sqlite3|migrate|drop|truncate|rm|rmdir|mv|cp|dd|kill|tee|chmod|chown|ln|mkdir|touch)\\b|\\bgit\\s+(commit|push|reset|checkout|rebase|clean|apply|merge)\\b|\\bsed\\b[^|]*-i|>>?)/i;\n/** Below 80% pressure, only bash outputs larger than this (chars) are elided. */\nconst BASH_EVICTION_CHAR_THRESHOLD = 2000;\n\nexport interface ContextGcOptions {\n\t/** Working directory used to resolve relative tool path arguments. */\n\tcwd: string;\n\t/**\n\t * Token-budget pressure in [0, 1] — the fraction of the model's context\n\t * window currently in use. Absent or 0 reproduces read-only GC behaviour.\n\t * Bash-output eviction begins at 0.6 and becomes unconditional at 0.8.\n\t */\n\tbudgetPressure?: number;\n}\n\ninterface AssistantLike {\n\trole: \"assistant\";\n\tcontent: Array<{ type: string; id?: string; name?: string; arguments?: Record<string, unknown> }>;\n}\n\ninterface ToolResultLike {\n\trole: \"toolResult\";\n\ttoolCallId: string;\n\ttoolName: string;\n\tcontent: Array<{ type: string; text?: string }>;\n\tisError: boolean;\n}\n\nfunction isAssistant(m: AgentMessage): m is AgentMessage & AssistantLike {\n\treturn (m as { role?: string }).role === \"assistant\" && Array.isArray((m as AssistantLike).content);\n}\n\nfunction isToolResult(m: AgentMessage): m is AgentMessage & ToolResultLike {\n\treturn (m as { role?: string }).role === \"toolResult\";\n}\n\n/** Concatenated text of a tool result's text blocks. */\nfunction toolResultText(m: ToolResultLike): string {\n\treturn m.content\n\t\t.filter((c) => c.type === \"text\")\n\t\t.map((c) => c.text ?? \"\")\n\t\t.join(\"\");\n}\n\n/**\n * Return a message array with superseded `read` results stubbed out. Returns the\n * original array reference unchanged when there is nothing to evict, so a\n * no-op turn does not needlessly perturb the outgoing context.\n */\nexport function evictSupersededReads(messages: AgentMessage[], options: ContextGcOptions): AgentMessage[] {\n\t// Map each tool call id -> the resolved path it operated on, plus a friendly\n\t// display path (the original argument) for the stub text.\n\tconst resolvedPathByCallId = new Map<string, string>();\n\tconst displayPathByResolved = new Map<string, string>();\n\t// Bash calls carry a `command`, not a `path`; capture it so the eviction\n\t// pass can consult the side-effect guard by tool call id.\n\tconst bashCommandByCallId = new Map<string, string>();\n\t// Read calls carry `offset`/`limit`; capture the line range each covers so a\n\t// later read only supersedes an earlier one when their ranges overlap.\n\tconst rangeByCallId = new Map<string, ReadRange>();\n\tfor (const m of messages) {\n\t\tif (!isAssistant(m)) continue;\n\t\tfor (const block of m.content) {\n\t\t\tif (block.type !== \"toolCall\" || !block.id) continue;\n\t\t\tif (block.name === \"bash\") {\n\t\t\t\tconst cmd = block.arguments?.command;\n\t\t\t\tif (typeof cmd === \"string\" && cmd.length > 0) bashCommandByCallId.set(block.id, cmd);\n\t\t\t}\n\t\t\tconst rawPath = block.arguments?.path;\n\t\t\tif (typeof rawPath !== \"string\" || rawPath.length === 0) continue;\n\t\t\tconst resolved = resolve(options.cwd, rawPath);\n\t\t\tresolvedPathByCallId.set(block.id, resolved);\n\t\t\tif (block.name && READ_TOOLS.has(block.name)) rangeByCallId.set(block.id, readRangeFromArgs(block.arguments));\n\t\t\tif (!displayPathByResolved.has(resolved)) displayPathByResolved.set(resolved, rawPath);\n\t\t}\n\t}\n\n\t// First pass: per path, collect every successful read (index + line range)\n\t// and the last successful mutate. A read at index i is superseded when a later\n\t// successful mutate exists (index > i, whole file changed) or a later read of\n\t// an overlapping range exists (index > i) for the same path.\n\tconst readsByPath = new Map<string, Array<{ index: number; range: ReadRange }>>();\n\tconst lastMutateIndex = new Map<string, number>();\n\tmessages.forEach((m, i) => {\n\t\tif (!isToolResult(m) || m.isError) return;\n\t\tconst path = resolvedPathByCallId.get(m.toolCallId);\n\t\tif (!path) return;\n\t\tif (READ_TOOLS.has(m.toolName)) {\n\t\t\t// A dedup pointer fetched no content, so it must not supersede (stub) the\n\t\t\t// earlier read it points at — leave it out of the supersession bookkeeping.\n\t\t\tif (isDedupPointerText(toolResultText(m))) return;\n\t\t\tconst range = rangeByCallId.get(m.toolCallId) ?? WHOLE_FILE_RANGE;\n\t\t\tconst list = readsByPath.get(path);\n\t\t\tif (list) list.push({ index: i, range });\n\t\t\telse readsByPath.set(path, [{ index: i, range }]);\n\t\t} else if (MUTATE_TOOLS.has(m.toolName)) {\n\t\t\tlastMutateIndex.set(path, i);\n\t\t}\n\t});\n\n\t// Second pass: build the output, stubbing evicted reads. Keep every other\n\t// message by reference; clone only the ones we rewrite so the persisted\n\t// history (which may share these objects) is never mutated.\n\tconst pressure = options.budgetPressure ?? 0;\n\tlet changed = false;\n\tconst out = messages.map((m, i) => {\n\t\tif (!isToolResult(m) || m.isError) return m;\n\n\t\t// Superseded-read eviction — always on, pressure-independent.\n\t\tif (READ_TOOLS.has(m.toolName)) {\n\t\t\tconst path = resolvedPathByCallId.get(m.toolCallId);\n\t\t\tif (!path) return m;\n\t\t\t// Leave at-call dedup pointers as-is; they are already minimal.\n\t\t\tif (isDedupPointerText(toolResultText(m))) return m;\n\t\t\tconst range = rangeByCallId.get(m.toolCallId) ?? WHOLE_FILE_RANGE;\n\t\t\tconst laterMutate = (lastMutateIndex.get(path) ?? -1) > i;\n\t\t\tconst laterOverlappingRead = (readsByPath.get(path) ?? []).some(\n\t\t\t\t(r) => r.index > i && rangesOverlap(r.range, range),\n\t\t\t);\n\t\t\tif (!laterOverlappingRead && !laterMutate) return m;\n\t\t\tchanged = true;\n\t\t\tconst display = displayPathByResolved.get(path) ?? path;\n\t\t\tconst reason = laterMutate ? \"the file was modified after this read\" : \"the file was read again later\";\n\t\t\treturn {\n\t\t\t\t...m,\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\ttext: `[Superseded read of ${display} elided to save context — ${reason}. Re-read the file if you need its current contents.]`,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t};\n\t\t}\n\n\t\t// Bash-output eviction — only under token-budget pressure (>= 0.6).\n\t\tif (pressure >= 0.6 && m.toolName === \"bash\") {\n\t\t\tconst cmd = bashCommandByCallId.get(m.toolCallId) ?? \"\";\n\t\t\tif (BASH_SIDE_EFFECT_PATTERN.test(cmd)) return m;\n\t\t\tconst textLen = m.content.filter((c) => c.type === \"text\").reduce((sum, c) => sum + (c.text?.length ?? 0), 0);\n\t\t\t// 60–79%: elide only large outputs. 80%+: elide unconditionally.\n\t\t\tconst shouldEvict = pressure >= 0.8 || textLen > BASH_EVICTION_CHAR_THRESHOLD;\n\t\t\tif (shouldEvict) {\n\t\t\t\tchanged = true;\n\t\t\t\treturn {\n\t\t\t\t\t...m,\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `[Bash output elided at ${Math.round(pressure * 100)}% token budget — re-run if needed.]`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\treturn m;\n\t});\n\n\treturn changed ? out : messages;\n}\n"]}
|
package/dist/core/context-gc.js
CHANGED
|
@@ -24,6 +24,11 @@
|
|
|
24
24
|
* Deliberately conservative: paths are matched after `path.resolve`, so two
|
|
25
25
|
* different files never collide, and any ambiguity results in NOT evicting.
|
|
26
26
|
*
|
|
27
|
+
* This post-hoc pass is complemented by an at-call-time guard in the `read`
|
|
28
|
+
* tool (see `tools/read-dedup.ts`), which short-circuits a redundant re-read
|
|
29
|
+
* before it fetches. Both share the range math in that module, and this pass
|
|
30
|
+
* skips the guard's pointer results so they never supersede the read they name.
|
|
31
|
+
*
|
|
27
32
|
* Bash-output eviction is additionally gated on token-budget pressure
|
|
28
33
|
* (`options.budgetPressure`, the fraction of the model's context window in
|
|
29
34
|
* use). At 0 pressure — the default — behaviour is identical to read-only GC.
|
|
@@ -33,6 +38,7 @@
|
|
|
33
38
|
* only large outputs are elided.
|
|
34
39
|
*/
|
|
35
40
|
import { resolve } from "node:path";
|
|
41
|
+
import { isDedupPointerText, rangesOverlap, readRangeFromArgs, WHOLE_FILE_RANGE, } from "./tools/read-dedup.js";
|
|
36
42
|
/** Tool names whose result injects file contents into the transcript. */
|
|
37
43
|
const READ_TOOLS = new Set(["read"]);
|
|
38
44
|
/** Tool names that change a file, making a prior read of that path stale. */
|
|
@@ -54,25 +60,13 @@ function isAssistant(m) {
|
|
|
54
60
|
function isToolResult(m) {
|
|
55
61
|
return m.role === "toolResult";
|
|
56
62
|
}
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const offsetRaw = args?.offset;
|
|
64
|
-
const limitRaw = args?.limit;
|
|
65
|
-
const offset = typeof offsetRaw === "number" && Number.isFinite(offsetRaw) && offsetRaw > 0 ? offsetRaw : 1;
|
|
66
|
-
const end = typeof limitRaw === "number" && Number.isFinite(limitRaw)
|
|
67
|
-
? offset + Math.max(0, limitRaw)
|
|
68
|
-
: Number.POSITIVE_INFINITY;
|
|
69
|
-
return { start: offset, end };
|
|
63
|
+
/** Concatenated text of a tool result's text blocks. */
|
|
64
|
+
function toolResultText(m) {
|
|
65
|
+
return m.content
|
|
66
|
+
.filter((c) => c.type === "text")
|
|
67
|
+
.map((c) => c.text ?? "")
|
|
68
|
+
.join("");
|
|
70
69
|
}
|
|
71
|
-
/** Whether two half-open line ranges intersect. */
|
|
72
|
-
function rangesOverlap(a, b) {
|
|
73
|
-
return a.start < b.end && b.start < a.end;
|
|
74
|
-
}
|
|
75
|
-
const WHOLE_FILE_RANGE = { start: 1, end: Number.POSITIVE_INFINITY };
|
|
76
70
|
/**
|
|
77
71
|
* Return a message array with superseded `read` results stubbed out. Returns the
|
|
78
72
|
* original array reference unchanged when there is nothing to evict, so a
|
|
@@ -124,6 +118,10 @@ export function evictSupersededReads(messages, options) {
|
|
|
124
118
|
if (!path)
|
|
125
119
|
return;
|
|
126
120
|
if (READ_TOOLS.has(m.toolName)) {
|
|
121
|
+
// A dedup pointer fetched no content, so it must not supersede (stub) the
|
|
122
|
+
// earlier read it points at — leave it out of the supersession bookkeeping.
|
|
123
|
+
if (isDedupPointerText(toolResultText(m)))
|
|
124
|
+
return;
|
|
127
125
|
const range = rangeByCallId.get(m.toolCallId) ?? WHOLE_FILE_RANGE;
|
|
128
126
|
const list = readsByPath.get(path);
|
|
129
127
|
if (list)
|
|
@@ -148,6 +146,9 @@ export function evictSupersededReads(messages, options) {
|
|
|
148
146
|
const path = resolvedPathByCallId.get(m.toolCallId);
|
|
149
147
|
if (!path)
|
|
150
148
|
return m;
|
|
149
|
+
// Leave at-call dedup pointers as-is; they are already minimal.
|
|
150
|
+
if (isDedupPointerText(toolResultText(m)))
|
|
151
|
+
return m;
|
|
151
152
|
const range = rangeByCallId.get(m.toolCallId) ?? WHOLE_FILE_RANGE;
|
|
152
153
|
const laterMutate = (lastMutateIndex.get(path) ?? -1) > i;
|
|
153
154
|
const laterOverlappingRead = (readsByPath.get(path) ?? []).some((r) => r.index > i && rangesOverlap(r.range, range));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-gc.js","sourceRoot":"","sources":["../../src/core/context-gc.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Context garbage collection.\n *\n * The whole transcript is re-sent on every turn, so a tool result that has\n * become useless keeps costing tokens for the rest of the session. This pass\n * runs on the OUTGOING message copy (via the agent's `transformContext` hook),\n * never on the persisted session, and replaces provably-dead `read` results\n * with a short stub. Nothing is lost: the persisted history is untouched and\n * the file is re-readable on demand.\n *\n * Current rule — superseded reads only (the read-then-edit / re-read pattern,\n * which dominates coding sessions):\n *\n * A `read` result for a path P is stale once, later in the transcript, the\n * same path is edited/written (its on-disk content changed) or read again\n * over an overlapping line range (the newer read reflects that region's newer\n * state). Reads of disjoint regions of the same file coexist — a later read of\n * lines 200-260 does not evict an earlier read of lines 1-40 — so paginating\n * through a large file never makes the model re-fetch a region it already has.\n * A read is only evicted when a *successful* later event supersedes it — so a\n * read whose edit failed (and which the model still needs to retry) is never\n * touched.\n *\n * Deliberately conservative: paths are matched after `path.resolve`, so two\n * different files never collide, and any ambiguity results in NOT evicting.\n *\n * Bash-output eviction is additionally gated on token-budget pressure\n * (`options.budgetPressure`, the fraction of the model's context window in\n * use). At 0 pressure — the default — behaviour is identical to read-only GC.\n * Bash output is not always recoverable, so it carries its own safeguards: a\n * command that looks side-effecting is never elided (re-running it, as the\n * stub invites, could repeat a destructive action), and below 80% pressure\n * only large outputs are elided.\n */\n\nimport { resolve } from \"node:path\";\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\n\n/** Tool names whose result injects file contents into the transcript. */\nconst READ_TOOLS = new Set([\"read\"]);\n/** Tool names that change a file, making a prior read of that path stale. */\nconst MUTATE_TOOLS = new Set([\"edit\", \"write\"]);\n\n/**\n * Commands whose bash output must never be elided, because the stub invites\n * the model to re-run the command and re-running could repeat a destructive or\n * state-changing action. Tested against the whole command string, and\n * deliberately over-broad: a false positive merely keeps an output (safe),\n * while the common verbose *read* commands (cat/grep/ls/find/git log/diff,\n * test runners) intentionally do NOT match, so they stay evictable.\n */\nconst BASH_SIDE_EFFECT_PATTERN =\n\t/(\\b(write|insert|delete|update|curl|wget|psql|mysql|sqlite3|migrate|drop|truncate|rm|rmdir|mv|cp|dd|kill|tee|chmod|chown|ln|mkdir|touch)\\b|\\bgit\\s+(commit|push|reset|checkout|rebase|clean|apply|merge)\\b|\\bsed\\b[^|]*-i|>>?)/i;\n/** Below 80% pressure, only bash outputs larger than this (chars) are elided. */\nconst BASH_EVICTION_CHAR_THRESHOLD = 2000;\n\nexport interface ContextGcOptions {\n\t/** Working directory used to resolve relative tool path arguments. */\n\tcwd: string;\n\t/**\n\t * Token-budget pressure in [0, 1] — the fraction of the model's context\n\t * window currently in use. Absent or 0 reproduces read-only GC behaviour.\n\t * Bash-output eviction begins at 0.6 and becomes unconditional at 0.8.\n\t */\n\tbudgetPressure?: number;\n}\n\ninterface AssistantLike {\n\trole: \"assistant\";\n\tcontent: Array<{ type: string; id?: string; name?: string; arguments?: Record<string, unknown> }>;\n}\n\ninterface ToolResultLike {\n\trole: \"toolResult\";\n\ttoolCallId: string;\n\ttoolName: string;\n\tcontent: Array<{ type: string; text?: string }>;\n\tisError: boolean;\n}\n\nfunction isAssistant(m: AgentMessage): m is AgentMessage & AssistantLike {\n\treturn (m as { role?: string }).role === \"assistant\" && Array.isArray((m as AssistantLike).content);\n}\n\nfunction isToolResult(m: AgentMessage): m is AgentMessage & ToolResultLike {\n\treturn (m as { role?: string }).role === \"toolResult\";\n}\n\n/** Half-open line interval [start, end) a read call covers; end is exclusive. */\ninterface ReadRange {\n\tstart: number;\n\tend: number;\n}\n\n/**\n * Derive the line range a read call covers from its `offset`/`limit` args.\n * Missing offset means \"from line 1\"; missing limit means \"to end of file\"\n * (open-ended, so it overlaps any later read of the same file).\n */\nfunction readRangeFromArgs(args: Record<string, unknown> | undefined): ReadRange {\n\tconst offsetRaw = args?.offset;\n\tconst limitRaw = args?.limit;\n\tconst offset = typeof offsetRaw === \"number\" && Number.isFinite(offsetRaw) && offsetRaw > 0 ? offsetRaw : 1;\n\tconst end =\n\t\ttypeof limitRaw === \"number\" && Number.isFinite(limitRaw)\n\t\t\t? offset + Math.max(0, limitRaw)\n\t\t\t: Number.POSITIVE_INFINITY;\n\treturn { start: offset, end };\n}\n\n/** Whether two half-open line ranges intersect. */\nfunction rangesOverlap(a: ReadRange, b: ReadRange): boolean {\n\treturn a.start < b.end && b.start < a.end;\n}\n\nconst WHOLE_FILE_RANGE: ReadRange = { start: 1, end: Number.POSITIVE_INFINITY };\n\n/**\n * Return a message array with superseded `read` results stubbed out. Returns the\n * original array reference unchanged when there is nothing to evict, so a\n * no-op turn does not needlessly perturb the outgoing context.\n */\nexport function evictSupersededReads(messages: AgentMessage[], options: ContextGcOptions): AgentMessage[] {\n\t// Map each tool call id -> the resolved path it operated on, plus a friendly\n\t// display path (the original argument) for the stub text.\n\tconst resolvedPathByCallId = new Map<string, string>();\n\tconst displayPathByResolved = new Map<string, string>();\n\t// Bash calls carry a `command`, not a `path`; capture it so the eviction\n\t// pass can consult the side-effect guard by tool call id.\n\tconst bashCommandByCallId = new Map<string, string>();\n\t// Read calls carry `offset`/`limit`; capture the line range each covers so a\n\t// later read only supersedes an earlier one when their ranges overlap.\n\tconst rangeByCallId = new Map<string, ReadRange>();\n\tfor (const m of messages) {\n\t\tif (!isAssistant(m)) continue;\n\t\tfor (const block of m.content) {\n\t\t\tif (block.type !== \"toolCall\" || !block.id) continue;\n\t\t\tif (block.name === \"bash\") {\n\t\t\t\tconst cmd = block.arguments?.command;\n\t\t\t\tif (typeof cmd === \"string\" && cmd.length > 0) bashCommandByCallId.set(block.id, cmd);\n\t\t\t}\n\t\t\tconst rawPath = block.arguments?.path;\n\t\t\tif (typeof rawPath !== \"string\" || rawPath.length === 0) continue;\n\t\t\tconst resolved = resolve(options.cwd, rawPath);\n\t\t\tresolvedPathByCallId.set(block.id, resolved);\n\t\t\tif (block.name && READ_TOOLS.has(block.name)) rangeByCallId.set(block.id, readRangeFromArgs(block.arguments));\n\t\t\tif (!displayPathByResolved.has(resolved)) displayPathByResolved.set(resolved, rawPath);\n\t\t}\n\t}\n\n\t// First pass: per path, collect every successful read (index + line range)\n\t// and the last successful mutate. A read at index i is superseded when a later\n\t// successful mutate exists (index > i, whole file changed) or a later read of\n\t// an overlapping range exists (index > i) for the same path.\n\tconst readsByPath = new Map<string, Array<{ index: number; range: ReadRange }>>();\n\tconst lastMutateIndex = new Map<string, number>();\n\tmessages.forEach((m, i) => {\n\t\tif (!isToolResult(m) || m.isError) return;\n\t\tconst path = resolvedPathByCallId.get(m.toolCallId);\n\t\tif (!path) return;\n\t\tif (READ_TOOLS.has(m.toolName)) {\n\t\t\tconst range = rangeByCallId.get(m.toolCallId) ?? WHOLE_FILE_RANGE;\n\t\t\tconst list = readsByPath.get(path);\n\t\t\tif (list) list.push({ index: i, range });\n\t\t\telse readsByPath.set(path, [{ index: i, range }]);\n\t\t} else if (MUTATE_TOOLS.has(m.toolName)) {\n\t\t\tlastMutateIndex.set(path, i);\n\t\t}\n\t});\n\n\t// Second pass: build the output, stubbing evicted reads. Keep every other\n\t// message by reference; clone only the ones we rewrite so the persisted\n\t// history (which may share these objects) is never mutated.\n\tconst pressure = options.budgetPressure ?? 0;\n\tlet changed = false;\n\tconst out = messages.map((m, i) => {\n\t\tif (!isToolResult(m) || m.isError) return m;\n\n\t\t// Superseded-read eviction — always on, pressure-independent.\n\t\tif (READ_TOOLS.has(m.toolName)) {\n\t\t\tconst path = resolvedPathByCallId.get(m.toolCallId);\n\t\t\tif (!path) return m;\n\t\t\tconst range = rangeByCallId.get(m.toolCallId) ?? WHOLE_FILE_RANGE;\n\t\t\tconst laterMutate = (lastMutateIndex.get(path) ?? -1) > i;\n\t\t\tconst laterOverlappingRead = (readsByPath.get(path) ?? []).some(\n\t\t\t\t(r) => r.index > i && rangesOverlap(r.range, range),\n\t\t\t);\n\t\t\tif (!laterOverlappingRead && !laterMutate) return m;\n\t\t\tchanged = true;\n\t\t\tconst display = displayPathByResolved.get(path) ?? path;\n\t\t\tconst reason = laterMutate ? \"the file was modified after this read\" : \"the file was read again later\";\n\t\t\treturn {\n\t\t\t\t...m,\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\ttext: `[Superseded read of ${display} elided to save context — ${reason}. Re-read the file if you need its current contents.]`,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t};\n\t\t}\n\n\t\t// Bash-output eviction — only under token-budget pressure (>= 0.6).\n\t\tif (pressure >= 0.6 && m.toolName === \"bash\") {\n\t\t\tconst cmd = bashCommandByCallId.get(m.toolCallId) ?? \"\";\n\t\t\tif (BASH_SIDE_EFFECT_PATTERN.test(cmd)) return m;\n\t\t\tconst textLen = m.content.filter((c) => c.type === \"text\").reduce((sum, c) => sum + (c.text?.length ?? 0), 0);\n\t\t\t// 60–79%: elide only large outputs. 80%+: elide unconditionally.\n\t\t\tconst shouldEvict = pressure >= 0.8 || textLen > BASH_EVICTION_CHAR_THRESHOLD;\n\t\t\tif (shouldEvict) {\n\t\t\t\tchanged = true;\n\t\t\t\treturn {\n\t\t\t\t\t...m,\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `[Bash output elided at ${Math.round(pressure * 100)}% token budget — re-run if needed.]`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\treturn m;\n\t});\n\n\treturn changed ? out : messages;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"context-gc.js","sourceRoot":"","sources":["../../src/core/context-gc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EACN,kBAAkB,EAElB,aAAa,EACb,iBAAiB,EACjB,gBAAgB,GAChB,MAAM,uBAAuB,CAAC;AAE/B,yEAAyE;AACzE,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;AACrC,6EAA6E;AAC7E,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAEhD;;;;;;;GAOG;AACH,MAAM,wBAAwB,GAC7B,iOAAiO,CAAC;AACnO,iFAAiF;AACjF,MAAM,4BAA4B,GAAG,IAAI,CAAC;AA0B1C,SAAS,WAAW,CAAC,CAAe,EAAqC;IACxE,OAAQ,CAAuB,CAAC,IAAI,KAAK,WAAW,IAAI,KAAK,CAAC,OAAO,CAAE,CAAmB,CAAC,OAAO,CAAC,CAAC;AAAA,CACpG;AAED,SAAS,YAAY,CAAC,CAAe,EAAsC;IAC1E,OAAQ,CAAuB,CAAC,IAAI,KAAK,YAAY,CAAC;AAAA,CACtD;AAED,wDAAwD;AACxD,SAAS,cAAc,CAAC,CAAiB,EAAU;IAClD,OAAO,CAAC,CAAC,OAAO;SACd,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC;SAChC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;SACxB,IAAI,CAAC,EAAE,CAAC,CAAC;AAAA,CACX;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,QAAwB,EAAE,OAAyB,EAAkB;IACzG,6EAA6E;IAC7E,0DAA0D;IAC1D,MAAM,oBAAoB,GAAG,IAAI,GAAG,EAAkB,CAAC;IACvD,MAAM,qBAAqB,GAAG,IAAI,GAAG,EAAkB,CAAC;IACxD,yEAAyE;IACzE,0DAA0D;IAC1D,MAAM,mBAAmB,GAAG,IAAI,GAAG,EAAkB,CAAC;IACtD,6EAA6E;IAC7E,uEAAuE;IACvE,MAAM,aAAa,GAAG,IAAI,GAAG,EAAqB,CAAC;IACnD,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QAC1B,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;YAAE,SAAS;QAC9B,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;YAC/B,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,IAAI,CAAC,KAAK,CAAC,EAAE;gBAAE,SAAS;YACrD,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;gBAC3B,MAAM,GAAG,GAAG,KAAK,CAAC,SAAS,EAAE,OAAO,CAAC;gBACrC,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC;oBAAE,mBAAmB,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;YACvF,CAAC;YACD,MAAM,OAAO,GAAG,KAAK,CAAC,SAAS,EAAE,IAAI,CAAC;YACtC,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;gBAAE,SAAS;YAClE,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;YAC/C,oBAAoB,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;YAC7C,IAAI,KAAK,CAAC,IAAI,IAAI,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;gBAAE,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,iBAAiB,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;YAC9G,IAAI,CAAC,qBAAqB,CAAC,GAAG,CAAC,QAAQ,CAAC;gBAAE,qBAAqB,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QACxF,CAAC;IACF,CAAC;IAED,2EAA2E;IAC3E,+EAA+E;IAC/E,8EAA8E;IAC9E,6DAA6D;IAC7D,MAAM,WAAW,GAAG,IAAI,GAAG,EAAsD,CAAC;IAClF,MAAM,eAAe,GAAG,IAAI,GAAG,EAAkB,CAAC;IAClD,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC1B,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO;YAAE,OAAO;QAC1C,MAAM,IAAI,GAAG,oBAAoB,CAAC,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;QACpD,IAAI,CAAC,IAAI;YAAE,OAAO;QAClB,IAAI,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC;YAChC,0EAA0E;YAC1E,8EAA4E;YAC5E,IAAI,kBAAkB,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC;gBAAE,OAAO;YAClD,MAAM,KAAK,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,gBAAgB,CAAC;YAClE,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACnC,IAAI,IAAI;gBAAE,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;;gBACpC,WAAW,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;QACnD,CAAC;aAAM,IAAI,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC;YACzC,eAAe,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QAC9B,CAAC;IAAA,CACD,CAAC,CAAC;IAEH,0EAA0E;IAC1E,wEAAwE;IACxE,4DAA4D;IAC5D,MAAM,QAAQ,GAAG,OAAO,CAAC,cAAc,IAAI,CAAC,CAAC;IAC7C,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAClC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO;YAAE,OAAO,CAAC,CAAC;QAE5C,gEAA8D;QAC9D,IAAI,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC;YAChC,MAAM,IAAI,GAAG,oBAAoB,CAAC,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;YACpD,IAAI,CAAC,IAAI;gBAAE,OAAO,CAAC,CAAC;YACpB,gEAAgE;YAChE,IAAI,kBAAkB,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC;gBAAE,OAAO,CAAC,CAAC;YACpD,MAAM,KAAK,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,gBAAgB,CAAC;YAClE,MAAM,WAAW,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;YAC1D,MAAM,oBAAoB,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAC9D,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,IAAI,aAAa,CAAC,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC,CACnD,CAAC;YACF,IAAI,CAAC,oBAAoB,IAAI,CAAC,WAAW;gBAAE,OAAO,CAAC,CAAC;YACpD,OAAO,GAAG,IAAI,CAAC;YACf,MAAM,OAAO,GAAG,qBAAqB,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC;YACxD,MAAM,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,uCAAuC,CAAC,CAAC,CAAC,+BAA+B,CAAC;YACvG,OAAO;gBACN,GAAG,CAAC;gBACJ,OAAO,EAAE;oBACR;wBACC,IAAI,EAAE,MAAe;wBACrB,IAAI,EAAE,uBAAuB,OAAO,+BAA6B,MAAM,uDAAuD;qBAC9H;iBACD;aACD,CAAC;QACH,CAAC;QAED,sEAAoE;QACpE,IAAI,QAAQ,IAAI,GAAG,IAAI,CAAC,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;YAC9C,MAAM,GAAG,GAAG,mBAAmB,CAAC,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC;YACxD,IAAI,wBAAwB,CAAC,IAAI,CAAC,GAAG,CAAC;gBAAE,OAAO,CAAC,CAAC;YACjD,MAAM,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;YAC9G,mEAAiE;YACjE,MAAM,WAAW,GAAG,QAAQ,IAAI,GAAG,IAAI,OAAO,GAAG,4BAA4B,CAAC;YAC9E,IAAI,WAAW,EAAE,CAAC;gBACjB,OAAO,GAAG,IAAI,CAAC;gBACf,OAAO;oBACN,GAAG,CAAC;oBACJ,OAAO,EAAE;wBACR;4BACC,IAAI,EAAE,MAAe;4BACrB,IAAI,EAAE,0BAA0B,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,GAAG,CAAC,uCAAqC;yBAC/F;qBACD;iBACD,CAAC;YACH,CAAC;QACF,CAAC;QAED,OAAO,CAAC,CAAC;IAAA,CACT,CAAC,CAAC;IAEH,OAAO,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC;AAAA,CAChC","sourcesContent":["/**\n * Context garbage collection.\n *\n * The whole transcript is re-sent on every turn, so a tool result that has\n * become useless keeps costing tokens for the rest of the session. This pass\n * runs on the OUTGOING message copy (via the agent's `transformContext` hook),\n * never on the persisted session, and replaces provably-dead `read` results\n * with a short stub. Nothing is lost: the persisted history is untouched and\n * the file is re-readable on demand.\n *\n * Current rule — superseded reads only (the read-then-edit / re-read pattern,\n * which dominates coding sessions):\n *\n * A `read` result for a path P is stale once, later in the transcript, the\n * same path is edited/written (its on-disk content changed) or read again\n * over an overlapping line range (the newer read reflects that region's newer\n * state). Reads of disjoint regions of the same file coexist — a later read of\n * lines 200-260 does not evict an earlier read of lines 1-40 — so paginating\n * through a large file never makes the model re-fetch a region it already has.\n * A read is only evicted when a *successful* later event supersedes it — so a\n * read whose edit failed (and which the model still needs to retry) is never\n * touched.\n *\n * Deliberately conservative: paths are matched after `path.resolve`, so two\n * different files never collide, and any ambiguity results in NOT evicting.\n *\n * This post-hoc pass is complemented by an at-call-time guard in the `read`\n * tool (see `tools/read-dedup.ts`), which short-circuits a redundant re-read\n * before it fetches. Both share the range math in that module, and this pass\n * skips the guard's pointer results so they never supersede the read they name.\n *\n * Bash-output eviction is additionally gated on token-budget pressure\n * (`options.budgetPressure`, the fraction of the model's context window in\n * use). At 0 pressure — the default — behaviour is identical to read-only GC.\n * Bash output is not always recoverable, so it carries its own safeguards: a\n * command that looks side-effecting is never elided (re-running it, as the\n * stub invites, could repeat a destructive action), and below 80% pressure\n * only large outputs are elided.\n */\n\nimport { resolve } from \"node:path\";\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport {\n\tisDedupPointerText,\n\ttype ReadRange,\n\trangesOverlap,\n\treadRangeFromArgs,\n\tWHOLE_FILE_RANGE,\n} from \"./tools/read-dedup.js\";\n\n/** Tool names whose result injects file contents into the transcript. */\nconst READ_TOOLS = new Set([\"read\"]);\n/** Tool names that change a file, making a prior read of that path stale. */\nconst MUTATE_TOOLS = new Set([\"edit\", \"write\"]);\n\n/**\n * Commands whose bash output must never be elided, because the stub invites\n * the model to re-run the command and re-running could repeat a destructive or\n * state-changing action. Tested against the whole command string, and\n * deliberately over-broad: a false positive merely keeps an output (safe),\n * while the common verbose *read* commands (cat/grep/ls/find/git log/diff,\n * test runners) intentionally do NOT match, so they stay evictable.\n */\nconst BASH_SIDE_EFFECT_PATTERN =\n\t/(\\b(write|insert|delete|update|curl|wget|psql|mysql|sqlite3|migrate|drop|truncate|rm|rmdir|mv|cp|dd|kill|tee|chmod|chown|ln|mkdir|touch)\\b|\\bgit\\s+(commit|push|reset|checkout|rebase|clean|apply|merge)\\b|\\bsed\\b[^|]*-i|>>?)/i;\n/** Below 80% pressure, only bash outputs larger than this (chars) are elided. */\nconst BASH_EVICTION_CHAR_THRESHOLD = 2000;\n\nexport interface ContextGcOptions {\n\t/** Working directory used to resolve relative tool path arguments. */\n\tcwd: string;\n\t/**\n\t * Token-budget pressure in [0, 1] — the fraction of the model's context\n\t * window currently in use. Absent or 0 reproduces read-only GC behaviour.\n\t * Bash-output eviction begins at 0.6 and becomes unconditional at 0.8.\n\t */\n\tbudgetPressure?: number;\n}\n\ninterface AssistantLike {\n\trole: \"assistant\";\n\tcontent: Array<{ type: string; id?: string; name?: string; arguments?: Record<string, unknown> }>;\n}\n\ninterface ToolResultLike {\n\trole: \"toolResult\";\n\ttoolCallId: string;\n\ttoolName: string;\n\tcontent: Array<{ type: string; text?: string }>;\n\tisError: boolean;\n}\n\nfunction isAssistant(m: AgentMessage): m is AgentMessage & AssistantLike {\n\treturn (m as { role?: string }).role === \"assistant\" && Array.isArray((m as AssistantLike).content);\n}\n\nfunction isToolResult(m: AgentMessage): m is AgentMessage & ToolResultLike {\n\treturn (m as { role?: string }).role === \"toolResult\";\n}\n\n/** Concatenated text of a tool result's text blocks. */\nfunction toolResultText(m: ToolResultLike): string {\n\treturn m.content\n\t\t.filter((c) => c.type === \"text\")\n\t\t.map((c) => c.text ?? \"\")\n\t\t.join(\"\");\n}\n\n/**\n * Return a message array with superseded `read` results stubbed out. Returns the\n * original array reference unchanged when there is nothing to evict, so a\n * no-op turn does not needlessly perturb the outgoing context.\n */\nexport function evictSupersededReads(messages: AgentMessage[], options: ContextGcOptions): AgentMessage[] {\n\t// Map each tool call id -> the resolved path it operated on, plus a friendly\n\t// display path (the original argument) for the stub text.\n\tconst resolvedPathByCallId = new Map<string, string>();\n\tconst displayPathByResolved = new Map<string, string>();\n\t// Bash calls carry a `command`, not a `path`; capture it so the eviction\n\t// pass can consult the side-effect guard by tool call id.\n\tconst bashCommandByCallId = new Map<string, string>();\n\t// Read calls carry `offset`/`limit`; capture the line range each covers so a\n\t// later read only supersedes an earlier one when their ranges overlap.\n\tconst rangeByCallId = new Map<string, ReadRange>();\n\tfor (const m of messages) {\n\t\tif (!isAssistant(m)) continue;\n\t\tfor (const block of m.content) {\n\t\t\tif (block.type !== \"toolCall\" || !block.id) continue;\n\t\t\tif (block.name === \"bash\") {\n\t\t\t\tconst cmd = block.arguments?.command;\n\t\t\t\tif (typeof cmd === \"string\" && cmd.length > 0) bashCommandByCallId.set(block.id, cmd);\n\t\t\t}\n\t\t\tconst rawPath = block.arguments?.path;\n\t\t\tif (typeof rawPath !== \"string\" || rawPath.length === 0) continue;\n\t\t\tconst resolved = resolve(options.cwd, rawPath);\n\t\t\tresolvedPathByCallId.set(block.id, resolved);\n\t\t\tif (block.name && READ_TOOLS.has(block.name)) rangeByCallId.set(block.id, readRangeFromArgs(block.arguments));\n\t\t\tif (!displayPathByResolved.has(resolved)) displayPathByResolved.set(resolved, rawPath);\n\t\t}\n\t}\n\n\t// First pass: per path, collect every successful read (index + line range)\n\t// and the last successful mutate. A read at index i is superseded when a later\n\t// successful mutate exists (index > i, whole file changed) or a later read of\n\t// an overlapping range exists (index > i) for the same path.\n\tconst readsByPath = new Map<string, Array<{ index: number; range: ReadRange }>>();\n\tconst lastMutateIndex = new Map<string, number>();\n\tmessages.forEach((m, i) => {\n\t\tif (!isToolResult(m) || m.isError) return;\n\t\tconst path = resolvedPathByCallId.get(m.toolCallId);\n\t\tif (!path) return;\n\t\tif (READ_TOOLS.has(m.toolName)) {\n\t\t\t// A dedup pointer fetched no content, so it must not supersede (stub) the\n\t\t\t// earlier read it points at — leave it out of the supersession bookkeeping.\n\t\t\tif (isDedupPointerText(toolResultText(m))) return;\n\t\t\tconst range = rangeByCallId.get(m.toolCallId) ?? WHOLE_FILE_RANGE;\n\t\t\tconst list = readsByPath.get(path);\n\t\t\tif (list) list.push({ index: i, range });\n\t\t\telse readsByPath.set(path, [{ index: i, range }]);\n\t\t} else if (MUTATE_TOOLS.has(m.toolName)) {\n\t\t\tlastMutateIndex.set(path, i);\n\t\t}\n\t});\n\n\t// Second pass: build the output, stubbing evicted reads. Keep every other\n\t// message by reference; clone only the ones we rewrite so the persisted\n\t// history (which may share these objects) is never mutated.\n\tconst pressure = options.budgetPressure ?? 0;\n\tlet changed = false;\n\tconst out = messages.map((m, i) => {\n\t\tif (!isToolResult(m) || m.isError) return m;\n\n\t\t// Superseded-read eviction — always on, pressure-independent.\n\t\tif (READ_TOOLS.has(m.toolName)) {\n\t\t\tconst path = resolvedPathByCallId.get(m.toolCallId);\n\t\t\tif (!path) return m;\n\t\t\t// Leave at-call dedup pointers as-is; they are already minimal.\n\t\t\tif (isDedupPointerText(toolResultText(m))) return m;\n\t\t\tconst range = rangeByCallId.get(m.toolCallId) ?? WHOLE_FILE_RANGE;\n\t\t\tconst laterMutate = (lastMutateIndex.get(path) ?? -1) > i;\n\t\t\tconst laterOverlappingRead = (readsByPath.get(path) ?? []).some(\n\t\t\t\t(r) => r.index > i && rangesOverlap(r.range, range),\n\t\t\t);\n\t\t\tif (!laterOverlappingRead && !laterMutate) return m;\n\t\t\tchanged = true;\n\t\t\tconst display = displayPathByResolved.get(path) ?? path;\n\t\t\tconst reason = laterMutate ? \"the file was modified after this read\" : \"the file was read again later\";\n\t\t\treturn {\n\t\t\t\t...m,\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\ttext: `[Superseded read of ${display} elided to save context — ${reason}. Re-read the file if you need its current contents.]`,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t};\n\t\t}\n\n\t\t// Bash-output eviction — only under token-budget pressure (>= 0.6).\n\t\tif (pressure >= 0.6 && m.toolName === \"bash\") {\n\t\t\tconst cmd = bashCommandByCallId.get(m.toolCallId) ?? \"\";\n\t\t\tif (BASH_SIDE_EFFECT_PATTERN.test(cmd)) return m;\n\t\t\tconst textLen = m.content.filter((c) => c.type === \"text\").reduce((sum, c) => sum + (c.text?.length ?? 0), 0);\n\t\t\t// 60–79%: elide only large outputs. 80%+: elide unconditionally.\n\t\t\tconst shouldEvict = pressure >= 0.8 || textLen > BASH_EVICTION_CHAR_THRESHOLD;\n\t\t\tif (shouldEvict) {\n\t\t\t\tchanged = true;\n\t\t\t\treturn {\n\t\t\t\t\t...m,\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `[Bash output elided at ${Math.round(pressure * 100)}% token budget — re-run if needed.]`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\treturn m;\n\t});\n\n\treturn changed ? out : messages;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings-defaults.d.ts","sourceRoot":"","sources":["../../src/core/settings-defaults.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,gBAAgB
|
|
1
|
+
{"version":3,"file":"settings-defaults.d.ts","sourceRoot":"","sources":["../../src/core/settings-defaults.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6ET,CAAC","sourcesContent":["import type { Settings } from \"./settings-manager.js\";\n\nexport const DEFAULT_SETTINGS = {\n\ttransport: \"auto\",\n\tsteeringMode: \"one-at-a-time\",\n\tfollowUpMode: \"one-at-a-time\",\n\tcompaction: {\n\t\tenabled: true,\n\t\treserveTokens: 16384,\n\t\tkeepRecentTokens: 20000,\n\t\tmaxContextRatio: 0.75,\n\t},\n\ttoolOutput: {\n\t\tmaxBytes: 32 * 1024,\n\t\tmaxLines: 800,\n\t},\n\tcontextGc: {\n\t\tenabled: true,\n\t},\n\tdisabledTools: [],\n\ttoolOutputDisplay: \"standard\",\n\tbranchSummary: {\n\t\treserveTokens: 16384,\n\t\tskipPrompt: false,\n\t},\n\tretry: {\n\t\tenabled: true,\n\t\tmaxRetries: 3,\n\t\tbaseDelayMs: 2000,\n\t\tprovider: {\n\t\t\tmaxRetryDelayMs: 60000,\n\t\t},\n\t},\n\thideThinkingBlock: false,\n\tquietStartup: false,\n\tcollapseChangelog: false,\n\tenableInstallTelemetry: true,\n\tenableSkillCommands: true,\n\tenableSubagent: true,\n\twarmSubagents: false,\n\tmaxSubagentDepth: 2,\n\tnestedSubagentConcurrency: 2,\n\tenableTodoWrite: true,\n\tenablePluginTools: false,\n\tdeferMcpSchemas: true,\n\tenableWebTools: false,\n\tenableBrowserTools: false,\n\tenableBrowserLivePreview: false,\n\tenableFileTools: false,\n\tenableEmbsearchTools: true,\n\tembsearchThresholdBytes: 0,\n\tlight: false,\n\tterminal: {\n\t\tshowImages: true,\n\t\timageWidthCells: 60,\n\t\tclearOnShrink: false,\n\t\tshowTerminalProgress: false,\n\t\tchimeOnTurnComplete: false,\n\t},\n\timages: {\n\t\tautoResize: true,\n\t\tblockImages: false,\n\t},\n\tdoubleEscapeAction: \"tree\",\n\ttreeFilterMode: \"default\",\n\teditorPaddingX: 0,\n\tautocompleteMaxVisible: 5,\n\tmarkdown: {\n\t\tcodeBlockIndent: \" \",\n\t},\n\twarnings: {\n\t\tanthropicExtraUsage: true,\n\t},\n\tpackages: [],\n\textensions: [],\n\tskills: [],\n\tprompts: [],\n\tslashCommands: [],\n\tthemes: [],\n} satisfies Settings;\n"]}
|
|
@@ -9,12 +9,14 @@ export const DEFAULT_SETTINGS = {
|
|
|
9
9
|
maxContextRatio: 0.75,
|
|
10
10
|
},
|
|
11
11
|
toolOutput: {
|
|
12
|
-
maxBytes:
|
|
12
|
+
maxBytes: 32 * 1024,
|
|
13
13
|
maxLines: 800,
|
|
14
14
|
},
|
|
15
15
|
contextGc: {
|
|
16
16
|
enabled: true,
|
|
17
17
|
},
|
|
18
|
+
disabledTools: [],
|
|
19
|
+
toolOutputDisplay: "standard",
|
|
18
20
|
branchSummary: {
|
|
19
21
|
reserveTokens: 16384,
|
|
20
22
|
skipPrompt: false,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings-defaults.js","sourceRoot":"","sources":["../../src/core/settings-defaults.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC/B,SAAS,EAAE,MAAM;IACjB,YAAY,EAAE,eAAe;IAC7B,YAAY,EAAE,eAAe;IAC7B,UAAU,EAAE;QACX,OAAO,EAAE,IAAI;QACb,aAAa,EAAE,KAAK;QACpB,gBAAgB,EAAE,KAAK;QACvB,eAAe,EAAE,IAAI;KACrB;IACD,UAAU,EAAE;QACX,QAAQ,EAAE,EAAE,GAAG,IAAI;QACnB,QAAQ,EAAE,GAAG;KACb;IACD,SAAS,EAAE;QACV,OAAO,EAAE,IAAI;KACb;IACD,aAAa,EAAE;QACd,aAAa,EAAE,KAAK;QACpB,UAAU,EAAE,KAAK;KACjB;IACD,KAAK,EAAE;QACN,OAAO,EAAE,IAAI;QACb,UAAU,EAAE,CAAC;QACb,WAAW,EAAE,IAAI;QACjB,QAAQ,EAAE;YACT,eAAe,EAAE,KAAK;SACtB;KACD;IACD,iBAAiB,EAAE,KAAK;IACxB,YAAY,EAAE,KAAK;IACnB,iBAAiB,EAAE,KAAK;IACxB,sBAAsB,EAAE,IAAI;IAC5B,mBAAmB,EAAE,IAAI;IACzB,cAAc,EAAE,IAAI;IACpB,aAAa,EAAE,KAAK;IACpB,gBAAgB,EAAE,CAAC;IACnB,yBAAyB,EAAE,CAAC;IAC5B,eAAe,EAAE,IAAI;IACrB,iBAAiB,EAAE,KAAK;IACxB,eAAe,EAAE,IAAI;IACrB,cAAc,EAAE,KAAK;IACrB,kBAAkB,EAAE,KAAK;IACzB,wBAAwB,EAAE,KAAK;IAC/B,eAAe,EAAE,KAAK;IACtB,oBAAoB,EAAE,IAAI;IAC1B,uBAAuB,EAAE,CAAC;IAC1B,KAAK,EAAE,KAAK;IACZ,QAAQ,EAAE;QACT,UAAU,EAAE,IAAI;QAChB,eAAe,EAAE,EAAE;QACnB,aAAa,EAAE,KAAK;QACpB,oBAAoB,EAAE,KAAK;QAC3B,mBAAmB,EAAE,KAAK;KAC1B;IACD,MAAM,EAAE;QACP,UAAU,EAAE,IAAI;QAChB,WAAW,EAAE,KAAK;KAClB;IACD,kBAAkB,EAAE,MAAM;IAC1B,cAAc,EAAE,SAAS;IACzB,cAAc,EAAE,CAAC;IACjB,sBAAsB,EAAE,CAAC;IACzB,QAAQ,EAAE;QACT,eAAe,EAAE,IAAI;KACrB;IACD,QAAQ,EAAE;QACT,mBAAmB,EAAE,IAAI;KACzB;IACD,QAAQ,EAAE,EAAE;IACZ,UAAU,EAAE,EAAE;IACd,MAAM,EAAE,EAAE;IACV,OAAO,EAAE,EAAE;IACX,aAAa,EAAE,EAAE;IACjB,MAAM,EAAE,EAAE;CACS,CAAC","sourcesContent":["import type { Settings } from \"./settings-manager.js\";\n\nexport const DEFAULT_SETTINGS = {\n\ttransport: \"auto\",\n\tsteeringMode: \"one-at-a-time\",\n\tfollowUpMode: \"one-at-a-time\",\n\tcompaction: {\n\t\tenabled: true,\n\t\treserveTokens: 16384,\n\t\tkeepRecentTokens: 20000,\n\t\tmaxContextRatio: 0.75,\n\t},\n\ttoolOutput: {\n\t\tmaxBytes:
|
|
1
|
+
{"version":3,"file":"settings-defaults.js","sourceRoot":"","sources":["../../src/core/settings-defaults.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC/B,SAAS,EAAE,MAAM;IACjB,YAAY,EAAE,eAAe;IAC7B,YAAY,EAAE,eAAe;IAC7B,UAAU,EAAE;QACX,OAAO,EAAE,IAAI;QACb,aAAa,EAAE,KAAK;QACpB,gBAAgB,EAAE,KAAK;QACvB,eAAe,EAAE,IAAI;KACrB;IACD,UAAU,EAAE;QACX,QAAQ,EAAE,EAAE,GAAG,IAAI;QACnB,QAAQ,EAAE,GAAG;KACb;IACD,SAAS,EAAE;QACV,OAAO,EAAE,IAAI;KACb;IACD,aAAa,EAAE,EAAE;IACjB,iBAAiB,EAAE,UAAU;IAC7B,aAAa,EAAE;QACd,aAAa,EAAE,KAAK;QACpB,UAAU,EAAE,KAAK;KACjB;IACD,KAAK,EAAE;QACN,OAAO,EAAE,IAAI;QACb,UAAU,EAAE,CAAC;QACb,WAAW,EAAE,IAAI;QACjB,QAAQ,EAAE;YACT,eAAe,EAAE,KAAK;SACtB;KACD;IACD,iBAAiB,EAAE,KAAK;IACxB,YAAY,EAAE,KAAK;IACnB,iBAAiB,EAAE,KAAK;IACxB,sBAAsB,EAAE,IAAI;IAC5B,mBAAmB,EAAE,IAAI;IACzB,cAAc,EAAE,IAAI;IACpB,aAAa,EAAE,KAAK;IACpB,gBAAgB,EAAE,CAAC;IACnB,yBAAyB,EAAE,CAAC;IAC5B,eAAe,EAAE,IAAI;IACrB,iBAAiB,EAAE,KAAK;IACxB,eAAe,EAAE,IAAI;IACrB,cAAc,EAAE,KAAK;IACrB,kBAAkB,EAAE,KAAK;IACzB,wBAAwB,EAAE,KAAK;IAC/B,eAAe,EAAE,KAAK;IACtB,oBAAoB,EAAE,IAAI;IAC1B,uBAAuB,EAAE,CAAC;IAC1B,KAAK,EAAE,KAAK;IACZ,QAAQ,EAAE;QACT,UAAU,EAAE,IAAI;QAChB,eAAe,EAAE,EAAE;QACnB,aAAa,EAAE,KAAK;QACpB,oBAAoB,EAAE,KAAK;QAC3B,mBAAmB,EAAE,KAAK;KAC1B;IACD,MAAM,EAAE;QACP,UAAU,EAAE,IAAI;QAChB,WAAW,EAAE,KAAK;KAClB;IACD,kBAAkB,EAAE,MAAM;IAC1B,cAAc,EAAE,SAAS;IACzB,cAAc,EAAE,CAAC;IACjB,sBAAsB,EAAE,CAAC;IACzB,QAAQ,EAAE;QACT,eAAe,EAAE,IAAI;KACrB;IACD,QAAQ,EAAE;QACT,mBAAmB,EAAE,IAAI;KACzB;IACD,QAAQ,EAAE,EAAE;IACZ,UAAU,EAAE,EAAE;IACd,MAAM,EAAE,EAAE;IACV,OAAO,EAAE,EAAE;IACX,aAAa,EAAE,EAAE;IACjB,MAAM,EAAE,EAAE;CACS,CAAC","sourcesContent":["import type { Settings } from \"./settings-manager.js\";\n\nexport const DEFAULT_SETTINGS = {\n\ttransport: \"auto\",\n\tsteeringMode: \"one-at-a-time\",\n\tfollowUpMode: \"one-at-a-time\",\n\tcompaction: {\n\t\tenabled: true,\n\t\treserveTokens: 16384,\n\t\tkeepRecentTokens: 20000,\n\t\tmaxContextRatio: 0.75,\n\t},\n\ttoolOutput: {\n\t\tmaxBytes: 32 * 1024,\n\t\tmaxLines: 800,\n\t},\n\tcontextGc: {\n\t\tenabled: true,\n\t},\n\tdisabledTools: [],\n\ttoolOutputDisplay: \"standard\",\n\tbranchSummary: {\n\t\treserveTokens: 16384,\n\t\tskipPrompt: false,\n\t},\n\tretry: {\n\t\tenabled: true,\n\t\tmaxRetries: 3,\n\t\tbaseDelayMs: 2000,\n\t\tprovider: {\n\t\t\tmaxRetryDelayMs: 60000,\n\t\t},\n\t},\n\thideThinkingBlock: false,\n\tquietStartup: false,\n\tcollapseChangelog: false,\n\tenableInstallTelemetry: true,\n\tenableSkillCommands: true,\n\tenableSubagent: true,\n\twarmSubagents: false,\n\tmaxSubagentDepth: 2,\n\tnestedSubagentConcurrency: 2,\n\tenableTodoWrite: true,\n\tenablePluginTools: false,\n\tdeferMcpSchemas: true,\n\tenableWebTools: false,\n\tenableBrowserTools: false,\n\tenableBrowserLivePreview: false,\n\tenableFileTools: false,\n\tenableEmbsearchTools: true,\n\tembsearchThresholdBytes: 0,\n\tlight: false,\n\tterminal: {\n\t\tshowImages: true,\n\t\timageWidthCells: 60,\n\t\tclearOnShrink: false,\n\t\tshowTerminalProgress: false,\n\t\tchimeOnTurnComplete: false,\n\t},\n\timages: {\n\t\tautoResize: true,\n\t\tblockImages: false,\n\t},\n\tdoubleEscapeAction: \"tree\",\n\ttreeFilterMode: \"default\",\n\teditorPaddingX: 0,\n\tautocompleteMaxVisible: 5,\n\tmarkdown: {\n\t\tcodeBlockIndent: \" \",\n\t},\n\twarnings: {\n\t\tanthropicExtraUsage: true,\n\t},\n\tpackages: [],\n\textensions: [],\n\tskills: [],\n\tprompts: [],\n\tslashCommands: [],\n\tthemes: [],\n} satisfies Settings;\n"]}
|
|
@@ -71,10 +71,23 @@ export declare class SettingsManager {
|
|
|
71
71
|
getCompactionSettings(): Required<CompactionSettings>;
|
|
72
72
|
/** Byte cap on a single read/bash tool result before truncation. Clamped to >= 1024. */
|
|
73
73
|
getToolOutputMaxBytes(): number;
|
|
74
|
+
setToolOutputMaxBytes(bytes: number): void;
|
|
74
75
|
/** Line cap on a single read/bash tool result before truncation. Clamped to >= 1. */
|
|
75
76
|
getToolOutputMaxLines(): number;
|
|
77
|
+
setToolOutputMaxLines(lines: number): void;
|
|
76
78
|
/** Whether to stub superseded read results out of the outgoing context. */
|
|
77
79
|
getContextGcEnabled(): boolean;
|
|
80
|
+
setContextGcEnabled(enabled: boolean): void;
|
|
81
|
+
/** Tool names disabled from the TUI; removed from the agent in every session. */
|
|
82
|
+
getDisabledTools(): string[];
|
|
83
|
+
setDisabledTools(names: string[]): void;
|
|
84
|
+
/** How tool result bodies render in the TUI (collapsed / peek / standard). */
|
|
85
|
+
getToolOutputDisplay(): "collapsed" | "peek" | "standard";
|
|
86
|
+
setToolOutputDisplay(display: "collapsed" | "peek" | "standard"): void;
|
|
87
|
+
/** Persisted overrides for extension-registered flags (name -> value). */
|
|
88
|
+
getFlagOverrides(): Record<string, boolean | string>;
|
|
89
|
+
setFlagOverride(name: string, value: boolean | string): void;
|
|
90
|
+
clearFlagOverride(name: string): void;
|
|
78
91
|
getBranchSummarySettings(): Required<BranchSummarySettings>;
|
|
79
92
|
getBranchSummarySkipPrompt(): boolean;
|
|
80
93
|
getRetryEnabled(): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings-manager.d.ts","sourceRoot":"","sources":["../../src/core/settings-manager.ts"],"names":[],"mappings":"AAIA,OAAO,EACN,mBAAmB,EACnB,uBAAuB,EACvB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EACX,qBAAqB,EACrB,kBAAkB,EAClB,aAAa,EACb,QAAQ,EACR,uBAAuB,EACvB,gBAAgB,EAChB,eAAe,EACf,MAAM,qBAAqB,CAAC;AAiC7B,OAAO,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,CAAC;AACxD,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,eAAe,EAAE,CAAC;AAC9D,YAAY,EACX,qBAAqB,EACrB,kBAAkB,EAClB,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,qBAAqB,EACrB,aAAa,EACb,QAAQ,EACR,gBAAgB,EAChB,uBAAuB,EACvB,gBAAgB,EAChB,eAAe,GACf,MAAM,qBAAqB,CAAC;AAE7B,qBAAa,eAAe;IAC3B,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,cAAc,CAAW;IACjC,OAAO,CAAC,eAAe,CAAW;IAClC,OAAO,CAAC,QAAQ,CAAW;IAC3B,OAAO,CAAC,cAAc,CAA6B;IACnD,OAAO,CAAC,oBAAoB,CAA0C;IACtE,OAAO,CAAC,qBAAqB,CAA6B;IAC1D,OAAO,CAAC,2BAA2B,CAA0C;IAC7E,OAAO,CAAC,uBAAuB,CAAsB;IACrD,OAAO,CAAC,wBAAwB,CAAsB;IACtD,OAAO,CAAC,UAAU,CAAoC;IACtD,OAAO,CAAC,MAAM,CAAkB;IAEhC,OAAO,eAeN;IAED,qDAAqD;IACrD,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAsB,GAAG,eAAe,CAG5E;IAED,iEAAiE;IACjE,MAAM,CAAC,WAAW,CAAC,OAAO,EAAE,eAAe,GAAG,eAAe,CAmB5D;IAED,wDAAwD;IACxD,MAAM,CAAC,QAAQ,CAAC,QAAQ,GAAE,OAAO,CAAC,QAAQ,CAAM,GAAG,eAAe,CAKjE;IAED,OAAO,CAAC,MAAM,CAAC,eAAe;IAc9B,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAWjC,gDAAgD;IAChD,OAAO,CAAC,MAAM,CAAC,eAAe;IA6D9B,iBAAiB,IAAI,QAAQ,CAE5B;IAED,kBAAkB,IAAI,QAAQ,CAE7B;IAED,kBAAkB,IAAI,QAAQ,CAE7B;IAEK,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CA0B5B;IAED,4DAA4D;IAC5D,cAAc,CAAC,SAAS,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,IAAI,CAEjD;IAED,0DAA0D;IAC1D,OAAO,CAAC,YAAY;IAUpB,2DAA2D;IAC3D,OAAO,CAAC,mBAAmB;IAU3B,OAAO,CAAC,WAAW;IAKnB,OAAO,CAAC,kBAAkB;IAW1B,OAAO,CAAC,YAAY;IAWpB,OAAO,CAAC,yBAAyB;IAQjC,OAAO,CAAC,qBAAqB;IA+B7B,OAAO,CAAC,IAAI;IAgBZ,OAAO,CAAC,mBAAmB;IAgBrB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAE3B;IAED,WAAW,IAAI,aAAa,EAAE,CAI7B;IAED,uBAAuB,IAAI,MAAM,GAAG,SAAS,CAE5C;IAED,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAI7C;IAED,aAAa,IAAI,MAAM,GAAG,SAAS,CAYlC;IAED,kBAAkB,IAAI,MAAM,GAAG,SAAS,CAEvC;IAED,eAAe,IAAI,MAAM,GAAG,SAAS,CAEpC;IAED,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAIzC;IAED,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAIrC;IAED,0BAA0B,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAMlE;IAED,eAAe,IAAI,KAAK,GAAG,eAAe,CAEzC;IAED,eAAe,CAAC,IAAI,EAAE,KAAK,GAAG,eAAe,GAAG,IAAI,CAInD;IAED,eAAe,IAAI,KAAK,GAAG,eAAe,CAEzC;IAED,eAAe,CAAC,IAAI,EAAE,KAAK,GAAG,eAAe,GAAG,IAAI,CAInD;IAED,QAAQ,IAAI,MAAM,GAAG,SAAS,CAE7B;IAED,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAI5B;IAED,uBAAuB,IAAI,KAAK,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAE7F;IAED,uBAAuB,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAI5F;IAED,YAAY,IAAI,gBAAgB,CAE/B;IAED,YAAY,CAAC,SAAS,EAAE,gBAAgB,GAAG,IAAI,CAI9C;IAED,oBAAoB,IAAI,OAAO,CAE9B;IAED,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAO3C;IAED,0BAA0B,IAAI,MAAM,CAEnC;IAED,6BAA6B,IAAI,MAAM,CAEtC;IAED,qBAAqB,IAAI,QAAQ,CAAC,kBAAkB,CAAC,CAEpD;IAED,wFAAwF;IACxF,qBAAqB,IAAI,MAAM,CAG9B;IAED,qFAAqF;IACrF,qBAAqB,IAAI,MAAM,CAG9B;IAED,2EAA2E;IAC3E,mBAAmB,IAAI,OAAO,CAE7B;IAED,wBAAwB,IAAI,QAAQ,CAAC,qBAAqB,CAAC,CAE1D;IAED,0BAA0B,IAAI,OAAO,CAEpC;IAED,eAAe,IAAI,OAAO,CAEzB;IAED,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAOtC;IAED,gBAAgB,IAAI;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAMhF;IAED,wBAAwB,IAAI;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,CAK/F;IAED,oBAAoB,IAAI,OAAO,CAE9B;IAED,oBAAoB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAIxC;IAED,YAAY,IAAI,MAAM,GAAG,SAAS,CAEjC;IAED,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAI3C;IAED,eAAe,IAAI,OAAO,CAEzB;IAED,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAIpC;IAED,qBAAqB,IAAI,MAAM,GAAG,SAAS,CAE1C;IAED,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAItD;IAED,aAAa,IAAI,MAAM,EAAE,GAAG,SAAS,CAEpC;IAED,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,SAAS,GAAG,IAAI,CAIjD;IAED,oBAAoB,IAAI,OAAO,CAE9B;IAED,oBAAoB,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAI5C;IAED,yBAAyB,IAAI,OAAO,CAEnC;IAED,yBAAyB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAIhD;IAED,WAAW,IAAI,aAAa,EAAE,CAE7B;IAED,WAAW,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,IAAI,CAI3C;IAED,kBAAkB,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,IAAI,CAKlD;IAED,iBAAiB,IAAI,MAAM,EAAE,CAE5B;IAED,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAIvC;IAED,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAK9C;IAED,aAAa,IAAI,MAAM,EAAE,CAExB;IAED,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAInC;IAED,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAK1C;IAED,sBAAsB,IAAI,MAAM,EAAE,CAEjC;IAED,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAI5C;IAED,6BAA6B,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAKnD;IAED,oBAAoB,IAAI,MAAM,EAAE,CAE/B;IAED,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAI1C;IAED,2BAA2B,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAKjD;IAED,aAAa,IAAI,MAAM,EAAE,CAExB;IAED,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAInC;IAED,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAK1C;IAED,sBAAsB,IAAI,OAAO,CAEhC;IAED,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAI7C;IAED,iBAAiB,IAAI,OAAO,CAE3B;IAED,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAIxC;IAED,oFAAoF;IACpF,gBAAgB,IAAI,OAAO,CAE1B;IAED,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAIvC;IAED,+FAA+F;IAC/F,mBAAmB,IAAI,MAAM,CAG5B;IAED,gFAAgF;IAChF,4BAA4B,IAAI,MAAM,CAGrC;IAED,kBAAkB,IAAI,OAAO,CAE5B;IAED,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAIzC;IAED,0FAA0F;IAC1F,kBAAkB,IAAI,MAAM,EAAE,GAAG,SAAS,CAIzC;IAED,oBAAoB,IAAI,OAAO,CAE9B;IAED,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAI3C;IAED,kBAAkB,IAAI,OAAO,CAE5B;IAED,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAIzC;IAED,iBAAiB,IAAI,OAAO,CAE3B;IAED,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAIxC;IAED,uBAAuB,IAAI,OAAO,CAEjC;IAED,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAI9C;IAED,sBAAsB,IAAI,MAAM,GAAG,SAAS,CAE3C;IAED,0BAA0B,IAAI,MAAM,CAEnC;IAED,qBAAqB,IAAI,OAAO,CAE/B;IAED,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAI5C;IAED,2BAA2B,IAAI,OAAO,CAErC;IAED,2BAA2B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAIlD;IAED,kBAAkB,IAAI,OAAO,CAE5B;IAED,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAIzC;IAED,QAAQ,IAAI,OAAO,CAElB;IAED,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAI/B;IAED,kBAAkB,IAAI,uBAAuB,GAAG,SAAS,CAExD;IAED,kBAAkB,IAAI,YAAY,GAAG,SAAS,GAAG,SAAS,CAEzD;IAED,aAAa,IAAI,OAAO,CAEvB;IAED,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAOjC;IAED,kBAAkB,IAAI,MAAM,CAM3B;IAED,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAOtC;IAED,gBAAgB,IAAI,OAAO,CAM1B;IAED,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAOvC;IAED,uBAAuB,IAAI,OAAO,CAEjC;IAED,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAO9C;IAED,sBAAsB,IAAI,OAAO,CAEhC;IAED,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAO7C;IAED,kBAAkB,IAAI,OAAO,CAE5B;IAED,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAOzC;IAED,cAAc,IAAI,OAAO,CAExB;IAED,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAOrC;IAED,gBAAgB,IAAI,MAAM,EAAE,GAAG,SAAS,CAEvC;IAED,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,SAAS,GAAG,IAAI,CAIrD;IAED,qBAAqB,IAAI,MAAM,GAAG,MAAM,GAAG,MAAM,CAEhD;IAED,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,CAI5D;IAED,iBAAiB,IAAI,SAAS,GAAG,UAAU,GAAG,WAAW,GAAG,cAAc,GAAG,KAAK,CAIjF;IAED,iBAAiB,CAAC,IAAI,EAAE,SAAS,GAAG,UAAU,GAAG,WAAW,GAAG,cAAc,GAAG,KAAK,GAAG,IAAI,CAI3F;IAED,qBAAqB,IAAI,OAAO,CAE/B;IAED,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAI5C;IAED,iBAAiB,IAAI,MAAM,CAE1B;IAED,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAIvC;IAED,yBAAyB,IAAI,MAAM,CAElC;IAED,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAIlD;IAED,kBAAkB,IAAI,MAAM,CAE3B;IAED,WAAW,IAAI,eAAe,CAE7B;IAED,WAAW,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAI3C;CACD","sourcesContent":["import { homedir } from \"os\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../config.js\";\nimport { DEFAULT_SETTINGS } from \"./settings-defaults.js\";\nimport {\n\tFileSettingsStorage,\n\tInMemorySettingsStorage,\n\ttype SettingsError,\n\ttype SettingsScope,\n\ttype SettingsStorage,\n} from \"./settings-storage.js\";\nimport type {\n\tBranchSummarySettings,\n\tCompactionSettings,\n\tPackageSource,\n\tSettings,\n\tThinkingBudgetsSettings,\n\tTransportSetting,\n\tWarningSettings,\n} from \"./settings-types.js\";\n\n/** Deep merge settings: project/overrides take precedence, nested objects merge recursively */\nfunction deepMergeSettings(base: Settings, overrides: Settings): Settings {\n\tconst result: Settings = { ...base };\n\n\tfor (const key of Object.keys(overrides) as (keyof Settings)[]) {\n\t\tconst overrideValue = overrides[key];\n\t\tconst baseValue = base[key];\n\n\t\tif (overrideValue === undefined) {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// For nested objects, merge recursively\n\t\tif (\n\t\t\ttypeof overrideValue === \"object\" &&\n\t\t\toverrideValue !== null &&\n\t\t\t!Array.isArray(overrideValue) &&\n\t\t\ttypeof baseValue === \"object\" &&\n\t\t\tbaseValue !== null &&\n\t\t\t!Array.isArray(baseValue)\n\t\t) {\n\t\t\t(result as Record<string, unknown>)[key] = { ...baseValue, ...overrideValue };\n\t\t} else {\n\t\t\t// For primitives and arrays, override value wins\n\t\t\t(result as Record<string, unknown>)[key] = overrideValue;\n\t\t}\n\t}\n\n\treturn result;\n}\n\nexport { FileSettingsStorage, InMemorySettingsStorage };\nexport type { SettingsError, SettingsScope, SettingsStorage };\nexport type {\n\tBranchSummarySettings,\n\tCompactionSettings,\n\tImageSettings,\n\tMarkdownSettings,\n\tModelCategories,\n\tPackageSource,\n\tProviderRetrySettings,\n\tRetrySettings,\n\tSettings,\n\tTerminalSettings,\n\tThinkingBudgetsSettings,\n\tTransportSetting,\n\tWarningSettings,\n} from \"./settings-types.js\";\n\nexport class SettingsManager {\n\tprivate storage: SettingsStorage;\n\tprivate globalSettings: Settings;\n\tprivate projectSettings: Settings;\n\tprivate settings: Settings;\n\tprivate modifiedFields = new Set<keyof Settings>(); // Track global fields modified during session\n\tprivate modifiedNestedFields = new Map<keyof Settings, Set<string>>(); // Track global nested field modifications\n\tprivate modifiedProjectFields = new Set<keyof Settings>(); // Track project fields modified during session\n\tprivate modifiedProjectNestedFields = new Map<keyof Settings, Set<string>>(); // Track project nested field modifications\n\tprivate globalSettingsLoadError: Error | null = null; // Track if global settings file had parse errors\n\tprivate projectSettingsLoadError: Error | null = null; // Track if project settings file had parse errors\n\tprivate writeQueue: Promise<void> = Promise.resolve();\n\tprivate errors: SettingsError[];\n\n\tprivate constructor(\n\t\tstorage: SettingsStorage,\n\t\tinitialGlobal: Settings,\n\t\tinitialProject: Settings,\n\t\tglobalLoadError: Error | null = null,\n\t\tprojectLoadError: Error | null = null,\n\t\tinitialErrors: SettingsError[] = [],\n\t) {\n\t\tthis.storage = storage;\n\t\tthis.globalSettings = initialGlobal;\n\t\tthis.projectSettings = initialProject;\n\t\tthis.globalSettingsLoadError = globalLoadError;\n\t\tthis.projectSettingsLoadError = projectLoadError;\n\t\tthis.errors = [...initialErrors];\n\t\tthis.settings = deepMergeSettings(this.globalSettings, this.projectSettings);\n\t}\n\n\t/** Create a SettingsManager that loads from files */\n\tstatic create(cwd: string, agentDir: string = getAgentDir()): SettingsManager {\n\t\tconst storage = new FileSettingsStorage(cwd, agentDir);\n\t\treturn SettingsManager.fromStorage(storage);\n\t}\n\n\t/** Create a SettingsManager from an arbitrary storage backend */\n\tstatic fromStorage(storage: SettingsStorage): SettingsManager {\n\t\tconst globalLoad = SettingsManager.tryLoadFromStorage(storage, \"global\");\n\t\tconst projectLoad = SettingsManager.tryLoadFromStorage(storage, \"project\");\n\t\tconst initialErrors: SettingsError[] = [];\n\t\tif (globalLoad.error) {\n\t\t\tinitialErrors.push({ scope: \"global\", error: globalLoad.error });\n\t\t}\n\t\tif (projectLoad.error) {\n\t\t\tinitialErrors.push({ scope: \"project\", error: projectLoad.error });\n\t\t}\n\n\t\treturn new SettingsManager(\n\t\t\tstorage,\n\t\t\tglobalLoad.settings,\n\t\t\tprojectLoad.settings,\n\t\t\tglobalLoad.error,\n\t\t\tprojectLoad.error,\n\t\t\tinitialErrors,\n\t\t);\n\t}\n\n\t/** Create an in-memory SettingsManager (no file I/O) */\n\tstatic inMemory(settings: Partial<Settings> = {}): SettingsManager {\n\t\tconst storage = new InMemorySettingsStorage();\n\t\tconst initialSettings = SettingsManager.migrateSettings(structuredClone(settings) as Record<string, unknown>);\n\t\tstorage.withLock(\"global\", () => JSON.stringify(initialSettings, null, 2));\n\t\treturn SettingsManager.fromStorage(storage);\n\t}\n\n\tprivate static loadFromStorage(storage: SettingsStorage, scope: SettingsScope): Settings {\n\t\tlet content: string | undefined;\n\t\tstorage.withLock(scope, (current) => {\n\t\t\tcontent = current;\n\t\t\treturn undefined;\n\t\t});\n\n\t\tif (!content) {\n\t\t\treturn {};\n\t\t}\n\t\tconst settings = JSON.parse(content);\n\t\treturn SettingsManager.migrateSettings(settings);\n\t}\n\n\tprivate static tryLoadFromStorage(\n\t\tstorage: SettingsStorage,\n\t\tscope: SettingsScope,\n\t): { settings: Settings; error: Error | null } {\n\t\ttry {\n\t\t\treturn { settings: SettingsManager.loadFromStorage(storage, scope), error: null };\n\t\t} catch (error) {\n\t\t\treturn { settings: {}, error: error as Error };\n\t\t}\n\t}\n\n\t/** Migrate old settings format to new format */\n\tprivate static migrateSettings(settings: Record<string, unknown>): Settings {\n\t\t// Migrate queueMode -> steeringMode\n\t\tif (\"queueMode\" in settings && !(\"steeringMode\" in settings)) {\n\t\t\tsettings.steeringMode = settings.queueMode;\n\t\t\tdelete settings.queueMode;\n\t\t}\n\n\t\t// Migrate legacy websockets boolean -> transport enum\n\t\tif (!(\"transport\" in settings) && typeof settings.websockets === \"boolean\") {\n\t\t\tsettings.transport = settings.websockets ? \"websocket\" : \"sse\";\n\t\t\tdelete settings.websockets;\n\t\t}\n\n\t\t// Migrate old skills object format to new array format\n\t\tif (\n\t\t\t\"skills\" in settings &&\n\t\t\ttypeof settings.skills === \"object\" &&\n\t\t\tsettings.skills !== null &&\n\t\t\t!Array.isArray(settings.skills)\n\t\t) {\n\t\t\tconst skillsSettings = settings.skills as {\n\t\t\t\tenableSkillCommands?: boolean;\n\t\t\t\tcustomDirectories?: unknown;\n\t\t\t};\n\t\t\tif (skillsSettings.enableSkillCommands !== undefined && settings.enableSkillCommands === undefined) {\n\t\t\t\tsettings.enableSkillCommands = skillsSettings.enableSkillCommands;\n\t\t\t}\n\t\t\tif (Array.isArray(skillsSettings.customDirectories) && skillsSettings.customDirectories.length > 0) {\n\t\t\t\tsettings.skills = skillsSettings.customDirectories;\n\t\t\t} else {\n\t\t\t\tdelete settings.skills;\n\t\t\t}\n\t\t}\n\n\t\t// Migrate retry.maxDelayMs -> retry.provider.maxRetryDelayMs\n\t\tif (\n\t\t\t\"retry\" in settings &&\n\t\t\ttypeof settings.retry === \"object\" &&\n\t\t\tsettings.retry !== null &&\n\t\t\t!Array.isArray(settings.retry)\n\t\t) {\n\t\t\tconst retrySettings = settings.retry as Record<string, unknown>;\n\t\t\tconst providerSettings =\n\t\t\t\ttypeof retrySettings.provider === \"object\" && retrySettings.provider !== null\n\t\t\t\t\t? (retrySettings.provider as Record<string, unknown>)\n\t\t\t\t\t: undefined;\n\t\t\tif (\n\t\t\t\ttypeof retrySettings.maxDelayMs === \"number\" &&\n\t\t\t\t(providerSettings?.maxRetryDelayMs === undefined || providerSettings?.maxRetryDelayMs === null)\n\t\t\t) {\n\t\t\t\tretrySettings.provider = {\n\t\t\t\t\t...(providerSettings ?? {}),\n\t\t\t\t\tmaxRetryDelayMs: retrySettings.maxDelayMs,\n\t\t\t\t};\n\t\t\t}\n\t\t\tdelete retrySettings.maxDelayMs;\n\t\t}\n\n\t\treturn settings as Settings;\n\t}\n\n\tgetGlobalSettings(): Settings {\n\t\treturn structuredClone(this.globalSettings);\n\t}\n\n\tgetProjectSettings(): Settings {\n\t\treturn structuredClone(this.projectSettings);\n\t}\n\n\tgetDefaultSettings(): Settings {\n\t\treturn structuredClone(DEFAULT_SETTINGS);\n\t}\n\n\tasync reload(): Promise<void> {\n\t\tawait this.writeQueue;\n\t\tconst globalLoad = SettingsManager.tryLoadFromStorage(this.storage, \"global\");\n\t\tif (!globalLoad.error) {\n\t\t\tthis.globalSettings = globalLoad.settings;\n\t\t\tthis.globalSettingsLoadError = null;\n\t\t} else {\n\t\t\tthis.globalSettingsLoadError = globalLoad.error;\n\t\t\tthis.recordError(\"global\", globalLoad.error);\n\t\t}\n\n\t\tthis.modifiedFields.clear();\n\t\tthis.modifiedNestedFields.clear();\n\t\tthis.modifiedProjectFields.clear();\n\t\tthis.modifiedProjectNestedFields.clear();\n\n\t\tconst projectLoad = SettingsManager.tryLoadFromStorage(this.storage, \"project\");\n\t\tif (!projectLoad.error) {\n\t\t\tthis.projectSettings = projectLoad.settings;\n\t\t\tthis.projectSettingsLoadError = null;\n\t\t} else {\n\t\t\tthis.projectSettingsLoadError = projectLoad.error;\n\t\t\tthis.recordError(\"project\", projectLoad.error);\n\t\t}\n\n\t\tthis.settings = deepMergeSettings(this.globalSettings, this.projectSettings);\n\t}\n\n\t/** Apply additional overrides on top of current settings */\n\tapplyOverrides(overrides: Partial<Settings>): void {\n\t\tthis.settings = deepMergeSettings(this.settings, overrides);\n\t}\n\n\t/** Mark a global field as modified during this session */\n\tprivate markModified(field: keyof Settings, nestedKey?: string): void {\n\t\tthis.modifiedFields.add(field);\n\t\tif (nestedKey) {\n\t\t\tif (!this.modifiedNestedFields.has(field)) {\n\t\t\t\tthis.modifiedNestedFields.set(field, new Set());\n\t\t\t}\n\t\t\tthis.modifiedNestedFields.get(field)!.add(nestedKey);\n\t\t}\n\t}\n\n\t/** Mark a project field as modified during this session */\n\tprivate markProjectModified(field: keyof Settings, nestedKey?: string): void {\n\t\tthis.modifiedProjectFields.add(field);\n\t\tif (nestedKey) {\n\t\t\tif (!this.modifiedProjectNestedFields.has(field)) {\n\t\t\t\tthis.modifiedProjectNestedFields.set(field, new Set());\n\t\t\t}\n\t\t\tthis.modifiedProjectNestedFields.get(field)!.add(nestedKey);\n\t\t}\n\t}\n\n\tprivate recordError(scope: SettingsScope, error: unknown): void {\n\t\tconst normalizedError = error instanceof Error ? error : new Error(String(error));\n\t\tthis.errors.push({ scope, error: normalizedError });\n\t}\n\n\tprivate clearModifiedScope(scope: SettingsScope): void {\n\t\tif (scope === \"global\") {\n\t\t\tthis.modifiedFields.clear();\n\t\t\tthis.modifiedNestedFields.clear();\n\t\t\treturn;\n\t\t}\n\n\t\tthis.modifiedProjectFields.clear();\n\t\tthis.modifiedProjectNestedFields.clear();\n\t}\n\n\tprivate enqueueWrite(scope: SettingsScope, task: () => void): void {\n\t\tthis.writeQueue = this.writeQueue\n\t\t\t.then(() => {\n\t\t\t\ttask();\n\t\t\t\tthis.clearModifiedScope(scope);\n\t\t\t})\n\t\t\t.catch((error) => {\n\t\t\t\tthis.recordError(scope, error);\n\t\t\t});\n\t}\n\n\tprivate cloneModifiedNestedFields(source: Map<keyof Settings, Set<string>>): Map<keyof Settings, Set<string>> {\n\t\tconst snapshot = new Map<keyof Settings, Set<string>>();\n\t\tfor (const [key, value] of source.entries()) {\n\t\t\tsnapshot.set(key, new Set(value));\n\t\t}\n\t\treturn snapshot;\n\t}\n\n\tprivate persistScopedSettings(\n\t\tscope: SettingsScope,\n\t\tsnapshotSettings: Settings,\n\t\tmodifiedFields: Set<keyof Settings>,\n\t\tmodifiedNestedFields: Map<keyof Settings, Set<string>>,\n\t): void {\n\t\tthis.storage.withLock(scope, (current) => {\n\t\t\tconst currentFileSettings = current\n\t\t\t\t? SettingsManager.migrateSettings(JSON.parse(current) as Record<string, unknown>)\n\t\t\t\t: {};\n\t\t\tconst mergedSettings: Settings = { ...currentFileSettings };\n\t\t\tfor (const field of modifiedFields) {\n\t\t\t\tconst value = snapshotSettings[field];\n\t\t\t\tif (modifiedNestedFields.has(field) && typeof value === \"object\" && value !== null) {\n\t\t\t\t\tconst nestedModified = modifiedNestedFields.get(field)!;\n\t\t\t\t\tconst baseNested = (currentFileSettings[field] as Record<string, unknown>) ?? {};\n\t\t\t\t\tconst inMemoryNested = value as Record<string, unknown>;\n\t\t\t\t\tconst mergedNested = { ...baseNested };\n\t\t\t\t\tfor (const nestedKey of nestedModified) {\n\t\t\t\t\t\tmergedNested[nestedKey] = inMemoryNested[nestedKey];\n\t\t\t\t\t}\n\t\t\t\t\t(mergedSettings as Record<string, unknown>)[field] = mergedNested;\n\t\t\t\t} else {\n\t\t\t\t\t(mergedSettings as Record<string, unknown>)[field] = value;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn JSON.stringify(mergedSettings, null, 2);\n\t\t});\n\t}\n\n\tprivate save(): void {\n\t\tthis.settings = deepMergeSettings(this.globalSettings, this.projectSettings);\n\n\t\tif (this.globalSettingsLoadError) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst snapshotGlobalSettings = structuredClone(this.globalSettings);\n\t\tconst modifiedFields = new Set(this.modifiedFields);\n\t\tconst modifiedNestedFields = this.cloneModifiedNestedFields(this.modifiedNestedFields);\n\n\t\tthis.enqueueWrite(\"global\", () => {\n\t\t\tthis.persistScopedSettings(\"global\", snapshotGlobalSettings, modifiedFields, modifiedNestedFields);\n\t\t});\n\t}\n\n\tprivate saveProjectSettings(settings: Settings): void {\n\t\tthis.projectSettings = structuredClone(settings);\n\t\tthis.settings = deepMergeSettings(this.globalSettings, this.projectSettings);\n\n\t\tif (this.projectSettingsLoadError) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst snapshotProjectSettings = structuredClone(this.projectSettings);\n\t\tconst modifiedFields = new Set(this.modifiedProjectFields);\n\t\tconst modifiedNestedFields = this.cloneModifiedNestedFields(this.modifiedProjectNestedFields);\n\t\tthis.enqueueWrite(\"project\", () => {\n\t\t\tthis.persistScopedSettings(\"project\", snapshotProjectSettings, modifiedFields, modifiedNestedFields);\n\t\t});\n\t}\n\n\tasync flush(): Promise<void> {\n\t\tawait this.writeQueue;\n\t}\n\n\tdrainErrors(): SettingsError[] {\n\t\tconst drained = [...this.errors];\n\t\tthis.errors = [];\n\t\treturn drained;\n\t}\n\n\tgetLastChangelogVersion(): string | undefined {\n\t\treturn this.settings.lastChangelogVersion;\n\t}\n\n\tsetLastChangelogVersion(version: string): void {\n\t\tthis.globalSettings.lastChangelogVersion = version;\n\t\tthis.markModified(\"lastChangelogVersion\");\n\t\tthis.save();\n\t}\n\n\tgetSessionDir(): string | undefined {\n\t\tconst sessionDir = this.settings.sessionDir;\n\t\tif (!sessionDir) {\n\t\t\treturn sessionDir;\n\t\t}\n\t\tif (sessionDir === \"~\") {\n\t\t\treturn homedir();\n\t\t}\n\t\tif (sessionDir.startsWith(\"~/\")) {\n\t\t\treturn join(homedir(), sessionDir.slice(2));\n\t\t}\n\t\treturn sessionDir;\n\t}\n\n\tgetDefaultProvider(): string | undefined {\n\t\treturn this.settings.defaultProvider;\n\t}\n\n\tgetDefaultModel(): string | undefined {\n\t\treturn this.settings.defaultModel;\n\t}\n\n\tsetDefaultProvider(provider: string): void {\n\t\tthis.globalSettings.defaultProvider = provider;\n\t\tthis.markModified(\"defaultProvider\");\n\t\tthis.save();\n\t}\n\n\tsetDefaultModel(modelId: string): void {\n\t\tthis.globalSettings.defaultModel = modelId;\n\t\tthis.markModified(\"defaultModel\");\n\t\tthis.save();\n\t}\n\n\tsetDefaultModelAndProvider(provider: string, modelId: string): void {\n\t\tthis.globalSettings.defaultProvider = provider;\n\t\tthis.globalSettings.defaultModel = modelId;\n\t\tthis.markModified(\"defaultProvider\");\n\t\tthis.markModified(\"defaultModel\");\n\t\tthis.save();\n\t}\n\n\tgetSteeringMode(): \"all\" | \"one-at-a-time\" {\n\t\treturn this.settings.steeringMode || DEFAULT_SETTINGS.steeringMode!;\n\t}\n\n\tsetSteeringMode(mode: \"all\" | \"one-at-a-time\"): void {\n\t\tthis.globalSettings.steeringMode = mode;\n\t\tthis.markModified(\"steeringMode\");\n\t\tthis.save();\n\t}\n\n\tgetFollowUpMode(): \"all\" | \"one-at-a-time\" {\n\t\treturn this.settings.followUpMode || DEFAULT_SETTINGS.followUpMode!;\n\t}\n\n\tsetFollowUpMode(mode: \"all\" | \"one-at-a-time\"): void {\n\t\tthis.globalSettings.followUpMode = mode;\n\t\tthis.markModified(\"followUpMode\");\n\t\tthis.save();\n\t}\n\n\tgetTheme(): string | undefined {\n\t\treturn this.settings.theme;\n\t}\n\n\tsetTheme(theme: string): void {\n\t\tthis.globalSettings.theme = theme;\n\t\tthis.markModified(\"theme\");\n\t\tthis.save();\n\t}\n\n\tgetDefaultThinkingLevel(): \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | undefined {\n\t\treturn this.settings.defaultThinkingLevel;\n\t}\n\n\tsetDefaultThinkingLevel(level: \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\"): void {\n\t\tthis.globalSettings.defaultThinkingLevel = level;\n\t\tthis.markModified(\"defaultThinkingLevel\");\n\t\tthis.save();\n\t}\n\n\tgetTransport(): TransportSetting {\n\t\treturn this.settings.transport ?? DEFAULT_SETTINGS.transport!;\n\t}\n\n\tsetTransport(transport: TransportSetting): void {\n\t\tthis.globalSettings.transport = transport;\n\t\tthis.markModified(\"transport\");\n\t\tthis.save();\n\t}\n\n\tgetCompactionEnabled(): boolean {\n\t\treturn this.settings.compaction?.enabled ?? DEFAULT_SETTINGS.compaction!.enabled;\n\t}\n\n\tsetCompactionEnabled(enabled: boolean): void {\n\t\tif (!this.globalSettings.compaction) {\n\t\t\tthis.globalSettings.compaction = {};\n\t\t}\n\t\tthis.globalSettings.compaction.enabled = enabled;\n\t\tthis.markModified(\"compaction\", \"enabled\");\n\t\tthis.save();\n\t}\n\n\tgetCompactionReserveTokens(): number {\n\t\treturn this.settings.compaction?.reserveTokens ?? DEFAULT_SETTINGS.compaction!.reserveTokens;\n\t}\n\n\tgetCompactionKeepRecentTokens(): number {\n\t\treturn this.settings.compaction?.keepRecentTokens ?? DEFAULT_SETTINGS.compaction!.keepRecentTokens;\n\t}\n\n\tgetCompactionSettings(): Required<CompactionSettings> {\n\t\treturn { ...DEFAULT_SETTINGS.compaction, ...this.settings.compaction };\n\t}\n\n\t/** Byte cap on a single read/bash tool result before truncation. Clamped to >= 1024. */\n\tgetToolOutputMaxBytes(): number {\n\t\tconst v = this.settings.toolOutput?.maxBytes ?? DEFAULT_SETTINGS.toolOutput!.maxBytes;\n\t\treturn Number.isFinite(v) && v >= 1024 ? Math.floor(v) : DEFAULT_SETTINGS.toolOutput!.maxBytes;\n\t}\n\n\t/** Line cap on a single read/bash tool result before truncation. Clamped to >= 1. */\n\tgetToolOutputMaxLines(): number {\n\t\tconst v = this.settings.toolOutput?.maxLines ?? DEFAULT_SETTINGS.toolOutput!.maxLines;\n\t\treturn Number.isFinite(v) && v >= 1 ? Math.floor(v) : DEFAULT_SETTINGS.toolOutput!.maxLines;\n\t}\n\n\t/** Whether to stub superseded read results out of the outgoing context. */\n\tgetContextGcEnabled(): boolean {\n\t\treturn this.settings.contextGc?.enabled ?? DEFAULT_SETTINGS.contextGc!.enabled;\n\t}\n\n\tgetBranchSummarySettings(): Required<BranchSummarySettings> {\n\t\treturn { ...DEFAULT_SETTINGS.branchSummary, ...this.settings.branchSummary };\n\t}\n\n\tgetBranchSummarySkipPrompt(): boolean {\n\t\treturn this.settings.branchSummary?.skipPrompt ?? DEFAULT_SETTINGS.branchSummary!.skipPrompt;\n\t}\n\n\tgetRetryEnabled(): boolean {\n\t\treturn this.settings.retry?.enabled ?? DEFAULT_SETTINGS.retry!.enabled;\n\t}\n\n\tsetRetryEnabled(enabled: boolean): void {\n\t\tif (!this.globalSettings.retry) {\n\t\t\tthis.globalSettings.retry = {};\n\t\t}\n\t\tthis.globalSettings.retry.enabled = enabled;\n\t\tthis.markModified(\"retry\", \"enabled\");\n\t\tthis.save();\n\t}\n\n\tgetRetrySettings(): { enabled: boolean; maxRetries: number; baseDelayMs: number } {\n\t\treturn {\n\t\t\tenabled: this.getRetryEnabled(),\n\t\t\tmaxRetries: this.settings.retry?.maxRetries ?? DEFAULT_SETTINGS.retry!.maxRetries,\n\t\t\tbaseDelayMs: this.settings.retry?.baseDelayMs ?? DEFAULT_SETTINGS.retry!.baseDelayMs,\n\t\t};\n\t}\n\n\tgetProviderRetrySettings(): { timeoutMs?: number; maxRetries?: number; maxRetryDelayMs: number } {\n\t\treturn {\n\t\t\t...DEFAULT_SETTINGS.retry!.provider,\n\t\t\t...this.settings.retry?.provider,\n\t\t};\n\t}\n\n\tgetHideThinkingBlock(): boolean {\n\t\treturn this.settings.hideThinkingBlock ?? DEFAULT_SETTINGS.hideThinkingBlock!;\n\t}\n\n\tsetHideThinkingBlock(hide: boolean): void {\n\t\tthis.globalSettings.hideThinkingBlock = hide;\n\t\tthis.markModified(\"hideThinkingBlock\");\n\t\tthis.save();\n\t}\n\n\tgetShellPath(): string | undefined {\n\t\treturn this.settings.shellPath;\n\t}\n\n\tsetShellPath(path: string | undefined): void {\n\t\tthis.globalSettings.shellPath = path;\n\t\tthis.markModified(\"shellPath\");\n\t\tthis.save();\n\t}\n\n\tgetQuietStartup(): boolean {\n\t\treturn this.settings.quietStartup ?? DEFAULT_SETTINGS.quietStartup!;\n\t}\n\n\tsetQuietStartup(quiet: boolean): void {\n\t\tthis.globalSettings.quietStartup = quiet;\n\t\tthis.markModified(\"quietStartup\");\n\t\tthis.save();\n\t}\n\n\tgetShellCommandPrefix(): string | undefined {\n\t\treturn this.settings.shellCommandPrefix;\n\t}\n\n\tsetShellCommandPrefix(prefix: string | undefined): void {\n\t\tthis.globalSettings.shellCommandPrefix = prefix;\n\t\tthis.markModified(\"shellCommandPrefix\");\n\t\tthis.save();\n\t}\n\n\tgetNpmCommand(): string[] | undefined {\n\t\treturn this.settings.npmCommand ? [...this.settings.npmCommand] : undefined;\n\t}\n\n\tsetNpmCommand(command: string[] | undefined): void {\n\t\tthis.globalSettings.npmCommand = command ? [...command] : undefined;\n\t\tthis.markModified(\"npmCommand\");\n\t\tthis.save();\n\t}\n\n\tgetCollapseChangelog(): boolean {\n\t\treturn this.settings.collapseChangelog ?? DEFAULT_SETTINGS.collapseChangelog!;\n\t}\n\n\tsetCollapseChangelog(collapse: boolean): void {\n\t\tthis.globalSettings.collapseChangelog = collapse;\n\t\tthis.markModified(\"collapseChangelog\");\n\t\tthis.save();\n\t}\n\n\tgetEnableInstallTelemetry(): boolean {\n\t\treturn this.settings.enableInstallTelemetry ?? DEFAULT_SETTINGS.enableInstallTelemetry!;\n\t}\n\n\tsetEnableInstallTelemetry(enabled: boolean): void {\n\t\tthis.globalSettings.enableInstallTelemetry = enabled;\n\t\tthis.markModified(\"enableInstallTelemetry\");\n\t\tthis.save();\n\t}\n\n\tgetPackages(): PackageSource[] {\n\t\treturn [...(this.settings.packages ?? DEFAULT_SETTINGS.packages!)];\n\t}\n\n\tsetPackages(packages: PackageSource[]): void {\n\t\tthis.globalSettings.packages = packages;\n\t\tthis.markModified(\"packages\");\n\t\tthis.save();\n\t}\n\n\tsetProjectPackages(packages: PackageSource[]): void {\n\t\tconst projectSettings = structuredClone(this.projectSettings);\n\t\tprojectSettings.packages = packages;\n\t\tthis.markProjectModified(\"packages\");\n\t\tthis.saveProjectSettings(projectSettings);\n\t}\n\n\tgetExtensionPaths(): string[] {\n\t\treturn [...(this.settings.extensions ?? DEFAULT_SETTINGS.extensions!)];\n\t}\n\n\tsetExtensionPaths(paths: string[]): void {\n\t\tthis.globalSettings.extensions = paths;\n\t\tthis.markModified(\"extensions\");\n\t\tthis.save();\n\t}\n\n\tsetProjectExtensionPaths(paths: string[]): void {\n\t\tconst projectSettings = structuredClone(this.projectSettings);\n\t\tprojectSettings.extensions = paths;\n\t\tthis.markProjectModified(\"extensions\");\n\t\tthis.saveProjectSettings(projectSettings);\n\t}\n\n\tgetSkillPaths(): string[] {\n\t\treturn [...(this.settings.skills ?? DEFAULT_SETTINGS.skills!)];\n\t}\n\n\tsetSkillPaths(paths: string[]): void {\n\t\tthis.globalSettings.skills = paths;\n\t\tthis.markModified(\"skills\");\n\t\tthis.save();\n\t}\n\n\tsetProjectSkillPaths(paths: string[]): void {\n\t\tconst projectSettings = structuredClone(this.projectSettings);\n\t\tprojectSettings.skills = paths;\n\t\tthis.markProjectModified(\"skills\");\n\t\tthis.saveProjectSettings(projectSettings);\n\t}\n\n\tgetPromptTemplatePaths(): string[] {\n\t\treturn [...(this.settings.prompts ?? DEFAULT_SETTINGS.prompts!)];\n\t}\n\n\tsetPromptTemplatePaths(paths: string[]): void {\n\t\tthis.globalSettings.prompts = paths;\n\t\tthis.markModified(\"prompts\");\n\t\tthis.save();\n\t}\n\n\tsetProjectPromptTemplatePaths(paths: string[]): void {\n\t\tconst projectSettings = structuredClone(this.projectSettings);\n\t\tprojectSettings.prompts = paths;\n\t\tthis.markProjectModified(\"prompts\");\n\t\tthis.saveProjectSettings(projectSettings);\n\t}\n\n\tgetSlashCommandPaths(): string[] {\n\t\treturn [...(this.settings.slashCommands ?? DEFAULT_SETTINGS.slashCommands!)];\n\t}\n\n\tsetSlashCommandPaths(paths: string[]): void {\n\t\tthis.globalSettings.slashCommands = paths;\n\t\tthis.markModified(\"slashCommands\");\n\t\tthis.save();\n\t}\n\n\tsetProjectSlashCommandPaths(paths: string[]): void {\n\t\tconst projectSettings = structuredClone(this.projectSettings);\n\t\tprojectSettings.slashCommands = paths;\n\t\tthis.markProjectModified(\"slashCommands\");\n\t\tthis.saveProjectSettings(projectSettings);\n\t}\n\n\tgetThemePaths(): string[] {\n\t\treturn [...(this.settings.themes ?? DEFAULT_SETTINGS.themes!)];\n\t}\n\n\tsetThemePaths(paths: string[]): void {\n\t\tthis.globalSettings.themes = paths;\n\t\tthis.markModified(\"themes\");\n\t\tthis.save();\n\t}\n\n\tsetProjectThemePaths(paths: string[]): void {\n\t\tconst projectSettings = structuredClone(this.projectSettings);\n\t\tprojectSettings.themes = paths;\n\t\tthis.markProjectModified(\"themes\");\n\t\tthis.saveProjectSettings(projectSettings);\n\t}\n\n\tgetEnableSkillCommands(): boolean {\n\t\treturn this.settings.enableSkillCommands ?? DEFAULT_SETTINGS.enableSkillCommands!;\n\t}\n\n\tsetEnableSkillCommands(enabled: boolean): void {\n\t\tthis.globalSettings.enableSkillCommands = enabled;\n\t\tthis.markModified(\"enableSkillCommands\");\n\t\tthis.save();\n\t}\n\n\tgetEnableSubagent(): boolean {\n\t\treturn this.settings.enableSubagent ?? DEFAULT_SETTINGS.enableSubagent!;\n\t}\n\n\tsetEnableSubagent(enabled: boolean): void {\n\t\tthis.globalSettings.enableSubagent = enabled;\n\t\tthis.markModified(\"enableSubagent\");\n\t\tthis.save();\n\t}\n\n\t/** Whether to dispatch eligible subagents on reused warm workers (experimental). */\n\tgetWarmSubagents(): boolean {\n\t\treturn this.settings.warmSubagents ?? DEFAULT_SETTINGS.warmSubagents!;\n\t}\n\n\tsetWarmSubagents(enabled: boolean): void {\n\t\tthis.globalSettings.warmSubagents = enabled;\n\t\tthis.markModified(\"warmSubagents\");\n\t\tthis.save();\n\t}\n\n\t/** Tree-wide subagent nesting cap. Clamped to >= 1 (delegation can never be disabled here). */\n\tgetMaxSubagentDepth(): number {\n\t\tconst v = this.settings.maxSubagentDepth ?? DEFAULT_SETTINGS.maxSubagentDepth!;\n\t\treturn Number.isFinite(v) && v >= 1 ? Math.floor(v) : 1;\n\t}\n\n\t/** Max concurrent subagents per pool at nesting depth >= 1. Clamped to >= 1. */\n\tgetNestedSubagentConcurrency(): number {\n\t\tconst v = this.settings.nestedSubagentConcurrency ?? DEFAULT_SETTINGS.nestedSubagentConcurrency!;\n\t\treturn Number.isFinite(v) && v >= 1 ? Math.floor(v) : 2;\n\t}\n\n\tgetEnableTodoWrite(): boolean {\n\t\treturn this.settings.enableTodoWrite ?? DEFAULT_SETTINGS.enableTodoWrite!;\n\t}\n\n\tsetEnableTodoWrite(enabled: boolean): void {\n\t\tthis.globalSettings.enableTodoWrite = enabled;\n\t\tthis.markModified(\"enableTodoWrite\");\n\t\tthis.save();\n\t}\n\n\t/** Raw `supportPlatform` tokens (string coerced to an array), or undefined when unset. */\n\tgetSupportPlatform(): string[] | undefined {\n\t\tconst value = this.settings.supportPlatform;\n\t\tif (value === undefined) return undefined;\n\t\treturn Array.isArray(value) ? value : [value];\n\t}\n\n\tgetEnablePluginTools(): boolean {\n\t\treturn this.settings.enablePluginTools ?? DEFAULT_SETTINGS.enablePluginTools!;\n\t}\n\n\tsetEnablePluginTools(enabled: boolean): void {\n\t\tthis.globalSettings.enablePluginTools = enabled;\n\t\tthis.markModified(\"enablePluginTools\");\n\t\tthis.save();\n\t}\n\n\tgetDeferMcpSchemas(): boolean {\n\t\treturn this.settings.deferMcpSchemas ?? DEFAULT_SETTINGS.deferMcpSchemas!;\n\t}\n\n\tsetDeferMcpSchemas(enabled: boolean): void {\n\t\tthis.globalSettings.deferMcpSchemas = enabled;\n\t\tthis.markModified(\"deferMcpSchemas\");\n\t\tthis.save();\n\t}\n\n\tgetEnableWebTools(): boolean {\n\t\treturn this.settings.enableWebTools ?? DEFAULT_SETTINGS.enableWebTools!;\n\t}\n\n\tsetEnableWebTools(enabled: boolean): void {\n\t\tthis.globalSettings.enableWebTools = enabled;\n\t\tthis.markModified(\"enableWebTools\");\n\t\tthis.save();\n\t}\n\n\tgetEnableEmbsearchTools(): boolean {\n\t\treturn this.settings.enableEmbsearchTools ?? DEFAULT_SETTINGS.enableEmbsearchTools!;\n\t}\n\n\tsetEnableEmbsearchTools(enabled: boolean): void {\n\t\tthis.globalSettings.enableEmbsearchTools = enabled;\n\t\tthis.markModified(\"enableEmbsearchTools\");\n\t\tthis.save();\n\t}\n\n\tgetEmbsearchBinaryPath(): string | undefined {\n\t\treturn this.settings.embsearchBinaryPath;\n\t}\n\n\tgetEmbsearchThresholdBytes(): number {\n\t\treturn this.settings.embsearchThresholdBytes ?? DEFAULT_SETTINGS.embsearchThresholdBytes!;\n\t}\n\n\tgetEnableBrowserTools(): boolean {\n\t\treturn this.settings.enableBrowserTools ?? DEFAULT_SETTINGS.enableBrowserTools!;\n\t}\n\n\tsetEnableBrowserTools(enabled: boolean): void {\n\t\tthis.globalSettings.enableBrowserTools = enabled;\n\t\tthis.markModified(\"enableBrowserTools\");\n\t\tthis.save();\n\t}\n\n\tgetEnableBrowserLivePreview(): boolean {\n\t\treturn this.settings.enableBrowserLivePreview ?? DEFAULT_SETTINGS.enableBrowserLivePreview!;\n\t}\n\n\tsetEnableBrowserLivePreview(enabled: boolean): void {\n\t\tthis.globalSettings.enableBrowserLivePreview = enabled;\n\t\tthis.markModified(\"enableBrowserLivePreview\");\n\t\tthis.save();\n\t}\n\n\tgetEnableFileTools(): boolean {\n\t\treturn this.settings.enableFileTools ?? DEFAULT_SETTINGS.enableFileTools!;\n\t}\n\n\tsetEnableFileTools(enabled: boolean): void {\n\t\tthis.globalSettings.enableFileTools = enabled;\n\t\tthis.markModified(\"enableFileTools\");\n\t\tthis.save();\n\t}\n\n\tgetLight(): boolean {\n\t\treturn this.settings.light ?? DEFAULT_SETTINGS.light!;\n\t}\n\n\tsetLight(enabled: boolean): void {\n\t\tthis.globalSettings.light = enabled;\n\t\tthis.markModified(\"light\");\n\t\tthis.save();\n\t}\n\n\tgetThinkingBudgets(): ThinkingBudgetsSettings | undefined {\n\t\treturn this.settings.thinkingBudgets;\n\t}\n\n\tgetThinkingDisplay(): \"summarized\" | \"omitted\" | undefined {\n\t\treturn this.settings.thinkingDisplay;\n\t}\n\n\tgetShowImages(): boolean {\n\t\treturn this.settings.terminal?.showImages ?? DEFAULT_SETTINGS.terminal!.showImages;\n\t}\n\n\tsetShowImages(show: boolean): void {\n\t\tif (!this.globalSettings.terminal) {\n\t\t\tthis.globalSettings.terminal = {};\n\t\t}\n\t\tthis.globalSettings.terminal.showImages = show;\n\t\tthis.markModified(\"terminal\", \"showImages\");\n\t\tthis.save();\n\t}\n\n\tgetImageWidthCells(): number {\n\t\tconst width = this.settings.terminal?.imageWidthCells ?? DEFAULT_SETTINGS.terminal!.imageWidthCells;\n\t\tif (typeof width !== \"number\" || !Number.isFinite(width)) {\n\t\t\treturn DEFAULT_SETTINGS.terminal!.imageWidthCells;\n\t\t}\n\t\treturn Math.max(1, Math.floor(width));\n\t}\n\n\tsetImageWidthCells(width: number): void {\n\t\tif (!this.globalSettings.terminal) {\n\t\t\tthis.globalSettings.terminal = {};\n\t\t}\n\t\tthis.globalSettings.terminal.imageWidthCells = Math.max(1, Math.floor(width));\n\t\tthis.markModified(\"terminal\", \"imageWidthCells\");\n\t\tthis.save();\n\t}\n\n\tgetClearOnShrink(): boolean {\n\t\t// Settings takes precedence, then env var, then default false\n\t\tif (this.settings.terminal?.clearOnShrink !== undefined) {\n\t\t\treturn this.settings.terminal.clearOnShrink;\n\t\t}\n\t\treturn process.env.HOOCODE_CLEAR_ON_SHRINK === \"1\";\n\t}\n\n\tsetClearOnShrink(enabled: boolean): void {\n\t\tif (!this.globalSettings.terminal) {\n\t\t\tthis.globalSettings.terminal = {};\n\t\t}\n\t\tthis.globalSettings.terminal.clearOnShrink = enabled;\n\t\tthis.markModified(\"terminal\", \"clearOnShrink\");\n\t\tthis.save();\n\t}\n\n\tgetShowTerminalProgress(): boolean {\n\t\treturn this.settings.terminal?.showTerminalProgress ?? DEFAULT_SETTINGS.terminal!.showTerminalProgress;\n\t}\n\n\tsetShowTerminalProgress(enabled: boolean): void {\n\t\tif (!this.globalSettings.terminal) {\n\t\t\tthis.globalSettings.terminal = {};\n\t\t}\n\t\tthis.globalSettings.terminal.showTerminalProgress = enabled;\n\t\tthis.markModified(\"terminal\", \"showTerminalProgress\");\n\t\tthis.save();\n\t}\n\n\tgetChimeOnTurnComplete(): boolean {\n\t\treturn this.settings.terminal?.chimeOnTurnComplete ?? DEFAULT_SETTINGS.terminal!.chimeOnTurnComplete;\n\t}\n\n\tsetChimeOnTurnComplete(enabled: boolean): void {\n\t\tif (!this.globalSettings.terminal) {\n\t\t\tthis.globalSettings.terminal = {};\n\t\t}\n\t\tthis.globalSettings.terminal.chimeOnTurnComplete = enabled;\n\t\tthis.markModified(\"terminal\", \"chimeOnTurnComplete\");\n\t\tthis.save();\n\t}\n\n\tgetImageAutoResize(): boolean {\n\t\treturn this.settings.images?.autoResize ?? DEFAULT_SETTINGS.images!.autoResize;\n\t}\n\n\tsetImageAutoResize(enabled: boolean): void {\n\t\tif (!this.globalSettings.images) {\n\t\t\tthis.globalSettings.images = {};\n\t\t}\n\t\tthis.globalSettings.images.autoResize = enabled;\n\t\tthis.markModified(\"images\", \"autoResize\");\n\t\tthis.save();\n\t}\n\n\tgetBlockImages(): boolean {\n\t\treturn this.settings.images?.blockImages ?? DEFAULT_SETTINGS.images!.blockImages;\n\t}\n\n\tsetBlockImages(blocked: boolean): void {\n\t\tif (!this.globalSettings.images) {\n\t\t\tthis.globalSettings.images = {};\n\t\t}\n\t\tthis.globalSettings.images.blockImages = blocked;\n\t\tthis.markModified(\"images\", \"blockImages\");\n\t\tthis.save();\n\t}\n\n\tgetEnabledModels(): string[] | undefined {\n\t\treturn this.settings.enabledModels;\n\t}\n\n\tsetEnabledModels(patterns: string[] | undefined): void {\n\t\tthis.globalSettings.enabledModels = patterns;\n\t\tthis.markModified(\"enabledModels\");\n\t\tthis.save();\n\t}\n\n\tgetDoubleEscapeAction(): \"fork\" | \"tree\" | \"none\" {\n\t\treturn this.settings.doubleEscapeAction ?? DEFAULT_SETTINGS.doubleEscapeAction!;\n\t}\n\n\tsetDoubleEscapeAction(action: \"fork\" | \"tree\" | \"none\"): void {\n\t\tthis.globalSettings.doubleEscapeAction = action;\n\t\tthis.markModified(\"doubleEscapeAction\");\n\t\tthis.save();\n\t}\n\n\tgetTreeFilterMode(): \"default\" | \"no-tools\" | \"user-only\" | \"labeled-only\" | \"all\" {\n\t\tconst mode = this.settings.treeFilterMode;\n\t\tconst valid = [\"default\", \"no-tools\", \"user-only\", \"labeled-only\", \"all\"];\n\t\treturn mode && valid.includes(mode) ? mode : DEFAULT_SETTINGS.treeFilterMode!;\n\t}\n\n\tsetTreeFilterMode(mode: \"default\" | \"no-tools\" | \"user-only\" | \"labeled-only\" | \"all\"): void {\n\t\tthis.globalSettings.treeFilterMode = mode;\n\t\tthis.markModified(\"treeFilterMode\");\n\t\tthis.save();\n\t}\n\n\tgetShowHardwareCursor(): boolean {\n\t\treturn this.settings.showHardwareCursor ?? process.env.HOOCODE_HARDWARE_CURSOR === \"1\";\n\t}\n\n\tsetShowHardwareCursor(enabled: boolean): void {\n\t\tthis.globalSettings.showHardwareCursor = enabled;\n\t\tthis.markModified(\"showHardwareCursor\");\n\t\tthis.save();\n\t}\n\n\tgetEditorPaddingX(): number {\n\t\treturn this.settings.editorPaddingX ?? DEFAULT_SETTINGS.editorPaddingX!;\n\t}\n\n\tsetEditorPaddingX(padding: number): void {\n\t\tthis.globalSettings.editorPaddingX = Math.max(0, Math.min(3, Math.floor(padding)));\n\t\tthis.markModified(\"editorPaddingX\");\n\t\tthis.save();\n\t}\n\n\tgetAutocompleteMaxVisible(): number {\n\t\treturn this.settings.autocompleteMaxVisible ?? DEFAULT_SETTINGS.autocompleteMaxVisible!;\n\t}\n\n\tsetAutocompleteMaxVisible(maxVisible: number): void {\n\t\tthis.globalSettings.autocompleteMaxVisible = Math.max(3, Math.min(20, Math.floor(maxVisible)));\n\t\tthis.markModified(\"autocompleteMaxVisible\");\n\t\tthis.save();\n\t}\n\n\tgetCodeBlockIndent(): string {\n\t\treturn this.settings.markdown?.codeBlockIndent ?? DEFAULT_SETTINGS.markdown!.codeBlockIndent;\n\t}\n\n\tgetWarnings(): WarningSettings {\n\t\treturn { ...DEFAULT_SETTINGS.warnings, ...this.settings.warnings };\n\t}\n\n\tsetWarnings(warnings: WarningSettings): void {\n\t\tthis.globalSettings.warnings = { ...warnings };\n\t\tthis.markModified(\"warnings\");\n\t\tthis.save();\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"settings-manager.d.ts","sourceRoot":"","sources":["../../src/core/settings-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import { homedir } from \"os\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../config.js\";\nimport { DEFAULT_SETTINGS } from \"./settings-defaults.js\";\nimport {\n\tFileSettingsStorage,\n\tInMemorySettingsStorage,\n\ttype SettingsError,\n\ttype SettingsScope,\n\ttype SettingsStorage,\n} from \"./settings-storage.js\";\nimport type {\n\tBranchSummarySettings,\n\tCompactionSettings,\n\tPackageSource,\n\tSettings,\n\tThinkingBudgetsSettings,\n\tTransportSetting,\n\tWarningSettings,\n} from \"./settings-types.js\";\n\n/** Deep merge settings: project/overrides take precedence, nested objects merge recursively */\nfunction deepMergeSettings(base: Settings, overrides: Settings): Settings {\n\tconst result: Settings = { ...base };\n\n\tfor (const key of Object.keys(overrides) as (keyof Settings)[]) {\n\t\tconst overrideValue = overrides[key];\n\t\tconst baseValue = base[key];\n\n\t\tif (overrideValue === undefined) {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// For nested objects, merge recursively\n\t\tif (\n\t\t\ttypeof overrideValue === \"object\" &&\n\t\t\toverrideValue !== null &&\n\t\t\t!Array.isArray(overrideValue) &&\n\t\t\ttypeof baseValue === \"object\" &&\n\t\t\tbaseValue !== null &&\n\t\t\t!Array.isArray(baseValue)\n\t\t) {\n\t\t\t(result as Record<string, unknown>)[key] = { ...baseValue, ...overrideValue };\n\t\t} else {\n\t\t\t// For primitives and arrays, override value wins\n\t\t\t(result as Record<string, unknown>)[key] = overrideValue;\n\t\t}\n\t}\n\n\treturn result;\n}\n\nexport { FileSettingsStorage, InMemorySettingsStorage };\nexport type { SettingsError, SettingsScope, SettingsStorage };\nexport type {\n\tBranchSummarySettings,\n\tCompactionSettings,\n\tImageSettings,\n\tMarkdownSettings,\n\tModelCategories,\n\tPackageSource,\n\tProviderRetrySettings,\n\tRetrySettings,\n\tSettings,\n\tTerminalSettings,\n\tThinkingBudgetsSettings,\n\tTransportSetting,\n\tWarningSettings,\n} from \"./settings-types.js\";\n\nexport class SettingsManager {\n\tprivate storage: SettingsStorage;\n\tprivate globalSettings: Settings;\n\tprivate projectSettings: Settings;\n\tprivate settings: Settings;\n\tprivate modifiedFields = new Set<keyof Settings>(); // Track global fields modified during session\n\tprivate modifiedNestedFields = new Map<keyof Settings, Set<string>>(); // Track global nested field modifications\n\tprivate modifiedProjectFields = new Set<keyof Settings>(); // Track project fields modified during session\n\tprivate modifiedProjectNestedFields = new Map<keyof Settings, Set<string>>(); // Track project nested field modifications\n\tprivate globalSettingsLoadError: Error | null = null; // Track if global settings file had parse errors\n\tprivate projectSettingsLoadError: Error | null = null; // Track if project settings file had parse errors\n\tprivate writeQueue: Promise<void> = Promise.resolve();\n\tprivate errors: SettingsError[];\n\n\tprivate constructor(\n\t\tstorage: SettingsStorage,\n\t\tinitialGlobal: Settings,\n\t\tinitialProject: Settings,\n\t\tglobalLoadError: Error | null = null,\n\t\tprojectLoadError: Error | null = null,\n\t\tinitialErrors: SettingsError[] = [],\n\t) {\n\t\tthis.storage = storage;\n\t\tthis.globalSettings = initialGlobal;\n\t\tthis.projectSettings = initialProject;\n\t\tthis.globalSettingsLoadError = globalLoadError;\n\t\tthis.projectSettingsLoadError = projectLoadError;\n\t\tthis.errors = [...initialErrors];\n\t\tthis.settings = deepMergeSettings(this.globalSettings, this.projectSettings);\n\t}\n\n\t/** Create a SettingsManager that loads from files */\n\tstatic create(cwd: string, agentDir: string = getAgentDir()): SettingsManager {\n\t\tconst storage = new FileSettingsStorage(cwd, agentDir);\n\t\treturn SettingsManager.fromStorage(storage);\n\t}\n\n\t/** Create a SettingsManager from an arbitrary storage backend */\n\tstatic fromStorage(storage: SettingsStorage): SettingsManager {\n\t\tconst globalLoad = SettingsManager.tryLoadFromStorage(storage, \"global\");\n\t\tconst projectLoad = SettingsManager.tryLoadFromStorage(storage, \"project\");\n\t\tconst initialErrors: SettingsError[] = [];\n\t\tif (globalLoad.error) {\n\t\t\tinitialErrors.push({ scope: \"global\", error: globalLoad.error });\n\t\t}\n\t\tif (projectLoad.error) {\n\t\t\tinitialErrors.push({ scope: \"project\", error: projectLoad.error });\n\t\t}\n\n\t\treturn new SettingsManager(\n\t\t\tstorage,\n\t\t\tglobalLoad.settings,\n\t\t\tprojectLoad.settings,\n\t\t\tglobalLoad.error,\n\t\t\tprojectLoad.error,\n\t\t\tinitialErrors,\n\t\t);\n\t}\n\n\t/** Create an in-memory SettingsManager (no file I/O) */\n\tstatic inMemory(settings: Partial<Settings> = {}): SettingsManager {\n\t\tconst storage = new InMemorySettingsStorage();\n\t\tconst initialSettings = SettingsManager.migrateSettings(structuredClone(settings) as Record<string, unknown>);\n\t\tstorage.withLock(\"global\", () => JSON.stringify(initialSettings, null, 2));\n\t\treturn SettingsManager.fromStorage(storage);\n\t}\n\n\tprivate static loadFromStorage(storage: SettingsStorage, scope: SettingsScope): Settings {\n\t\tlet content: string | undefined;\n\t\tstorage.withLock(scope, (current) => {\n\t\t\tcontent = current;\n\t\t\treturn undefined;\n\t\t});\n\n\t\tif (!content) {\n\t\t\treturn {};\n\t\t}\n\t\tconst settings = JSON.parse(content);\n\t\treturn SettingsManager.migrateSettings(settings);\n\t}\n\n\tprivate static tryLoadFromStorage(\n\t\tstorage: SettingsStorage,\n\t\tscope: SettingsScope,\n\t): { settings: Settings; error: Error | null } {\n\t\ttry {\n\t\t\treturn { settings: SettingsManager.loadFromStorage(storage, scope), error: null };\n\t\t} catch (error) {\n\t\t\treturn { settings: {}, error: error as Error };\n\t\t}\n\t}\n\n\t/** Migrate old settings format to new format */\n\tprivate static migrateSettings(settings: Record<string, unknown>): Settings {\n\t\t// Migrate queueMode -> steeringMode\n\t\tif (\"queueMode\" in settings && !(\"steeringMode\" in settings)) {\n\t\t\tsettings.steeringMode = settings.queueMode;\n\t\t\tdelete settings.queueMode;\n\t\t}\n\n\t\t// Migrate legacy websockets boolean -> transport enum\n\t\tif (!(\"transport\" in settings) && typeof settings.websockets === \"boolean\") {\n\t\t\tsettings.transport = settings.websockets ? \"websocket\" : \"sse\";\n\t\t\tdelete settings.websockets;\n\t\t}\n\n\t\t// Migrate old skills object format to new array format\n\t\tif (\n\t\t\t\"skills\" in settings &&\n\t\t\ttypeof settings.skills === \"object\" &&\n\t\t\tsettings.skills !== null &&\n\t\t\t!Array.isArray(settings.skills)\n\t\t) {\n\t\t\tconst skillsSettings = settings.skills as {\n\t\t\t\tenableSkillCommands?: boolean;\n\t\t\t\tcustomDirectories?: unknown;\n\t\t\t};\n\t\t\tif (skillsSettings.enableSkillCommands !== undefined && settings.enableSkillCommands === undefined) {\n\t\t\t\tsettings.enableSkillCommands = skillsSettings.enableSkillCommands;\n\t\t\t}\n\t\t\tif (Array.isArray(skillsSettings.customDirectories) && skillsSettings.customDirectories.length > 0) {\n\t\t\t\tsettings.skills = skillsSettings.customDirectories;\n\t\t\t} else {\n\t\t\t\tdelete settings.skills;\n\t\t\t}\n\t\t}\n\n\t\t// Migrate retry.maxDelayMs -> retry.provider.maxRetryDelayMs\n\t\tif (\n\t\t\t\"retry\" in settings &&\n\t\t\ttypeof settings.retry === \"object\" &&\n\t\t\tsettings.retry !== null &&\n\t\t\t!Array.isArray(settings.retry)\n\t\t) {\n\t\t\tconst retrySettings = settings.retry as Record<string, unknown>;\n\t\t\tconst providerSettings =\n\t\t\t\ttypeof retrySettings.provider === \"object\" && retrySettings.provider !== null\n\t\t\t\t\t? (retrySettings.provider as Record<string, unknown>)\n\t\t\t\t\t: undefined;\n\t\t\tif (\n\t\t\t\ttypeof retrySettings.maxDelayMs === \"number\" &&\n\t\t\t\t(providerSettings?.maxRetryDelayMs === undefined || providerSettings?.maxRetryDelayMs === null)\n\t\t\t) {\n\t\t\t\tretrySettings.provider = {\n\t\t\t\t\t...(providerSettings ?? {}),\n\t\t\t\t\tmaxRetryDelayMs: retrySettings.maxDelayMs,\n\t\t\t\t};\n\t\t\t}\n\t\t\tdelete retrySettings.maxDelayMs;\n\t\t}\n\n\t\treturn settings as Settings;\n\t}\n\n\tgetGlobalSettings(): Settings {\n\t\treturn structuredClone(this.globalSettings);\n\t}\n\n\tgetProjectSettings(): Settings {\n\t\treturn structuredClone(this.projectSettings);\n\t}\n\n\tgetDefaultSettings(): Settings {\n\t\treturn structuredClone(DEFAULT_SETTINGS);\n\t}\n\n\tasync reload(): Promise<void> {\n\t\tawait this.writeQueue;\n\t\tconst globalLoad = SettingsManager.tryLoadFromStorage(this.storage, \"global\");\n\t\tif (!globalLoad.error) {\n\t\t\tthis.globalSettings = globalLoad.settings;\n\t\t\tthis.globalSettingsLoadError = null;\n\t\t} else {\n\t\t\tthis.globalSettingsLoadError = globalLoad.error;\n\t\t\tthis.recordError(\"global\", globalLoad.error);\n\t\t}\n\n\t\tthis.modifiedFields.clear();\n\t\tthis.modifiedNestedFields.clear();\n\t\tthis.modifiedProjectFields.clear();\n\t\tthis.modifiedProjectNestedFields.clear();\n\n\t\tconst projectLoad = SettingsManager.tryLoadFromStorage(this.storage, \"project\");\n\t\tif (!projectLoad.error) {\n\t\t\tthis.projectSettings = projectLoad.settings;\n\t\t\tthis.projectSettingsLoadError = null;\n\t\t} else {\n\t\t\tthis.projectSettingsLoadError = projectLoad.error;\n\t\t\tthis.recordError(\"project\", projectLoad.error);\n\t\t}\n\n\t\tthis.settings = deepMergeSettings(this.globalSettings, this.projectSettings);\n\t}\n\n\t/** Apply additional overrides on top of current settings */\n\tapplyOverrides(overrides: Partial<Settings>): void {\n\t\tthis.settings = deepMergeSettings(this.settings, overrides);\n\t}\n\n\t/** Mark a global field as modified during this session */\n\tprivate markModified(field: keyof Settings, nestedKey?: string): void {\n\t\tthis.modifiedFields.add(field);\n\t\tif (nestedKey) {\n\t\t\tif (!this.modifiedNestedFields.has(field)) {\n\t\t\t\tthis.modifiedNestedFields.set(field, new Set());\n\t\t\t}\n\t\t\tthis.modifiedNestedFields.get(field)!.add(nestedKey);\n\t\t}\n\t}\n\n\t/** Mark a project field as modified during this session */\n\tprivate markProjectModified(field: keyof Settings, nestedKey?: string): void {\n\t\tthis.modifiedProjectFields.add(field);\n\t\tif (nestedKey) {\n\t\t\tif (!this.modifiedProjectNestedFields.has(field)) {\n\t\t\t\tthis.modifiedProjectNestedFields.set(field, new Set());\n\t\t\t}\n\t\t\tthis.modifiedProjectNestedFields.get(field)!.add(nestedKey);\n\t\t}\n\t}\n\n\tprivate recordError(scope: SettingsScope, error: unknown): void {\n\t\tconst normalizedError = error instanceof Error ? error : new Error(String(error));\n\t\tthis.errors.push({ scope, error: normalizedError });\n\t}\n\n\tprivate clearModifiedScope(scope: SettingsScope): void {\n\t\tif (scope === \"global\") {\n\t\t\tthis.modifiedFields.clear();\n\t\t\tthis.modifiedNestedFields.clear();\n\t\t\treturn;\n\t\t}\n\n\t\tthis.modifiedProjectFields.clear();\n\t\tthis.modifiedProjectNestedFields.clear();\n\t}\n\n\tprivate enqueueWrite(scope: SettingsScope, task: () => void): void {\n\t\tthis.writeQueue = this.writeQueue\n\t\t\t.then(() => {\n\t\t\t\ttask();\n\t\t\t\tthis.clearModifiedScope(scope);\n\t\t\t})\n\t\t\t.catch((error) => {\n\t\t\t\tthis.recordError(scope, error);\n\t\t\t});\n\t}\n\n\tprivate cloneModifiedNestedFields(source: Map<keyof Settings, Set<string>>): Map<keyof Settings, Set<string>> {\n\t\tconst snapshot = new Map<keyof Settings, Set<string>>();\n\t\tfor (const [key, value] of source.entries()) {\n\t\t\tsnapshot.set(key, new Set(value));\n\t\t}\n\t\treturn snapshot;\n\t}\n\n\tprivate persistScopedSettings(\n\t\tscope: SettingsScope,\n\t\tsnapshotSettings: Settings,\n\t\tmodifiedFields: Set<keyof Settings>,\n\t\tmodifiedNestedFields: Map<keyof Settings, Set<string>>,\n\t): void {\n\t\tthis.storage.withLock(scope, (current) => {\n\t\t\tconst currentFileSettings = current\n\t\t\t\t? SettingsManager.migrateSettings(JSON.parse(current) as Record<string, unknown>)\n\t\t\t\t: {};\n\t\t\tconst mergedSettings: Settings = { ...currentFileSettings };\n\t\t\tfor (const field of modifiedFields) {\n\t\t\t\tconst value = snapshotSettings[field];\n\t\t\t\tif (modifiedNestedFields.has(field) && typeof value === \"object\" && value !== null) {\n\t\t\t\t\tconst nestedModified = modifiedNestedFields.get(field)!;\n\t\t\t\t\tconst baseNested = (currentFileSettings[field] as Record<string, unknown>) ?? {};\n\t\t\t\t\tconst inMemoryNested = value as Record<string, unknown>;\n\t\t\t\t\tconst mergedNested = { ...baseNested };\n\t\t\t\t\tfor (const nestedKey of nestedModified) {\n\t\t\t\t\t\tmergedNested[nestedKey] = inMemoryNested[nestedKey];\n\t\t\t\t\t}\n\t\t\t\t\t(mergedSettings as Record<string, unknown>)[field] = mergedNested;\n\t\t\t\t} else {\n\t\t\t\t\t(mergedSettings as Record<string, unknown>)[field] = value;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn JSON.stringify(mergedSettings, null, 2);\n\t\t});\n\t}\n\n\tprivate save(): void {\n\t\tthis.settings = deepMergeSettings(this.globalSettings, this.projectSettings);\n\n\t\tif (this.globalSettingsLoadError) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst snapshotGlobalSettings = structuredClone(this.globalSettings);\n\t\tconst modifiedFields = new Set(this.modifiedFields);\n\t\tconst modifiedNestedFields = this.cloneModifiedNestedFields(this.modifiedNestedFields);\n\n\t\tthis.enqueueWrite(\"global\", () => {\n\t\t\tthis.persistScopedSettings(\"global\", snapshotGlobalSettings, modifiedFields, modifiedNestedFields);\n\t\t});\n\t}\n\n\tprivate saveProjectSettings(settings: Settings): void {\n\t\tthis.projectSettings = structuredClone(settings);\n\t\tthis.settings = deepMergeSettings(this.globalSettings, this.projectSettings);\n\n\t\tif (this.projectSettingsLoadError) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst snapshotProjectSettings = structuredClone(this.projectSettings);\n\t\tconst modifiedFields = new Set(this.modifiedProjectFields);\n\t\tconst modifiedNestedFields = this.cloneModifiedNestedFields(this.modifiedProjectNestedFields);\n\t\tthis.enqueueWrite(\"project\", () => {\n\t\t\tthis.persistScopedSettings(\"project\", snapshotProjectSettings, modifiedFields, modifiedNestedFields);\n\t\t});\n\t}\n\n\tasync flush(): Promise<void> {\n\t\tawait this.writeQueue;\n\t}\n\n\tdrainErrors(): SettingsError[] {\n\t\tconst drained = [...this.errors];\n\t\tthis.errors = [];\n\t\treturn drained;\n\t}\n\n\tgetLastChangelogVersion(): string | undefined {\n\t\treturn this.settings.lastChangelogVersion;\n\t}\n\n\tsetLastChangelogVersion(version: string): void {\n\t\tthis.globalSettings.lastChangelogVersion = version;\n\t\tthis.markModified(\"lastChangelogVersion\");\n\t\tthis.save();\n\t}\n\n\tgetSessionDir(): string | undefined {\n\t\tconst sessionDir = this.settings.sessionDir;\n\t\tif (!sessionDir) {\n\t\t\treturn sessionDir;\n\t\t}\n\t\tif (sessionDir === \"~\") {\n\t\t\treturn homedir();\n\t\t}\n\t\tif (sessionDir.startsWith(\"~/\")) {\n\t\t\treturn join(homedir(), sessionDir.slice(2));\n\t\t}\n\t\treturn sessionDir;\n\t}\n\n\tgetDefaultProvider(): string | undefined {\n\t\treturn this.settings.defaultProvider;\n\t}\n\n\tgetDefaultModel(): string | undefined {\n\t\treturn this.settings.defaultModel;\n\t}\n\n\tsetDefaultProvider(provider: string): void {\n\t\tthis.globalSettings.defaultProvider = provider;\n\t\tthis.markModified(\"defaultProvider\");\n\t\tthis.save();\n\t}\n\n\tsetDefaultModel(modelId: string): void {\n\t\tthis.globalSettings.defaultModel = modelId;\n\t\tthis.markModified(\"defaultModel\");\n\t\tthis.save();\n\t}\n\n\tsetDefaultModelAndProvider(provider: string, modelId: string): void {\n\t\tthis.globalSettings.defaultProvider = provider;\n\t\tthis.globalSettings.defaultModel = modelId;\n\t\tthis.markModified(\"defaultProvider\");\n\t\tthis.markModified(\"defaultModel\");\n\t\tthis.save();\n\t}\n\n\tgetSteeringMode(): \"all\" | \"one-at-a-time\" {\n\t\treturn this.settings.steeringMode || DEFAULT_SETTINGS.steeringMode!;\n\t}\n\n\tsetSteeringMode(mode: \"all\" | \"one-at-a-time\"): void {\n\t\tthis.globalSettings.steeringMode = mode;\n\t\tthis.markModified(\"steeringMode\");\n\t\tthis.save();\n\t}\n\n\tgetFollowUpMode(): \"all\" | \"one-at-a-time\" {\n\t\treturn this.settings.followUpMode || DEFAULT_SETTINGS.followUpMode!;\n\t}\n\n\tsetFollowUpMode(mode: \"all\" | \"one-at-a-time\"): void {\n\t\tthis.globalSettings.followUpMode = mode;\n\t\tthis.markModified(\"followUpMode\");\n\t\tthis.save();\n\t}\n\n\tgetTheme(): string | undefined {\n\t\treturn this.settings.theme;\n\t}\n\n\tsetTheme(theme: string): void {\n\t\tthis.globalSettings.theme = theme;\n\t\tthis.markModified(\"theme\");\n\t\tthis.save();\n\t}\n\n\tgetDefaultThinkingLevel(): \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | undefined {\n\t\treturn this.settings.defaultThinkingLevel;\n\t}\n\n\tsetDefaultThinkingLevel(level: \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\"): void {\n\t\tthis.globalSettings.defaultThinkingLevel = level;\n\t\tthis.markModified(\"defaultThinkingLevel\");\n\t\tthis.save();\n\t}\n\n\tgetTransport(): TransportSetting {\n\t\treturn this.settings.transport ?? DEFAULT_SETTINGS.transport!;\n\t}\n\n\tsetTransport(transport: TransportSetting): void {\n\t\tthis.globalSettings.transport = transport;\n\t\tthis.markModified(\"transport\");\n\t\tthis.save();\n\t}\n\n\tgetCompactionEnabled(): boolean {\n\t\treturn this.settings.compaction?.enabled ?? DEFAULT_SETTINGS.compaction!.enabled;\n\t}\n\n\tsetCompactionEnabled(enabled: boolean): void {\n\t\tif (!this.globalSettings.compaction) {\n\t\t\tthis.globalSettings.compaction = {};\n\t\t}\n\t\tthis.globalSettings.compaction.enabled = enabled;\n\t\tthis.markModified(\"compaction\", \"enabled\");\n\t\tthis.save();\n\t}\n\n\tgetCompactionReserveTokens(): number {\n\t\treturn this.settings.compaction?.reserveTokens ?? DEFAULT_SETTINGS.compaction!.reserveTokens;\n\t}\n\n\tgetCompactionKeepRecentTokens(): number {\n\t\treturn this.settings.compaction?.keepRecentTokens ?? DEFAULT_SETTINGS.compaction!.keepRecentTokens;\n\t}\n\n\tgetCompactionSettings(): Required<CompactionSettings> {\n\t\treturn { ...DEFAULT_SETTINGS.compaction, ...this.settings.compaction };\n\t}\n\n\t/** Byte cap on a single read/bash tool result before truncation. Clamped to >= 1024. */\n\tgetToolOutputMaxBytes(): number {\n\t\tconst v = this.settings.toolOutput?.maxBytes ?? DEFAULT_SETTINGS.toolOutput!.maxBytes;\n\t\treturn Number.isFinite(v) && v >= 1024 ? Math.floor(v) : DEFAULT_SETTINGS.toolOutput!.maxBytes;\n\t}\n\n\tsetToolOutputMaxBytes(bytes: number): void {\n\t\tif (!this.globalSettings.toolOutput) {\n\t\t\tthis.globalSettings.toolOutput = {};\n\t\t}\n\t\tthis.globalSettings.toolOutput.maxBytes = Math.max(1024, Math.floor(bytes));\n\t\tthis.markModified(\"toolOutput\", \"maxBytes\");\n\t\tthis.save();\n\t}\n\n\t/** Line cap on a single read/bash tool result before truncation. Clamped to >= 1. */\n\tgetToolOutputMaxLines(): number {\n\t\tconst v = this.settings.toolOutput?.maxLines ?? DEFAULT_SETTINGS.toolOutput!.maxLines;\n\t\treturn Number.isFinite(v) && v >= 1 ? Math.floor(v) : DEFAULT_SETTINGS.toolOutput!.maxLines;\n\t}\n\n\tsetToolOutputMaxLines(lines: number): void {\n\t\tif (!this.globalSettings.toolOutput) {\n\t\t\tthis.globalSettings.toolOutput = {};\n\t\t}\n\t\tthis.globalSettings.toolOutput.maxLines = Math.max(1, Math.floor(lines));\n\t\tthis.markModified(\"toolOutput\", \"maxLines\");\n\t\tthis.save();\n\t}\n\n\t/** Whether to stub superseded read results out of the outgoing context. */\n\tgetContextGcEnabled(): boolean {\n\t\treturn this.settings.contextGc?.enabled ?? DEFAULT_SETTINGS.contextGc!.enabled;\n\t}\n\n\tsetContextGcEnabled(enabled: boolean): void {\n\t\tif (!this.globalSettings.contextGc) {\n\t\t\tthis.globalSettings.contextGc = {};\n\t\t}\n\t\tthis.globalSettings.contextGc.enabled = enabled;\n\t\tthis.markModified(\"contextGc\", \"enabled\");\n\t\tthis.save();\n\t}\n\n\t/** Tool names disabled from the TUI; removed from the agent in every session. */\n\tgetDisabledTools(): string[] {\n\t\treturn [...(this.settings.disabledTools ?? DEFAULT_SETTINGS.disabledTools!)];\n\t}\n\n\tsetDisabledTools(names: string[]): void {\n\t\tthis.globalSettings.disabledTools = [...names];\n\t\tthis.markModified(\"disabledTools\");\n\t\tthis.save();\n\t}\n\n\t/** How tool result bodies render in the TUI (collapsed / peek / standard). */\n\tgetToolOutputDisplay(): \"collapsed\" | \"peek\" | \"standard\" {\n\t\tconst v = this.settings.toolOutputDisplay;\n\t\treturn v === \"collapsed\" || v === \"peek\" || v === \"standard\" ? v : DEFAULT_SETTINGS.toolOutputDisplay!;\n\t}\n\n\tsetToolOutputDisplay(display: \"collapsed\" | \"peek\" | \"standard\"): void {\n\t\tthis.globalSettings.toolOutputDisplay = display;\n\t\tthis.markModified(\"toolOutputDisplay\");\n\t\tthis.save();\n\t}\n\n\t/** Persisted overrides for extension-registered flags (name -> value). */\n\tgetFlagOverrides(): Record<string, boolean | string> {\n\t\treturn { ...(this.settings.flags ?? {}) };\n\t}\n\n\tsetFlagOverride(name: string, value: boolean | string): void {\n\t\tif (!this.globalSettings.flags) {\n\t\t\tthis.globalSettings.flags = {};\n\t\t}\n\t\tthis.globalSettings.flags[name] = value;\n\t\tthis.markModified(\"flags\", name);\n\t\tthis.save();\n\t}\n\n\tclearFlagOverride(name: string): void {\n\t\tif (this.globalSettings.flags && name in this.globalSettings.flags) {\n\t\t\tdelete this.globalSettings.flags[name];\n\t\t\tthis.markModified(\"flags\", name);\n\t\t\tthis.save();\n\t\t}\n\t}\n\n\tgetBranchSummarySettings(): Required<BranchSummarySettings> {\n\t\treturn { ...DEFAULT_SETTINGS.branchSummary, ...this.settings.branchSummary };\n\t}\n\n\tgetBranchSummarySkipPrompt(): boolean {\n\t\treturn this.settings.branchSummary?.skipPrompt ?? DEFAULT_SETTINGS.branchSummary!.skipPrompt;\n\t}\n\n\tgetRetryEnabled(): boolean {\n\t\treturn this.settings.retry?.enabled ?? DEFAULT_SETTINGS.retry!.enabled;\n\t}\n\n\tsetRetryEnabled(enabled: boolean): void {\n\t\tif (!this.globalSettings.retry) {\n\t\t\tthis.globalSettings.retry = {};\n\t\t}\n\t\tthis.globalSettings.retry.enabled = enabled;\n\t\tthis.markModified(\"retry\", \"enabled\");\n\t\tthis.save();\n\t}\n\n\tgetRetrySettings(): { enabled: boolean; maxRetries: number; baseDelayMs: number } {\n\t\treturn {\n\t\t\tenabled: this.getRetryEnabled(),\n\t\t\tmaxRetries: this.settings.retry?.maxRetries ?? DEFAULT_SETTINGS.retry!.maxRetries,\n\t\t\tbaseDelayMs: this.settings.retry?.baseDelayMs ?? DEFAULT_SETTINGS.retry!.baseDelayMs,\n\t\t};\n\t}\n\n\tgetProviderRetrySettings(): { timeoutMs?: number; maxRetries?: number; maxRetryDelayMs: number } {\n\t\treturn {\n\t\t\t...DEFAULT_SETTINGS.retry!.provider,\n\t\t\t...this.settings.retry?.provider,\n\t\t};\n\t}\n\n\tgetHideThinkingBlock(): boolean {\n\t\treturn this.settings.hideThinkingBlock ?? DEFAULT_SETTINGS.hideThinkingBlock!;\n\t}\n\n\tsetHideThinkingBlock(hide: boolean): void {\n\t\tthis.globalSettings.hideThinkingBlock = hide;\n\t\tthis.markModified(\"hideThinkingBlock\");\n\t\tthis.save();\n\t}\n\n\tgetShellPath(): string | undefined {\n\t\treturn this.settings.shellPath;\n\t}\n\n\tsetShellPath(path: string | undefined): void {\n\t\tthis.globalSettings.shellPath = path;\n\t\tthis.markModified(\"shellPath\");\n\t\tthis.save();\n\t}\n\n\tgetQuietStartup(): boolean {\n\t\treturn this.settings.quietStartup ?? DEFAULT_SETTINGS.quietStartup!;\n\t}\n\n\tsetQuietStartup(quiet: boolean): void {\n\t\tthis.globalSettings.quietStartup = quiet;\n\t\tthis.markModified(\"quietStartup\");\n\t\tthis.save();\n\t}\n\n\tgetShellCommandPrefix(): string | undefined {\n\t\treturn this.settings.shellCommandPrefix;\n\t}\n\n\tsetShellCommandPrefix(prefix: string | undefined): void {\n\t\tthis.globalSettings.shellCommandPrefix = prefix;\n\t\tthis.markModified(\"shellCommandPrefix\");\n\t\tthis.save();\n\t}\n\n\tgetNpmCommand(): string[] | undefined {\n\t\treturn this.settings.npmCommand ? [...this.settings.npmCommand] : undefined;\n\t}\n\n\tsetNpmCommand(command: string[] | undefined): void {\n\t\tthis.globalSettings.npmCommand = command ? [...command] : undefined;\n\t\tthis.markModified(\"npmCommand\");\n\t\tthis.save();\n\t}\n\n\tgetCollapseChangelog(): boolean {\n\t\treturn this.settings.collapseChangelog ?? DEFAULT_SETTINGS.collapseChangelog!;\n\t}\n\n\tsetCollapseChangelog(collapse: boolean): void {\n\t\tthis.globalSettings.collapseChangelog = collapse;\n\t\tthis.markModified(\"collapseChangelog\");\n\t\tthis.save();\n\t}\n\n\tgetEnableInstallTelemetry(): boolean {\n\t\treturn this.settings.enableInstallTelemetry ?? DEFAULT_SETTINGS.enableInstallTelemetry!;\n\t}\n\n\tsetEnableInstallTelemetry(enabled: boolean): void {\n\t\tthis.globalSettings.enableInstallTelemetry = enabled;\n\t\tthis.markModified(\"enableInstallTelemetry\");\n\t\tthis.save();\n\t}\n\n\tgetPackages(): PackageSource[] {\n\t\treturn [...(this.settings.packages ?? DEFAULT_SETTINGS.packages!)];\n\t}\n\n\tsetPackages(packages: PackageSource[]): void {\n\t\tthis.globalSettings.packages = packages;\n\t\tthis.markModified(\"packages\");\n\t\tthis.save();\n\t}\n\n\tsetProjectPackages(packages: PackageSource[]): void {\n\t\tconst projectSettings = structuredClone(this.projectSettings);\n\t\tprojectSettings.packages = packages;\n\t\tthis.markProjectModified(\"packages\");\n\t\tthis.saveProjectSettings(projectSettings);\n\t}\n\n\tgetExtensionPaths(): string[] {\n\t\treturn [...(this.settings.extensions ?? DEFAULT_SETTINGS.extensions!)];\n\t}\n\n\tsetExtensionPaths(paths: string[]): void {\n\t\tthis.globalSettings.extensions = paths;\n\t\tthis.markModified(\"extensions\");\n\t\tthis.save();\n\t}\n\n\tsetProjectExtensionPaths(paths: string[]): void {\n\t\tconst projectSettings = structuredClone(this.projectSettings);\n\t\tprojectSettings.extensions = paths;\n\t\tthis.markProjectModified(\"extensions\");\n\t\tthis.saveProjectSettings(projectSettings);\n\t}\n\n\tgetSkillPaths(): string[] {\n\t\treturn [...(this.settings.skills ?? DEFAULT_SETTINGS.skills!)];\n\t}\n\n\tsetSkillPaths(paths: string[]): void {\n\t\tthis.globalSettings.skills = paths;\n\t\tthis.markModified(\"skills\");\n\t\tthis.save();\n\t}\n\n\tsetProjectSkillPaths(paths: string[]): void {\n\t\tconst projectSettings = structuredClone(this.projectSettings);\n\t\tprojectSettings.skills = paths;\n\t\tthis.markProjectModified(\"skills\");\n\t\tthis.saveProjectSettings(projectSettings);\n\t}\n\n\tgetPromptTemplatePaths(): string[] {\n\t\treturn [...(this.settings.prompts ?? DEFAULT_SETTINGS.prompts!)];\n\t}\n\n\tsetPromptTemplatePaths(paths: string[]): void {\n\t\tthis.globalSettings.prompts = paths;\n\t\tthis.markModified(\"prompts\");\n\t\tthis.save();\n\t}\n\n\tsetProjectPromptTemplatePaths(paths: string[]): void {\n\t\tconst projectSettings = structuredClone(this.projectSettings);\n\t\tprojectSettings.prompts = paths;\n\t\tthis.markProjectModified(\"prompts\");\n\t\tthis.saveProjectSettings(projectSettings);\n\t}\n\n\tgetSlashCommandPaths(): string[] {\n\t\treturn [...(this.settings.slashCommands ?? DEFAULT_SETTINGS.slashCommands!)];\n\t}\n\n\tsetSlashCommandPaths(paths: string[]): void {\n\t\tthis.globalSettings.slashCommands = paths;\n\t\tthis.markModified(\"slashCommands\");\n\t\tthis.save();\n\t}\n\n\tsetProjectSlashCommandPaths(paths: string[]): void {\n\t\tconst projectSettings = structuredClone(this.projectSettings);\n\t\tprojectSettings.slashCommands = paths;\n\t\tthis.markProjectModified(\"slashCommands\");\n\t\tthis.saveProjectSettings(projectSettings);\n\t}\n\n\tgetThemePaths(): string[] {\n\t\treturn [...(this.settings.themes ?? DEFAULT_SETTINGS.themes!)];\n\t}\n\n\tsetThemePaths(paths: string[]): void {\n\t\tthis.globalSettings.themes = paths;\n\t\tthis.markModified(\"themes\");\n\t\tthis.save();\n\t}\n\n\tsetProjectThemePaths(paths: string[]): void {\n\t\tconst projectSettings = structuredClone(this.projectSettings);\n\t\tprojectSettings.themes = paths;\n\t\tthis.markProjectModified(\"themes\");\n\t\tthis.saveProjectSettings(projectSettings);\n\t}\n\n\tgetEnableSkillCommands(): boolean {\n\t\treturn this.settings.enableSkillCommands ?? DEFAULT_SETTINGS.enableSkillCommands!;\n\t}\n\n\tsetEnableSkillCommands(enabled: boolean): void {\n\t\tthis.globalSettings.enableSkillCommands = enabled;\n\t\tthis.markModified(\"enableSkillCommands\");\n\t\tthis.save();\n\t}\n\n\tgetEnableSubagent(): boolean {\n\t\treturn this.settings.enableSubagent ?? DEFAULT_SETTINGS.enableSubagent!;\n\t}\n\n\tsetEnableSubagent(enabled: boolean): void {\n\t\tthis.globalSettings.enableSubagent = enabled;\n\t\tthis.markModified(\"enableSubagent\");\n\t\tthis.save();\n\t}\n\n\t/** Whether to dispatch eligible subagents on reused warm workers (experimental). */\n\tgetWarmSubagents(): boolean {\n\t\treturn this.settings.warmSubagents ?? DEFAULT_SETTINGS.warmSubagents!;\n\t}\n\n\tsetWarmSubagents(enabled: boolean): void {\n\t\tthis.globalSettings.warmSubagents = enabled;\n\t\tthis.markModified(\"warmSubagents\");\n\t\tthis.save();\n\t}\n\n\t/** Tree-wide subagent nesting cap. Clamped to >= 1 (delegation can never be disabled here). */\n\tgetMaxSubagentDepth(): number {\n\t\tconst v = this.settings.maxSubagentDepth ?? DEFAULT_SETTINGS.maxSubagentDepth!;\n\t\treturn Number.isFinite(v) && v >= 1 ? Math.floor(v) : 1;\n\t}\n\n\t/** Max concurrent subagents per pool at nesting depth >= 1. Clamped to >= 1. */\n\tgetNestedSubagentConcurrency(): number {\n\t\tconst v = this.settings.nestedSubagentConcurrency ?? DEFAULT_SETTINGS.nestedSubagentConcurrency!;\n\t\treturn Number.isFinite(v) && v >= 1 ? Math.floor(v) : 2;\n\t}\n\n\tgetEnableTodoWrite(): boolean {\n\t\treturn this.settings.enableTodoWrite ?? DEFAULT_SETTINGS.enableTodoWrite!;\n\t}\n\n\tsetEnableTodoWrite(enabled: boolean): void {\n\t\tthis.globalSettings.enableTodoWrite = enabled;\n\t\tthis.markModified(\"enableTodoWrite\");\n\t\tthis.save();\n\t}\n\n\t/** Raw `supportPlatform` tokens (string coerced to an array), or undefined when unset. */\n\tgetSupportPlatform(): string[] | undefined {\n\t\tconst value = this.settings.supportPlatform;\n\t\tif (value === undefined) return undefined;\n\t\treturn Array.isArray(value) ? value : [value];\n\t}\n\n\tgetEnablePluginTools(): boolean {\n\t\treturn this.settings.enablePluginTools ?? DEFAULT_SETTINGS.enablePluginTools!;\n\t}\n\n\tsetEnablePluginTools(enabled: boolean): void {\n\t\tthis.globalSettings.enablePluginTools = enabled;\n\t\tthis.markModified(\"enablePluginTools\");\n\t\tthis.save();\n\t}\n\n\tgetDeferMcpSchemas(): boolean {\n\t\treturn this.settings.deferMcpSchemas ?? DEFAULT_SETTINGS.deferMcpSchemas!;\n\t}\n\n\tsetDeferMcpSchemas(enabled: boolean): void {\n\t\tthis.globalSettings.deferMcpSchemas = enabled;\n\t\tthis.markModified(\"deferMcpSchemas\");\n\t\tthis.save();\n\t}\n\n\tgetEnableWebTools(): boolean {\n\t\treturn this.settings.enableWebTools ?? DEFAULT_SETTINGS.enableWebTools!;\n\t}\n\n\tsetEnableWebTools(enabled: boolean): void {\n\t\tthis.globalSettings.enableWebTools = enabled;\n\t\tthis.markModified(\"enableWebTools\");\n\t\tthis.save();\n\t}\n\n\tgetEnableEmbsearchTools(): boolean {\n\t\treturn this.settings.enableEmbsearchTools ?? DEFAULT_SETTINGS.enableEmbsearchTools!;\n\t}\n\n\tsetEnableEmbsearchTools(enabled: boolean): void {\n\t\tthis.globalSettings.enableEmbsearchTools = enabled;\n\t\tthis.markModified(\"enableEmbsearchTools\");\n\t\tthis.save();\n\t}\n\n\tgetEmbsearchBinaryPath(): string | undefined {\n\t\treturn this.settings.embsearchBinaryPath;\n\t}\n\n\tgetEmbsearchThresholdBytes(): number {\n\t\treturn this.settings.embsearchThresholdBytes ?? DEFAULT_SETTINGS.embsearchThresholdBytes!;\n\t}\n\n\tgetEnableBrowserTools(): boolean {\n\t\treturn this.settings.enableBrowserTools ?? DEFAULT_SETTINGS.enableBrowserTools!;\n\t}\n\n\tsetEnableBrowserTools(enabled: boolean): void {\n\t\tthis.globalSettings.enableBrowserTools = enabled;\n\t\tthis.markModified(\"enableBrowserTools\");\n\t\tthis.save();\n\t}\n\n\tgetEnableBrowserLivePreview(): boolean {\n\t\treturn this.settings.enableBrowserLivePreview ?? DEFAULT_SETTINGS.enableBrowserLivePreview!;\n\t}\n\n\tsetEnableBrowserLivePreview(enabled: boolean): void {\n\t\tthis.globalSettings.enableBrowserLivePreview = enabled;\n\t\tthis.markModified(\"enableBrowserLivePreview\");\n\t\tthis.save();\n\t}\n\n\tgetEnableFileTools(): boolean {\n\t\treturn this.settings.enableFileTools ?? DEFAULT_SETTINGS.enableFileTools!;\n\t}\n\n\tsetEnableFileTools(enabled: boolean): void {\n\t\tthis.globalSettings.enableFileTools = enabled;\n\t\tthis.markModified(\"enableFileTools\");\n\t\tthis.save();\n\t}\n\n\tgetLight(): boolean {\n\t\treturn this.settings.light ?? DEFAULT_SETTINGS.light!;\n\t}\n\n\tsetLight(enabled: boolean): void {\n\t\tthis.globalSettings.light = enabled;\n\t\tthis.markModified(\"light\");\n\t\tthis.save();\n\t}\n\n\tgetThinkingBudgets(): ThinkingBudgetsSettings | undefined {\n\t\treturn this.settings.thinkingBudgets;\n\t}\n\n\tgetThinkingDisplay(): \"summarized\" | \"omitted\" | undefined {\n\t\treturn this.settings.thinkingDisplay;\n\t}\n\n\tgetShowImages(): boolean {\n\t\treturn this.settings.terminal?.showImages ?? DEFAULT_SETTINGS.terminal!.showImages;\n\t}\n\n\tsetShowImages(show: boolean): void {\n\t\tif (!this.globalSettings.terminal) {\n\t\t\tthis.globalSettings.terminal = {};\n\t\t}\n\t\tthis.globalSettings.terminal.showImages = show;\n\t\tthis.markModified(\"terminal\", \"showImages\");\n\t\tthis.save();\n\t}\n\n\tgetImageWidthCells(): number {\n\t\tconst width = this.settings.terminal?.imageWidthCells ?? DEFAULT_SETTINGS.terminal!.imageWidthCells;\n\t\tif (typeof width !== \"number\" || !Number.isFinite(width)) {\n\t\t\treturn DEFAULT_SETTINGS.terminal!.imageWidthCells;\n\t\t}\n\t\treturn Math.max(1, Math.floor(width));\n\t}\n\n\tsetImageWidthCells(width: number): void {\n\t\tif (!this.globalSettings.terminal) {\n\t\t\tthis.globalSettings.terminal = {};\n\t\t}\n\t\tthis.globalSettings.terminal.imageWidthCells = Math.max(1, Math.floor(width));\n\t\tthis.markModified(\"terminal\", \"imageWidthCells\");\n\t\tthis.save();\n\t}\n\n\tgetClearOnShrink(): boolean {\n\t\t// Settings takes precedence, then env var, then default false\n\t\tif (this.settings.terminal?.clearOnShrink !== undefined) {\n\t\t\treturn this.settings.terminal.clearOnShrink;\n\t\t}\n\t\treturn process.env.HOOCODE_CLEAR_ON_SHRINK === \"1\";\n\t}\n\n\tsetClearOnShrink(enabled: boolean): void {\n\t\tif (!this.globalSettings.terminal) {\n\t\t\tthis.globalSettings.terminal = {};\n\t\t}\n\t\tthis.globalSettings.terminal.clearOnShrink = enabled;\n\t\tthis.markModified(\"terminal\", \"clearOnShrink\");\n\t\tthis.save();\n\t}\n\n\tgetShowTerminalProgress(): boolean {\n\t\treturn this.settings.terminal?.showTerminalProgress ?? DEFAULT_SETTINGS.terminal!.showTerminalProgress;\n\t}\n\n\tsetShowTerminalProgress(enabled: boolean): void {\n\t\tif (!this.globalSettings.terminal) {\n\t\t\tthis.globalSettings.terminal = {};\n\t\t}\n\t\tthis.globalSettings.terminal.showTerminalProgress = enabled;\n\t\tthis.markModified(\"terminal\", \"showTerminalProgress\");\n\t\tthis.save();\n\t}\n\n\tgetChimeOnTurnComplete(): boolean {\n\t\treturn this.settings.terminal?.chimeOnTurnComplete ?? DEFAULT_SETTINGS.terminal!.chimeOnTurnComplete;\n\t}\n\n\tsetChimeOnTurnComplete(enabled: boolean): void {\n\t\tif (!this.globalSettings.terminal) {\n\t\t\tthis.globalSettings.terminal = {};\n\t\t}\n\t\tthis.globalSettings.terminal.chimeOnTurnComplete = enabled;\n\t\tthis.markModified(\"terminal\", \"chimeOnTurnComplete\");\n\t\tthis.save();\n\t}\n\n\tgetImageAutoResize(): boolean {\n\t\treturn this.settings.images?.autoResize ?? DEFAULT_SETTINGS.images!.autoResize;\n\t}\n\n\tsetImageAutoResize(enabled: boolean): void {\n\t\tif (!this.globalSettings.images) {\n\t\t\tthis.globalSettings.images = {};\n\t\t}\n\t\tthis.globalSettings.images.autoResize = enabled;\n\t\tthis.markModified(\"images\", \"autoResize\");\n\t\tthis.save();\n\t}\n\n\tgetBlockImages(): boolean {\n\t\treturn this.settings.images?.blockImages ?? DEFAULT_SETTINGS.images!.blockImages;\n\t}\n\n\tsetBlockImages(blocked: boolean): void {\n\t\tif (!this.globalSettings.images) {\n\t\t\tthis.globalSettings.images = {};\n\t\t}\n\t\tthis.globalSettings.images.blockImages = blocked;\n\t\tthis.markModified(\"images\", \"blockImages\");\n\t\tthis.save();\n\t}\n\n\tgetEnabledModels(): string[] | undefined {\n\t\treturn this.settings.enabledModels;\n\t}\n\n\tsetEnabledModels(patterns: string[] | undefined): void {\n\t\tthis.globalSettings.enabledModels = patterns;\n\t\tthis.markModified(\"enabledModels\");\n\t\tthis.save();\n\t}\n\n\tgetDoubleEscapeAction(): \"fork\" | \"tree\" | \"none\" {\n\t\treturn this.settings.doubleEscapeAction ?? DEFAULT_SETTINGS.doubleEscapeAction!;\n\t}\n\n\tsetDoubleEscapeAction(action: \"fork\" | \"tree\" | \"none\"): void {\n\t\tthis.globalSettings.doubleEscapeAction = action;\n\t\tthis.markModified(\"doubleEscapeAction\");\n\t\tthis.save();\n\t}\n\n\tgetTreeFilterMode(): \"default\" | \"no-tools\" | \"user-only\" | \"labeled-only\" | \"all\" {\n\t\tconst mode = this.settings.treeFilterMode;\n\t\tconst valid = [\"default\", \"no-tools\", \"user-only\", \"labeled-only\", \"all\"];\n\t\treturn mode && valid.includes(mode) ? mode : DEFAULT_SETTINGS.treeFilterMode!;\n\t}\n\n\tsetTreeFilterMode(mode: \"default\" | \"no-tools\" | \"user-only\" | \"labeled-only\" | \"all\"): void {\n\t\tthis.globalSettings.treeFilterMode = mode;\n\t\tthis.markModified(\"treeFilterMode\");\n\t\tthis.save();\n\t}\n\n\tgetShowHardwareCursor(): boolean {\n\t\treturn this.settings.showHardwareCursor ?? process.env.HOOCODE_HARDWARE_CURSOR === \"1\";\n\t}\n\n\tsetShowHardwareCursor(enabled: boolean): void {\n\t\tthis.globalSettings.showHardwareCursor = enabled;\n\t\tthis.markModified(\"showHardwareCursor\");\n\t\tthis.save();\n\t}\n\n\tgetEditorPaddingX(): number {\n\t\treturn this.settings.editorPaddingX ?? DEFAULT_SETTINGS.editorPaddingX!;\n\t}\n\n\tsetEditorPaddingX(padding: number): void {\n\t\tthis.globalSettings.editorPaddingX = Math.max(0, Math.min(3, Math.floor(padding)));\n\t\tthis.markModified(\"editorPaddingX\");\n\t\tthis.save();\n\t}\n\n\tgetAutocompleteMaxVisible(): number {\n\t\treturn this.settings.autocompleteMaxVisible ?? DEFAULT_SETTINGS.autocompleteMaxVisible!;\n\t}\n\n\tsetAutocompleteMaxVisible(maxVisible: number): void {\n\t\tthis.globalSettings.autocompleteMaxVisible = Math.max(3, Math.min(20, Math.floor(maxVisible)));\n\t\tthis.markModified(\"autocompleteMaxVisible\");\n\t\tthis.save();\n\t}\n\n\tgetCodeBlockIndent(): string {\n\t\treturn this.settings.markdown?.codeBlockIndent ?? DEFAULT_SETTINGS.markdown!.codeBlockIndent;\n\t}\n\n\tgetWarnings(): WarningSettings {\n\t\treturn { ...DEFAULT_SETTINGS.warnings, ...this.settings.warnings };\n\t}\n\n\tsetWarnings(warnings: WarningSettings): void {\n\t\tthis.globalSettings.warnings = { ...warnings };\n\t\tthis.markModified(\"warnings\");\n\t\tthis.save();\n\t}\n}\n"]}
|
|
@@ -400,15 +400,77 @@ export class SettingsManager {
|
|
|
400
400
|
const v = this.settings.toolOutput?.maxBytes ?? DEFAULT_SETTINGS.toolOutput.maxBytes;
|
|
401
401
|
return Number.isFinite(v) && v >= 1024 ? Math.floor(v) : DEFAULT_SETTINGS.toolOutput.maxBytes;
|
|
402
402
|
}
|
|
403
|
+
setToolOutputMaxBytes(bytes) {
|
|
404
|
+
if (!this.globalSettings.toolOutput) {
|
|
405
|
+
this.globalSettings.toolOutput = {};
|
|
406
|
+
}
|
|
407
|
+
this.globalSettings.toolOutput.maxBytes = Math.max(1024, Math.floor(bytes));
|
|
408
|
+
this.markModified("toolOutput", "maxBytes");
|
|
409
|
+
this.save();
|
|
410
|
+
}
|
|
403
411
|
/** Line cap on a single read/bash tool result before truncation. Clamped to >= 1. */
|
|
404
412
|
getToolOutputMaxLines() {
|
|
405
413
|
const v = this.settings.toolOutput?.maxLines ?? DEFAULT_SETTINGS.toolOutput.maxLines;
|
|
406
414
|
return Number.isFinite(v) && v >= 1 ? Math.floor(v) : DEFAULT_SETTINGS.toolOutput.maxLines;
|
|
407
415
|
}
|
|
416
|
+
setToolOutputMaxLines(lines) {
|
|
417
|
+
if (!this.globalSettings.toolOutput) {
|
|
418
|
+
this.globalSettings.toolOutput = {};
|
|
419
|
+
}
|
|
420
|
+
this.globalSettings.toolOutput.maxLines = Math.max(1, Math.floor(lines));
|
|
421
|
+
this.markModified("toolOutput", "maxLines");
|
|
422
|
+
this.save();
|
|
423
|
+
}
|
|
408
424
|
/** Whether to stub superseded read results out of the outgoing context. */
|
|
409
425
|
getContextGcEnabled() {
|
|
410
426
|
return this.settings.contextGc?.enabled ?? DEFAULT_SETTINGS.contextGc.enabled;
|
|
411
427
|
}
|
|
428
|
+
setContextGcEnabled(enabled) {
|
|
429
|
+
if (!this.globalSettings.contextGc) {
|
|
430
|
+
this.globalSettings.contextGc = {};
|
|
431
|
+
}
|
|
432
|
+
this.globalSettings.contextGc.enabled = enabled;
|
|
433
|
+
this.markModified("contextGc", "enabled");
|
|
434
|
+
this.save();
|
|
435
|
+
}
|
|
436
|
+
/** Tool names disabled from the TUI; removed from the agent in every session. */
|
|
437
|
+
getDisabledTools() {
|
|
438
|
+
return [...(this.settings.disabledTools ?? DEFAULT_SETTINGS.disabledTools)];
|
|
439
|
+
}
|
|
440
|
+
setDisabledTools(names) {
|
|
441
|
+
this.globalSettings.disabledTools = [...names];
|
|
442
|
+
this.markModified("disabledTools");
|
|
443
|
+
this.save();
|
|
444
|
+
}
|
|
445
|
+
/** How tool result bodies render in the TUI (collapsed / peek / standard). */
|
|
446
|
+
getToolOutputDisplay() {
|
|
447
|
+
const v = this.settings.toolOutputDisplay;
|
|
448
|
+
return v === "collapsed" || v === "peek" || v === "standard" ? v : DEFAULT_SETTINGS.toolOutputDisplay;
|
|
449
|
+
}
|
|
450
|
+
setToolOutputDisplay(display) {
|
|
451
|
+
this.globalSettings.toolOutputDisplay = display;
|
|
452
|
+
this.markModified("toolOutputDisplay");
|
|
453
|
+
this.save();
|
|
454
|
+
}
|
|
455
|
+
/** Persisted overrides for extension-registered flags (name -> value). */
|
|
456
|
+
getFlagOverrides() {
|
|
457
|
+
return { ...(this.settings.flags ?? {}) };
|
|
458
|
+
}
|
|
459
|
+
setFlagOverride(name, value) {
|
|
460
|
+
if (!this.globalSettings.flags) {
|
|
461
|
+
this.globalSettings.flags = {};
|
|
462
|
+
}
|
|
463
|
+
this.globalSettings.flags[name] = value;
|
|
464
|
+
this.markModified("flags", name);
|
|
465
|
+
this.save();
|
|
466
|
+
}
|
|
467
|
+
clearFlagOverride(name) {
|
|
468
|
+
if (this.globalSettings.flags && name in this.globalSettings.flags) {
|
|
469
|
+
delete this.globalSettings.flags[name];
|
|
470
|
+
this.markModified("flags", name);
|
|
471
|
+
this.save();
|
|
472
|
+
}
|
|
473
|
+
}
|
|
412
474
|
getBranchSummarySettings() {
|
|
413
475
|
return { ...DEFAULT_SETTINGS.branchSummary, ...this.settings.branchSummary };
|
|
414
476
|
}
|