@oh-my-pi/pi-coding-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/CHANGELOG.md +1629 -0
  2. package/README.md +1041 -0
  3. package/docs/compaction.md +403 -0
  4. package/docs/config-usage.md +113 -0
  5. package/docs/custom-tools.md +541 -0
  6. package/docs/extension-loading.md +1004 -0
  7. package/docs/hooks.md +867 -0
  8. package/docs/rpc.md +1040 -0
  9. package/docs/sdk.md +994 -0
  10. package/docs/session-tree-plan.md +441 -0
  11. package/docs/session.md +240 -0
  12. package/docs/skills.md +290 -0
  13. package/docs/theme.md +670 -0
  14. package/docs/tree.md +197 -0
  15. package/docs/tui.md +341 -0
  16. package/examples/README.md +21 -0
  17. package/examples/custom-tools/README.md +124 -0
  18. package/examples/custom-tools/hello/index.ts +20 -0
  19. package/examples/custom-tools/question/index.ts +84 -0
  20. package/examples/custom-tools/subagent/README.md +172 -0
  21. package/examples/custom-tools/subagent/agents/planner.md +37 -0
  22. package/examples/custom-tools/subagent/agents/scout.md +50 -0
  23. package/examples/custom-tools/subagent/agents/worker.md +24 -0
  24. package/examples/custom-tools/subagent/agents.ts +156 -0
  25. package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
  26. package/examples/custom-tools/subagent/commands/implement.md +10 -0
  27. package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
  28. package/examples/custom-tools/subagent/index.ts +1002 -0
  29. package/examples/custom-tools/todo/index.ts +212 -0
  30. package/examples/hooks/README.md +56 -0
  31. package/examples/hooks/auto-commit-on-exit.ts +49 -0
  32. package/examples/hooks/confirm-destructive.ts +59 -0
  33. package/examples/hooks/custom-compaction.ts +116 -0
  34. package/examples/hooks/dirty-repo-guard.ts +52 -0
  35. package/examples/hooks/file-trigger.ts +41 -0
  36. package/examples/hooks/git-checkpoint.ts +53 -0
  37. package/examples/hooks/handoff.ts +150 -0
  38. package/examples/hooks/permission-gate.ts +34 -0
  39. package/examples/hooks/protected-paths.ts +30 -0
  40. package/examples/hooks/qna.ts +119 -0
  41. package/examples/hooks/snake.ts +343 -0
  42. package/examples/hooks/status-line.ts +40 -0
  43. package/examples/sdk/01-minimal.ts +22 -0
  44. package/examples/sdk/02-custom-model.ts +49 -0
  45. package/examples/sdk/03-custom-prompt.ts +44 -0
  46. package/examples/sdk/04-skills.ts +44 -0
  47. package/examples/sdk/05-tools.ts +90 -0
  48. package/examples/sdk/06-hooks.ts +61 -0
  49. package/examples/sdk/07-context-files.ts +36 -0
  50. package/examples/sdk/08-slash-commands.ts +42 -0
  51. package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
  52. package/examples/sdk/10-settings.ts +38 -0
  53. package/examples/sdk/11-sessions.ts +48 -0
  54. package/examples/sdk/12-full-control.ts +95 -0
  55. package/examples/sdk/README.md +154 -0
  56. package/package.json +89 -0
  57. package/src/bun-imports.d.ts +16 -0
  58. package/src/capability/context-file.ts +40 -0
  59. package/src/capability/extension.ts +48 -0
  60. package/src/capability/hook.ts +40 -0
  61. package/src/capability/index.ts +616 -0
  62. package/src/capability/instruction.ts +37 -0
  63. package/src/capability/mcp.ts +52 -0
  64. package/src/capability/prompt.ts +35 -0
  65. package/src/capability/rule.ts +56 -0
  66. package/src/capability/settings.ts +35 -0
  67. package/src/capability/skill.ts +49 -0
  68. package/src/capability/slash-command.ts +40 -0
  69. package/src/capability/system-prompt.ts +35 -0
  70. package/src/capability/tool.ts +38 -0
  71. package/src/capability/types.ts +166 -0
  72. package/src/cli/args.ts +259 -0
  73. package/src/cli/file-processor.ts +121 -0
  74. package/src/cli/list-models.ts +104 -0
  75. package/src/cli/plugin-cli.ts +661 -0
  76. package/src/cli/session-picker.ts +41 -0
  77. package/src/cli/update-cli.ts +274 -0
  78. package/src/cli.ts +10 -0
  79. package/src/config.ts +391 -0
  80. package/src/core/agent-session.ts +2178 -0
  81. package/src/core/auth-storage.ts +258 -0
  82. package/src/core/bash-executor.ts +197 -0
  83. package/src/core/compaction/branch-summarization.ts +315 -0
  84. package/src/core/compaction/compaction.ts +664 -0
  85. package/src/core/compaction/index.ts +7 -0
  86. package/src/core/compaction/utils.ts +153 -0
  87. package/src/core/custom-commands/bundled/review/index.ts +156 -0
  88. package/src/core/custom-commands/index.ts +15 -0
  89. package/src/core/custom-commands/loader.ts +226 -0
  90. package/src/core/custom-commands/types.ts +112 -0
  91. package/src/core/custom-tools/index.ts +22 -0
  92. package/src/core/custom-tools/loader.ts +248 -0
  93. package/src/core/custom-tools/types.ts +185 -0
  94. package/src/core/custom-tools/wrapper.ts +29 -0
  95. package/src/core/exec.ts +139 -0
  96. package/src/core/export-html/index.ts +159 -0
  97. package/src/core/export-html/template.css +774 -0
  98. package/src/core/export-html/template.generated.ts +2 -0
  99. package/src/core/export-html/template.html +45 -0
  100. package/src/core/export-html/template.js +1185 -0
  101. package/src/core/export-html/template.macro.ts +24 -0
  102. package/src/core/file-mentions.ts +54 -0
  103. package/src/core/hooks/index.ts +16 -0
  104. package/src/core/hooks/loader.ts +288 -0
  105. package/src/core/hooks/runner.ts +434 -0
  106. package/src/core/hooks/tool-wrapper.ts +98 -0
  107. package/src/core/hooks/types.ts +770 -0
  108. package/src/core/index.ts +53 -0
  109. package/src/core/logger.ts +112 -0
  110. package/src/core/mcp/client.ts +185 -0
  111. package/src/core/mcp/config.ts +248 -0
  112. package/src/core/mcp/index.ts +45 -0
  113. package/src/core/mcp/loader.ts +99 -0
  114. package/src/core/mcp/manager.ts +235 -0
  115. package/src/core/mcp/tool-bridge.ts +156 -0
  116. package/src/core/mcp/transports/http.ts +316 -0
  117. package/src/core/mcp/transports/index.ts +6 -0
  118. package/src/core/mcp/transports/stdio.ts +252 -0
  119. package/src/core/mcp/types.ts +228 -0
  120. package/src/core/messages.ts +211 -0
  121. package/src/core/model-registry.ts +334 -0
  122. package/src/core/model-resolver.ts +494 -0
  123. package/src/core/plugins/doctor.ts +67 -0
  124. package/src/core/plugins/index.ts +38 -0
  125. package/src/core/plugins/installer.ts +189 -0
  126. package/src/core/plugins/loader.ts +339 -0
  127. package/src/core/plugins/manager.ts +672 -0
  128. package/src/core/plugins/parser.ts +105 -0
  129. package/src/core/plugins/paths.ts +37 -0
  130. package/src/core/plugins/types.ts +190 -0
  131. package/src/core/sdk.ts +900 -0
  132. package/src/core/session-manager.ts +1837 -0
  133. package/src/core/settings-manager.ts +860 -0
  134. package/src/core/skills.ts +352 -0
  135. package/src/core/slash-commands.ts +132 -0
  136. package/src/core/system-prompt.ts +442 -0
  137. package/src/core/timings.ts +25 -0
  138. package/src/core/title-generator.ts +110 -0
  139. package/src/core/tools/ask.ts +193 -0
  140. package/src/core/tools/bash-interceptor.ts +120 -0
  141. package/src/core/tools/bash.ts +91 -0
  142. package/src/core/tools/context.ts +32 -0
  143. package/src/core/tools/edit-diff.ts +487 -0
  144. package/src/core/tools/edit.ts +140 -0
  145. package/src/core/tools/exa/company.ts +59 -0
  146. package/src/core/tools/exa/index.ts +63 -0
  147. package/src/core/tools/exa/linkedin.ts +59 -0
  148. package/src/core/tools/exa/mcp-client.ts +368 -0
  149. package/src/core/tools/exa/render.ts +200 -0
  150. package/src/core/tools/exa/researcher.ts +90 -0
  151. package/src/core/tools/exa/search.ts +338 -0
  152. package/src/core/tools/exa/types.ts +167 -0
  153. package/src/core/tools/exa/websets.ts +248 -0
  154. package/src/core/tools/find.ts +244 -0
  155. package/src/core/tools/grep.ts +584 -0
  156. package/src/core/tools/index.ts +283 -0
  157. package/src/core/tools/ls.ts +142 -0
  158. package/src/core/tools/lsp/client.ts +767 -0
  159. package/src/core/tools/lsp/clients/biome-client.ts +207 -0
  160. package/src/core/tools/lsp/clients/index.ts +49 -0
  161. package/src/core/tools/lsp/clients/lsp-linter-client.ts +98 -0
  162. package/src/core/tools/lsp/config.ts +845 -0
  163. package/src/core/tools/lsp/edits.ts +110 -0
  164. package/src/core/tools/lsp/index.ts +1364 -0
  165. package/src/core/tools/lsp/render.ts +560 -0
  166. package/src/core/tools/lsp/rust-analyzer.ts +145 -0
  167. package/src/core/tools/lsp/types.ts +495 -0
  168. package/src/core/tools/lsp/utils.ts +526 -0
  169. package/src/core/tools/notebook.ts +182 -0
  170. package/src/core/tools/output.ts +198 -0
  171. package/src/core/tools/path-utils.ts +61 -0
  172. package/src/core/tools/read.ts +507 -0
  173. package/src/core/tools/renderers.ts +820 -0
  174. package/src/core/tools/review.ts +275 -0
  175. package/src/core/tools/rulebook.ts +124 -0
  176. package/src/core/tools/task/agents.ts +158 -0
  177. package/src/core/tools/task/artifacts.ts +114 -0
  178. package/src/core/tools/task/commands.ts +157 -0
  179. package/src/core/tools/task/discovery.ts +217 -0
  180. package/src/core/tools/task/executor.ts +531 -0
  181. package/src/core/tools/task/index.ts +548 -0
  182. package/src/core/tools/task/model-resolver.ts +176 -0
  183. package/src/core/tools/task/parallel.ts +38 -0
  184. package/src/core/tools/task/render.ts +502 -0
  185. package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
  186. package/src/core/tools/task/types.ts +142 -0
  187. package/src/core/tools/truncate.ts +265 -0
  188. package/src/core/tools/web-fetch.ts +2511 -0
  189. package/src/core/tools/web-search/auth.ts +199 -0
  190. package/src/core/tools/web-search/index.ts +583 -0
  191. package/src/core/tools/web-search/providers/anthropic.ts +198 -0
  192. package/src/core/tools/web-search/providers/exa.ts +196 -0
  193. package/src/core/tools/web-search/providers/perplexity.ts +195 -0
  194. package/src/core/tools/web-search/render.ts +372 -0
  195. package/src/core/tools/web-search/types.ts +180 -0
  196. package/src/core/tools/write.ts +63 -0
  197. package/src/core/ttsr.ts +211 -0
  198. package/src/core/utils.ts +187 -0
  199. package/src/discovery/agents-md.ts +75 -0
  200. package/src/discovery/builtin.ts +647 -0
  201. package/src/discovery/claude.ts +623 -0
  202. package/src/discovery/cline.ts +104 -0
  203. package/src/discovery/codex.ts +571 -0
  204. package/src/discovery/cursor.ts +266 -0
  205. package/src/discovery/gemini.ts +368 -0
  206. package/src/discovery/github.ts +120 -0
  207. package/src/discovery/helpers.test.ts +127 -0
  208. package/src/discovery/helpers.ts +249 -0
  209. package/src/discovery/index.ts +84 -0
  210. package/src/discovery/mcp-json.ts +127 -0
  211. package/src/discovery/vscode.ts +99 -0
  212. package/src/discovery/windsurf.ts +219 -0
  213. package/src/index.ts +192 -0
  214. package/src/main.ts +507 -0
  215. package/src/migrations.ts +156 -0
  216. package/src/modes/cleanup.ts +23 -0
  217. package/src/modes/index.ts +48 -0
  218. package/src/modes/interactive/components/armin.ts +382 -0
  219. package/src/modes/interactive/components/assistant-message.ts +86 -0
  220. package/src/modes/interactive/components/bash-execution.ts +199 -0
  221. package/src/modes/interactive/components/bordered-loader.ts +41 -0
  222. package/src/modes/interactive/components/branch-summary-message.ts +42 -0
  223. package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
  224. package/src/modes/interactive/components/custom-editor.ts +122 -0
  225. package/src/modes/interactive/components/diff.ts +147 -0
  226. package/src/modes/interactive/components/dynamic-border.ts +25 -0
  227. package/src/modes/interactive/components/extensions/extension-dashboard.ts +296 -0
  228. package/src/modes/interactive/components/extensions/extension-list.ts +479 -0
  229. package/src/modes/interactive/components/extensions/index.ts +9 -0
  230. package/src/modes/interactive/components/extensions/inspector-panel.ts +313 -0
  231. package/src/modes/interactive/components/extensions/state-manager.ts +558 -0
  232. package/src/modes/interactive/components/extensions/types.ts +191 -0
  233. package/src/modes/interactive/components/hook-editor.ts +117 -0
  234. package/src/modes/interactive/components/hook-input.ts +64 -0
  235. package/src/modes/interactive/components/hook-message.ts +96 -0
  236. package/src/modes/interactive/components/hook-selector.ts +91 -0
  237. package/src/modes/interactive/components/model-selector.ts +560 -0
  238. package/src/modes/interactive/components/oauth-selector.ts +136 -0
  239. package/src/modes/interactive/components/plugin-settings.ts +481 -0
  240. package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
  241. package/src/modes/interactive/components/session-selector.ts +220 -0
  242. package/src/modes/interactive/components/settings-defs.ts +597 -0
  243. package/src/modes/interactive/components/settings-selector.ts +545 -0
  244. package/src/modes/interactive/components/show-images-selector.ts +45 -0
  245. package/src/modes/interactive/components/status-line/index.ts +4 -0
  246. package/src/modes/interactive/components/status-line/presets.ts +94 -0
  247. package/src/modes/interactive/components/status-line/segments.ts +350 -0
  248. package/src/modes/interactive/components/status-line/separators.ts +55 -0
  249. package/src/modes/interactive/components/status-line/types.ts +81 -0
  250. package/src/modes/interactive/components/status-line-segment-editor.ts +357 -0
  251. package/src/modes/interactive/components/status-line.ts +384 -0
  252. package/src/modes/interactive/components/theme-selector.ts +62 -0
  253. package/src/modes/interactive/components/thinking-selector.ts +64 -0
  254. package/src/modes/interactive/components/tool-execution.ts +946 -0
  255. package/src/modes/interactive/components/tree-selector.ts +877 -0
  256. package/src/modes/interactive/components/ttsr-notification.ts +82 -0
  257. package/src/modes/interactive/components/user-message-selector.ts +159 -0
  258. package/src/modes/interactive/components/user-message.ts +18 -0
  259. package/src/modes/interactive/components/visual-truncate.ts +50 -0
  260. package/src/modes/interactive/components/welcome.ts +228 -0
  261. package/src/modes/interactive/interactive-mode.ts +2669 -0
  262. package/src/modes/interactive/theme/dark.json +102 -0
  263. package/src/modes/interactive/theme/defaults/dark-arctic.json +111 -0
  264. package/src/modes/interactive/theme/defaults/dark-catppuccin.json +106 -0
  265. package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +109 -0
  266. package/src/modes/interactive/theme/defaults/dark-dracula.json +105 -0
  267. package/src/modes/interactive/theme/defaults/dark-forest.json +103 -0
  268. package/src/modes/interactive/theme/defaults/dark-github.json +112 -0
  269. package/src/modes/interactive/theme/defaults/dark-gruvbox.json +119 -0
  270. package/src/modes/interactive/theme/defaults/dark-monochrome.json +101 -0
  271. package/src/modes/interactive/theme/defaults/dark-monokai.json +105 -0
  272. package/src/modes/interactive/theme/defaults/dark-nord.json +104 -0
  273. package/src/modes/interactive/theme/defaults/dark-ocean.json +108 -0
  274. package/src/modes/interactive/theme/defaults/dark-one.json +107 -0
  275. package/src/modes/interactive/theme/defaults/dark-retro.json +99 -0
  276. package/src/modes/interactive/theme/defaults/dark-rose-pine.json +95 -0
  277. package/src/modes/interactive/theme/defaults/dark-solarized.json +96 -0
  278. package/src/modes/interactive/theme/defaults/dark-sunset.json +106 -0
  279. package/src/modes/interactive/theme/defaults/dark-synthwave.json +102 -0
  280. package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +108 -0
  281. package/src/modes/interactive/theme/defaults/index.ts +67 -0
  282. package/src/modes/interactive/theme/defaults/light-arctic.json +106 -0
  283. package/src/modes/interactive/theme/defaults/light-catppuccin.json +105 -0
  284. package/src/modes/interactive/theme/defaults/light-cyberpunk.json +103 -0
  285. package/src/modes/interactive/theme/defaults/light-forest.json +107 -0
  286. package/src/modes/interactive/theme/defaults/light-github.json +114 -0
  287. package/src/modes/interactive/theme/defaults/light-gruvbox.json +115 -0
  288. package/src/modes/interactive/theme/defaults/light-monochrome.json +100 -0
  289. package/src/modes/interactive/theme/defaults/light-ocean.json +106 -0
  290. package/src/modes/interactive/theme/defaults/light-one.json +105 -0
  291. package/src/modes/interactive/theme/defaults/light-retro.json +105 -0
  292. package/src/modes/interactive/theme/defaults/light-solarized.json +101 -0
  293. package/src/modes/interactive/theme/defaults/light-sunset.json +106 -0
  294. package/src/modes/interactive/theme/defaults/light-synthwave.json +105 -0
  295. package/src/modes/interactive/theme/defaults/light-tokyo-night.json +118 -0
  296. package/src/modes/interactive/theme/light.json +99 -0
  297. package/src/modes/interactive/theme/theme-schema.json +424 -0
  298. package/src/modes/interactive/theme/theme.ts +2211 -0
  299. package/src/modes/print-mode.ts +163 -0
  300. package/src/modes/rpc/rpc-client.ts +527 -0
  301. package/src/modes/rpc/rpc-mode.ts +494 -0
  302. package/src/modes/rpc/rpc-types.ts +203 -0
  303. package/src/prompts/architect-plan.md +10 -0
  304. package/src/prompts/branch-summary-preamble.md +3 -0
  305. package/src/prompts/branch-summary.md +28 -0
  306. package/src/prompts/browser.md +71 -0
  307. package/src/prompts/compaction-summary.md +34 -0
  308. package/src/prompts/compaction-turn-prefix.md +16 -0
  309. package/src/prompts/compaction-update-summary.md +41 -0
  310. package/src/prompts/explore.md +82 -0
  311. package/src/prompts/implement-with-critic.md +11 -0
  312. package/src/prompts/implement.md +11 -0
  313. package/src/prompts/init.md +30 -0
  314. package/src/prompts/plan.md +54 -0
  315. package/src/prompts/reviewer.md +81 -0
  316. package/src/prompts/summarization-system.md +3 -0
  317. package/src/prompts/system-prompt.md +27 -0
  318. package/src/prompts/task.md +56 -0
  319. package/src/prompts/title-system.md +8 -0
  320. package/src/prompts/tools/ask.md +24 -0
  321. package/src/prompts/tools/bash.md +23 -0
  322. package/src/prompts/tools/edit.md +9 -0
  323. package/src/prompts/tools/find.md +6 -0
  324. package/src/prompts/tools/grep.md +12 -0
  325. package/src/prompts/tools/lsp.md +14 -0
  326. package/src/prompts/tools/output.md +23 -0
  327. package/src/prompts/tools/read.md +25 -0
  328. package/src/prompts/tools/web-fetch.md +8 -0
  329. package/src/prompts/tools/web-search.md +10 -0
  330. package/src/prompts/tools/write.md +10 -0
  331. package/src/utils/changelog.ts +99 -0
  332. package/src/utils/clipboard.ts +265 -0
  333. package/src/utils/fuzzy.ts +108 -0
  334. package/src/utils/mime.ts +30 -0
  335. package/src/utils/shell-snapshot.ts +218 -0
  336. package/src/utils/shell.ts +364 -0
  337. package/src/utils/tools-manager.ts +265 -0
@@ -0,0 +1,860 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { type Settings as SettingsItem, settingsCapability } from "../capability/settings";
4
+ import { getAgentDir } from "../config";
5
+ import { loadSync } from "../discovery";
6
+ import type { SymbolPreset } from "../modes/interactive/theme/theme";
7
+
8
+ export interface CompactionSettings {
9
+ enabled?: boolean; // default: true
10
+ reserveTokens?: number; // default: 16384
11
+ keepRecentTokens?: number; // default: 20000
12
+ }
13
+
14
+ export interface BranchSummarySettings {
15
+ reserveTokens?: number; // default: 16384 (tokens reserved for prompt + LLM response)
16
+ }
17
+
18
+ export interface RetrySettings {
19
+ enabled?: boolean; // default: true
20
+ maxRetries?: number; // default: 3
21
+ baseDelayMs?: number; // default: 2000 (exponential backoff: 2s, 4s, 8s)
22
+ }
23
+
24
+ export interface SkillsSettings {
25
+ enabled?: boolean; // default: true
26
+ enableCodexUser?: boolean; // default: true
27
+ enableClaudeUser?: boolean; // default: true
28
+ enableClaudeProject?: boolean; // default: true
29
+ enablePiUser?: boolean; // default: true
30
+ enablePiProject?: boolean; // default: true
31
+ customDirectories?: string[]; // default: []
32
+ ignoredSkills?: string[]; // default: [] (glob patterns to exclude; takes precedence over includeSkills)
33
+ includeSkills?: string[]; // default: [] (empty = include all; glob patterns to filter)
34
+ }
35
+
36
+ export interface CommandsSettings {
37
+ enableClaudeUser?: boolean; // default: true (load from ~/.claude/commands/)
38
+ enableClaudeProject?: boolean; // default: true (load from .claude/commands/)
39
+ }
40
+
41
+ export interface TerminalSettings {
42
+ showImages?: boolean; // default: true (only relevant if terminal supports images)
43
+ }
44
+
45
+ export interface ExaSettings {
46
+ enabled?: boolean; // default: true (master toggle for all Exa tools)
47
+ enableSearch?: boolean; // default: true (search, deep, code, crawl)
48
+ enableLinkedin?: boolean; // default: false
49
+ enableCompany?: boolean; // default: false
50
+ enableResearcher?: boolean; // default: false
51
+ enableWebsets?: boolean; // default: false
52
+ }
53
+
54
+ export interface BashInterceptorSettings {
55
+ enabled?: boolean; // default: false (blocks shell commands that have dedicated tools)
56
+ }
57
+
58
+ export interface MCPSettings {
59
+ enableProjectConfig?: boolean; // default: true (load .mcp.json from project root)
60
+ }
61
+
62
+ export interface LspSettings {
63
+ formatOnWrite?: boolean; // default: false (format files using LSP after write tool writes code files)
64
+ diagnosticsOnWrite?: boolean; // default: true (return LSP diagnostics after write tool writes code files)
65
+ diagnosticsOnEdit?: boolean; // default: false (return LSP diagnostics after edit tool edits code files)
66
+ }
67
+
68
+ export interface EditSettings {
69
+ fuzzyMatch?: boolean; // default: true (accept high-confidence fuzzy matches for whitespace/indentation)
70
+ }
71
+
72
+ export type { SymbolPreset };
73
+
74
+ export interface TtsrSettings {
75
+ enabled?: boolean; // default: true
76
+ /** What to do with partial output when TTSR triggers: "keep" shows interrupted attempt, "discard" removes it */
77
+ contextMode?: "keep" | "discard"; // default: "discard"
78
+ /** How TTSR rules repeat: "once" = only trigger once per session, "after-gap" = can repeat after N messages */
79
+ repeatMode?: "once" | "after-gap"; // default: "once"
80
+ /** Number of messages before a rule can trigger again (only used when repeatMode is "after-gap") */
81
+ repeatGap?: number; // default: 10
82
+ }
83
+
84
+ export type StatusLineSegmentId =
85
+ | "pi"
86
+ | "model"
87
+ | "path"
88
+ | "git"
89
+ | "subagents"
90
+ | "token_in"
91
+ | "token_out"
92
+ | "token_total"
93
+ | "cost"
94
+ | "context_pct"
95
+ | "context_total"
96
+ | "time_spent"
97
+ | "time"
98
+ | "session"
99
+ | "hostname"
100
+ | "cache_read"
101
+ | "cache_write";
102
+
103
+ export type StatusLineSeparatorStyle = "powerline" | "powerline-thin" | "slash" | "pipe" | "block" | "none" | "ascii";
104
+
105
+ export type StatusLinePreset = "default" | "minimal" | "compact" | "full" | "nerd" | "ascii" | "custom";
106
+
107
+ export interface StatusLineSegmentOptions {
108
+ model?: { showThinkingLevel?: boolean };
109
+ path?: { abbreviate?: boolean; maxLength?: number; stripWorkPrefix?: boolean };
110
+ git?: { showBranch?: boolean; showStaged?: boolean; showUnstaged?: boolean; showUntracked?: boolean };
111
+ time?: { format?: "12h" | "24h"; showSeconds?: boolean };
112
+ }
113
+
114
+ export interface StatusLineSettings {
115
+ preset?: StatusLinePreset;
116
+ leftSegments?: StatusLineSegmentId[];
117
+ rightSegments?: StatusLineSegmentId[];
118
+ separator?: StatusLineSeparatorStyle;
119
+ segmentOptions?: StatusLineSegmentOptions;
120
+ showHookStatus?: boolean;
121
+ }
122
+
123
+ export interface Settings {
124
+ lastChangelogVersion?: string;
125
+ /** Model roles map: { default: "provider/modelId", small: "provider/modelId", ... } */
126
+ modelRoles?: Record<string, string>;
127
+ defaultThinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
128
+ queueMode?: "all" | "one-at-a-time";
129
+ interruptMode?: "immediate" | "wait";
130
+ theme?: string;
131
+ symbolPreset?: SymbolPreset; // default: uses theme's preset or "unicode"
132
+ compaction?: CompactionSettings;
133
+ branchSummary?: BranchSummarySettings;
134
+ retry?: RetrySettings;
135
+ hideThinkingBlock?: boolean;
136
+ shellPath?: string; // Custom shell path (e.g., for Cygwin users on Windows)
137
+ collapseChangelog?: boolean; // Show condensed changelog after update (use /changelog for full)
138
+ hooks?: string[]; // Array of hook file paths
139
+ customTools?: string[]; // Array of custom tool file paths
140
+ skills?: SkillsSettings;
141
+ commands?: CommandsSettings;
142
+ terminal?: TerminalSettings;
143
+ enabledModels?: string[]; // Model patterns for cycling (same format as --models CLI flag)
144
+ exa?: ExaSettings;
145
+ bashInterceptor?: BashInterceptorSettings;
146
+ mcp?: MCPSettings;
147
+ lsp?: LspSettings;
148
+ edit?: EditSettings;
149
+ ttsr?: TtsrSettings;
150
+ disabledProviders?: string[]; // Discovery provider IDs that are disabled
151
+ disabledExtensions?: string[]; // Individual extension IDs that are disabled (e.g., "skill:commit")
152
+ statusLine?: StatusLineSettings; // Status line configuration
153
+ }
154
+
155
+ /** Deep merge settings: project/overrides take precedence, nested objects merge recursively */
156
+ function deepMergeSettings(base: Settings, overrides: Settings): Settings {
157
+ const result: Settings = { ...base };
158
+
159
+ for (const key of Object.keys(overrides) as (keyof Settings)[]) {
160
+ const overrideValue = overrides[key];
161
+ const baseValue = base[key];
162
+
163
+ if (overrideValue === undefined) {
164
+ continue;
165
+ }
166
+
167
+ // For nested objects, merge recursively
168
+ if (
169
+ typeof overrideValue === "object" &&
170
+ overrideValue !== null &&
171
+ !Array.isArray(overrideValue) &&
172
+ typeof baseValue === "object" &&
173
+ baseValue !== null &&
174
+ !Array.isArray(baseValue)
175
+ ) {
176
+ (result as Record<string, unknown>)[key] = { ...baseValue, ...overrideValue };
177
+ } else {
178
+ // For primitives and arrays, override value wins
179
+ (result as Record<string, unknown>)[key] = overrideValue;
180
+ }
181
+ }
182
+
183
+ return result;
184
+ }
185
+
186
+ export class SettingsManager {
187
+ private settingsPath: string | null;
188
+ private cwd: string | null;
189
+ private globalSettings: Settings;
190
+ private settings: Settings;
191
+ private persist: boolean;
192
+
193
+ private constructor(settingsPath: string | null, cwd: string | null, initialSettings: Settings, persist: boolean) {
194
+ this.settingsPath = settingsPath;
195
+ this.cwd = cwd;
196
+ this.persist = persist;
197
+ this.globalSettings = initialSettings;
198
+ const projectSettings = this.loadProjectSettings();
199
+ this.settings = deepMergeSettings(this.globalSettings, projectSettings);
200
+ }
201
+
202
+ /** Create a SettingsManager that loads from files */
203
+ static create(cwd: string = process.cwd(), agentDir: string = getAgentDir()): SettingsManager {
204
+ const settingsPath = join(agentDir, "settings.json");
205
+
206
+ // Use capability API to load user-level settings from all providers
207
+ const result = loadSync(settingsCapability.id, { cwd });
208
+
209
+ // Merge all user-level settings
210
+ let globalSettings: Settings = {};
211
+ for (const item of result.items as SettingsItem[]) {
212
+ if (item.level === "user") {
213
+ globalSettings = deepMergeSettings(globalSettings, item.data as Settings);
214
+ }
215
+ }
216
+
217
+ // Also load from agentDir for backward compatibility (if not covered by providers)
218
+ const legacySettings = SettingsManager.loadFromFile(settingsPath);
219
+ globalSettings = deepMergeSettings(globalSettings, legacySettings);
220
+
221
+ return new SettingsManager(settingsPath, cwd, globalSettings, true);
222
+ }
223
+
224
+ /** Create an in-memory SettingsManager (no file I/O) */
225
+ static inMemory(settings: Partial<Settings> = {}): SettingsManager {
226
+ return new SettingsManager(null, null, settings, false);
227
+ }
228
+
229
+ private static loadFromFile(path: string): Settings {
230
+ if (!existsSync(path)) {
231
+ return {};
232
+ }
233
+ try {
234
+ const content = readFileSync(path, "utf-8");
235
+ return JSON.parse(content);
236
+ } catch (error) {
237
+ console.error(`Warning: Could not read settings file ${path}: ${error}`);
238
+ return {};
239
+ }
240
+ }
241
+
242
+ private loadProjectSettings(): Settings {
243
+ if (!this.cwd) return {};
244
+
245
+ // Use capability API to discover settings from all providers
246
+ const result = loadSync(settingsCapability.id, { cwd: this.cwd });
247
+
248
+ // Merge only project-level settings (user-level settings are handled separately via globalSettings)
249
+ let merged: Settings = {};
250
+ for (const item of result.items as SettingsItem[]) {
251
+ if (item.level === "project") {
252
+ merged = deepMergeSettings(merged, item.data as Settings);
253
+ }
254
+ }
255
+
256
+ return merged;
257
+ }
258
+
259
+ /** Apply additional overrides on top of current settings */
260
+ applyOverrides(overrides: Partial<Settings>): void {
261
+ this.settings = deepMergeSettings(this.settings, overrides);
262
+ }
263
+
264
+ private save(): void {
265
+ if (!this.persist || !this.settingsPath) return;
266
+
267
+ try {
268
+ const dir = dirname(this.settingsPath);
269
+ if (!existsSync(dir)) {
270
+ mkdirSync(dir, { recursive: true });
271
+ }
272
+
273
+ // Save only global settings (project settings are read-only)
274
+ writeFileSync(this.settingsPath, JSON.stringify(this.globalSettings, null, 2), "utf-8");
275
+
276
+ // Re-merge project settings into active settings
277
+ const projectSettings = this.loadProjectSettings();
278
+ this.settings = deepMergeSettings(this.globalSettings, projectSettings);
279
+ } catch (error) {
280
+ console.error(`Warning: Could not save settings file: ${error}`);
281
+ }
282
+ }
283
+
284
+ getLastChangelogVersion(): string | undefined {
285
+ return this.settings.lastChangelogVersion;
286
+ }
287
+
288
+ setLastChangelogVersion(version: string): void {
289
+ this.globalSettings.lastChangelogVersion = version;
290
+ this.save();
291
+ }
292
+
293
+ /**
294
+ * Get model for a role. Returns "provider/modelId" string or undefined.
295
+ */
296
+ getModelRole(role: string): string | undefined {
297
+ return this.settings.modelRoles?.[role];
298
+ }
299
+
300
+ /**
301
+ * Set model for a role. Model should be "provider/modelId" format.
302
+ */
303
+ setModelRole(role: string, model: string): void {
304
+ if (!this.globalSettings.modelRoles) {
305
+ this.globalSettings.modelRoles = {};
306
+ }
307
+ this.globalSettings.modelRoles[role] = model;
308
+ this.save();
309
+ }
310
+
311
+ /**
312
+ * Get all model roles.
313
+ */
314
+ getModelRoles(): Record<string, string> {
315
+ return { ...this.settings.modelRoles };
316
+ }
317
+
318
+ getQueueMode(): "all" | "one-at-a-time" {
319
+ return this.settings.queueMode || "one-at-a-time";
320
+ }
321
+
322
+ setQueueMode(mode: "all" | "one-at-a-time"): void {
323
+ this.globalSettings.queueMode = mode;
324
+ this.save();
325
+ }
326
+
327
+ getInterruptMode(): "immediate" | "wait" {
328
+ return this.settings.interruptMode || "immediate";
329
+ }
330
+
331
+ setInterruptMode(mode: "immediate" | "wait"): void {
332
+ this.globalSettings.interruptMode = mode;
333
+ this.save();
334
+ }
335
+
336
+ getTheme(): string | undefined {
337
+ return this.settings.theme;
338
+ }
339
+
340
+ setTheme(theme: string): void {
341
+ this.globalSettings.theme = theme;
342
+ this.save();
343
+ }
344
+
345
+ getSymbolPreset(): SymbolPreset | undefined {
346
+ return this.settings.symbolPreset;
347
+ }
348
+
349
+ setSymbolPreset(preset: SymbolPreset): void {
350
+ this.globalSettings.symbolPreset = preset;
351
+ this.save();
352
+ }
353
+
354
+ getDefaultThinkingLevel(): "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | undefined {
355
+ return this.settings.defaultThinkingLevel;
356
+ }
357
+
358
+ setDefaultThinkingLevel(level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"): void {
359
+ this.globalSettings.defaultThinkingLevel = level;
360
+ this.save();
361
+ }
362
+
363
+ getCompactionEnabled(): boolean {
364
+ return this.settings.compaction?.enabled ?? true;
365
+ }
366
+
367
+ setCompactionEnabled(enabled: boolean): void {
368
+ if (!this.globalSettings.compaction) {
369
+ this.globalSettings.compaction = {};
370
+ }
371
+ this.globalSettings.compaction.enabled = enabled;
372
+ this.save();
373
+ }
374
+
375
+ getCompactionReserveTokens(): number {
376
+ return this.settings.compaction?.reserveTokens ?? 16384;
377
+ }
378
+
379
+ getCompactionKeepRecentTokens(): number {
380
+ return this.settings.compaction?.keepRecentTokens ?? 20000;
381
+ }
382
+
383
+ getCompactionSettings(): { enabled: boolean; reserveTokens: number; keepRecentTokens: number } {
384
+ return {
385
+ enabled: this.getCompactionEnabled(),
386
+ reserveTokens: this.getCompactionReserveTokens(),
387
+ keepRecentTokens: this.getCompactionKeepRecentTokens(),
388
+ };
389
+ }
390
+
391
+ getBranchSummarySettings(): { reserveTokens: number } {
392
+ return {
393
+ reserveTokens: this.settings.branchSummary?.reserveTokens ?? 16384,
394
+ };
395
+ }
396
+
397
+ getRetryEnabled(): boolean {
398
+ return this.settings.retry?.enabled ?? true;
399
+ }
400
+
401
+ setRetryEnabled(enabled: boolean): void {
402
+ if (!this.globalSettings.retry) {
403
+ this.globalSettings.retry = {};
404
+ }
405
+ this.globalSettings.retry.enabled = enabled;
406
+ this.save();
407
+ }
408
+
409
+ getRetrySettings(): { enabled: boolean; maxRetries: number; baseDelayMs: number } {
410
+ return {
411
+ enabled: this.getRetryEnabled(),
412
+ maxRetries: this.settings.retry?.maxRetries ?? 3,
413
+ baseDelayMs: this.settings.retry?.baseDelayMs ?? 2000,
414
+ };
415
+ }
416
+
417
+ getHideThinkingBlock(): boolean {
418
+ return this.settings.hideThinkingBlock ?? false;
419
+ }
420
+
421
+ setHideThinkingBlock(hide: boolean): void {
422
+ this.globalSettings.hideThinkingBlock = hide;
423
+ this.save();
424
+ }
425
+
426
+ getShellPath(): string | undefined {
427
+ return this.settings.shellPath;
428
+ }
429
+
430
+ setShellPath(path: string | undefined): void {
431
+ this.globalSettings.shellPath = path;
432
+ this.save();
433
+ }
434
+
435
+ getCollapseChangelog(): boolean {
436
+ return this.settings.collapseChangelog ?? false;
437
+ }
438
+
439
+ setCollapseChangelog(collapse: boolean): void {
440
+ this.globalSettings.collapseChangelog = collapse;
441
+ this.save();
442
+ }
443
+
444
+ getHookPaths(): string[] {
445
+ return [...(this.settings.hooks ?? [])];
446
+ }
447
+
448
+ setHookPaths(paths: string[]): void {
449
+ this.globalSettings.hooks = paths;
450
+ this.save();
451
+ }
452
+
453
+ getCustomToolPaths(): string[] {
454
+ return [...(this.settings.customTools ?? [])];
455
+ }
456
+
457
+ setCustomToolPaths(paths: string[]): void {
458
+ this.globalSettings.customTools = paths;
459
+ this.save();
460
+ }
461
+
462
+ getSkillsEnabled(): boolean {
463
+ return this.settings.skills?.enabled ?? true;
464
+ }
465
+
466
+ setSkillsEnabled(enabled: boolean): void {
467
+ if (!this.globalSettings.skills) {
468
+ this.globalSettings.skills = {};
469
+ }
470
+ this.globalSettings.skills.enabled = enabled;
471
+ this.save();
472
+ }
473
+
474
+ getSkillsSettings(): Required<SkillsSettings> {
475
+ return {
476
+ enabled: this.settings.skills?.enabled ?? true,
477
+ enableCodexUser: this.settings.skills?.enableCodexUser ?? true,
478
+ enableClaudeUser: this.settings.skills?.enableClaudeUser ?? true,
479
+ enableClaudeProject: this.settings.skills?.enableClaudeProject ?? true,
480
+ enablePiUser: this.settings.skills?.enablePiUser ?? true,
481
+ enablePiProject: this.settings.skills?.enablePiProject ?? true,
482
+ customDirectories: [...(this.settings.skills?.customDirectories ?? [])],
483
+ ignoredSkills: [...(this.settings.skills?.ignoredSkills ?? [])],
484
+ includeSkills: [...(this.settings.skills?.includeSkills ?? [])],
485
+ };
486
+ }
487
+
488
+ getCommandsSettings(): Required<CommandsSettings> {
489
+ return {
490
+ enableClaudeUser: this.settings.commands?.enableClaudeUser ?? true,
491
+ enableClaudeProject: this.settings.commands?.enableClaudeProject ?? true,
492
+ };
493
+ }
494
+
495
+ getShowImages(): boolean {
496
+ return this.settings.terminal?.showImages ?? true;
497
+ }
498
+
499
+ setShowImages(show: boolean): void {
500
+ if (!this.globalSettings.terminal) {
501
+ this.globalSettings.terminal = {};
502
+ }
503
+ this.globalSettings.terminal.showImages = show;
504
+ this.save();
505
+ }
506
+
507
+ getEnabledModels(): string[] | undefined {
508
+ return this.settings.enabledModels;
509
+ }
510
+
511
+ getExaSettings(): Required<ExaSettings> {
512
+ return {
513
+ enabled: this.settings.exa?.enabled ?? true,
514
+ enableSearch: this.settings.exa?.enableSearch ?? true,
515
+ enableLinkedin: this.settings.exa?.enableLinkedin ?? false,
516
+ enableCompany: this.settings.exa?.enableCompany ?? false,
517
+ enableResearcher: this.settings.exa?.enableResearcher ?? false,
518
+ enableWebsets: this.settings.exa?.enableWebsets ?? false,
519
+ };
520
+ }
521
+
522
+ setExaEnabled(enabled: boolean): void {
523
+ if (!this.globalSettings.exa) {
524
+ this.globalSettings.exa = {};
525
+ }
526
+ this.globalSettings.exa.enabled = enabled;
527
+ this.save();
528
+ }
529
+
530
+ setExaSearchEnabled(enabled: boolean): void {
531
+ if (!this.globalSettings.exa) {
532
+ this.globalSettings.exa = {};
533
+ }
534
+ this.globalSettings.exa.enableSearch = enabled;
535
+ this.save();
536
+ }
537
+
538
+ setExaLinkedinEnabled(enabled: boolean): void {
539
+ if (!this.globalSettings.exa) {
540
+ this.globalSettings.exa = {};
541
+ }
542
+ this.globalSettings.exa.enableLinkedin = enabled;
543
+ this.save();
544
+ }
545
+
546
+ setExaCompanyEnabled(enabled: boolean): void {
547
+ if (!this.globalSettings.exa) {
548
+ this.globalSettings.exa = {};
549
+ }
550
+ this.globalSettings.exa.enableCompany = enabled;
551
+ this.save();
552
+ }
553
+
554
+ setExaResearcherEnabled(enabled: boolean): void {
555
+ if (!this.globalSettings.exa) {
556
+ this.globalSettings.exa = {};
557
+ }
558
+ this.globalSettings.exa.enableResearcher = enabled;
559
+ this.save();
560
+ }
561
+
562
+ setExaWebsetsEnabled(enabled: boolean): void {
563
+ if (!this.globalSettings.exa) {
564
+ this.globalSettings.exa = {};
565
+ }
566
+ this.globalSettings.exa.enableWebsets = enabled;
567
+ this.save();
568
+ }
569
+
570
+ getBashInterceptorEnabled(): boolean {
571
+ return this.settings.bashInterceptor?.enabled ?? false;
572
+ }
573
+
574
+ setBashInterceptorEnabled(enabled: boolean): void {
575
+ if (!this.globalSettings.bashInterceptor) {
576
+ this.globalSettings.bashInterceptor = {};
577
+ }
578
+ this.globalSettings.bashInterceptor.enabled = enabled;
579
+ this.save();
580
+ }
581
+
582
+ getMCPProjectConfigEnabled(): boolean {
583
+ return this.settings.mcp?.enableProjectConfig ?? true;
584
+ }
585
+
586
+ setMCPProjectConfigEnabled(enabled: boolean): void {
587
+ if (!this.globalSettings.mcp) {
588
+ this.globalSettings.mcp = {};
589
+ }
590
+ this.globalSettings.mcp.enableProjectConfig = enabled;
591
+ this.save();
592
+ }
593
+
594
+ getLspFormatOnWrite(): boolean {
595
+ return this.settings.lsp?.formatOnWrite ?? false;
596
+ }
597
+
598
+ setLspFormatOnWrite(enabled: boolean): void {
599
+ if (!this.globalSettings.lsp) {
600
+ this.globalSettings.lsp = {};
601
+ }
602
+ this.globalSettings.lsp.formatOnWrite = enabled;
603
+ this.save();
604
+ }
605
+
606
+ getLspDiagnosticsOnWrite(): boolean {
607
+ return this.settings.lsp?.diagnosticsOnWrite ?? true;
608
+ }
609
+
610
+ setLspDiagnosticsOnWrite(enabled: boolean): void {
611
+ if (!this.globalSettings.lsp) {
612
+ this.globalSettings.lsp = {};
613
+ }
614
+ this.globalSettings.lsp.diagnosticsOnWrite = enabled;
615
+ this.save();
616
+ }
617
+
618
+ getLspDiagnosticsOnEdit(): boolean {
619
+ return this.settings.lsp?.diagnosticsOnEdit ?? false;
620
+ }
621
+
622
+ setLspDiagnosticsOnEdit(enabled: boolean): void {
623
+ if (!this.globalSettings.lsp) {
624
+ this.globalSettings.lsp = {};
625
+ }
626
+ this.globalSettings.lsp.diagnosticsOnEdit = enabled;
627
+ this.save();
628
+ }
629
+
630
+ getEditFuzzyMatch(): boolean {
631
+ return this.settings.edit?.fuzzyMatch ?? true;
632
+ }
633
+
634
+ setEditFuzzyMatch(enabled: boolean): void {
635
+ if (!this.globalSettings.edit) {
636
+ this.globalSettings.edit = {};
637
+ }
638
+ this.globalSettings.edit.fuzzyMatch = enabled;
639
+ this.save();
640
+ }
641
+
642
+ getDisabledProviders(): string[] {
643
+ return [...(this.settings.disabledProviders ?? [])];
644
+ }
645
+
646
+ setDisabledProviders(providerIds: string[]): void {
647
+ this.globalSettings.disabledProviders = providerIds;
648
+ this.save();
649
+ }
650
+
651
+ getDisabledExtensions(): string[] {
652
+ return [...(this.settings.disabledExtensions ?? [])];
653
+ }
654
+
655
+ setDisabledExtensions(extensionIds: string[]): void {
656
+ this.globalSettings.disabledExtensions = extensionIds;
657
+ this.save();
658
+ }
659
+
660
+ isExtensionEnabled(extensionId: string): boolean {
661
+ return !(this.settings.disabledExtensions ?? []).includes(extensionId);
662
+ }
663
+
664
+ enableExtension(extensionId: string): void {
665
+ const disabled = this.globalSettings.disabledExtensions ?? [];
666
+ const index = disabled.indexOf(extensionId);
667
+ if (index !== -1) {
668
+ disabled.splice(index, 1);
669
+ this.globalSettings.disabledExtensions = disabled;
670
+ this.save();
671
+ }
672
+ }
673
+
674
+ disableExtension(extensionId: string): void {
675
+ const disabled = this.globalSettings.disabledExtensions ?? [];
676
+ if (!disabled.includes(extensionId)) {
677
+ disabled.push(extensionId);
678
+ this.globalSettings.disabledExtensions = disabled;
679
+ this.save();
680
+ }
681
+ }
682
+
683
+ getTtsrSettings(): TtsrSettings {
684
+ return this.settings.ttsr ?? {};
685
+ }
686
+
687
+ setTtsrSettings(settings: TtsrSettings): void {
688
+ this.globalSettings.ttsr = { ...this.globalSettings.ttsr, ...settings };
689
+ this.save();
690
+ }
691
+
692
+ getTtsrEnabled(): boolean {
693
+ return this.settings.ttsr?.enabled ?? true;
694
+ }
695
+
696
+ setTtsrEnabled(enabled: boolean): void {
697
+ if (!this.globalSettings.ttsr) {
698
+ this.globalSettings.ttsr = {};
699
+ }
700
+ this.globalSettings.ttsr.enabled = enabled;
701
+ this.save();
702
+ }
703
+
704
+ getTtsrContextMode(): "keep" | "discard" {
705
+ return this.settings.ttsr?.contextMode ?? "discard";
706
+ }
707
+
708
+ setTtsrContextMode(mode: "keep" | "discard"): void {
709
+ if (!this.globalSettings.ttsr) {
710
+ this.globalSettings.ttsr = {};
711
+ }
712
+ this.globalSettings.ttsr.contextMode = mode;
713
+ this.save();
714
+ }
715
+
716
+ getTtsrRepeatMode(): "once" | "after-gap" {
717
+ return this.settings.ttsr?.repeatMode ?? "once";
718
+ }
719
+
720
+ setTtsrRepeatMode(mode: "once" | "after-gap"): void {
721
+ if (!this.globalSettings.ttsr) {
722
+ this.globalSettings.ttsr = {};
723
+ }
724
+ this.globalSettings.ttsr.repeatMode = mode;
725
+ this.save();
726
+ }
727
+
728
+ getTtsrRepeatGap(): number {
729
+ return this.settings.ttsr?.repeatGap ?? 10;
730
+ }
731
+
732
+ setTtsrRepeatGap(gap: number): void {
733
+ if (!this.globalSettings.ttsr) {
734
+ this.globalSettings.ttsr = {};
735
+ }
736
+ this.globalSettings.ttsr.repeatGap = gap;
737
+ this.save();
738
+ }
739
+
740
+ // ═══════════════════════════════════════════════════════════════════════════
741
+ // Status Line Settings
742
+ // ═══════════════════════════════════════════════════════════════════════════
743
+
744
+ getStatusLineSettings(): StatusLineSettings {
745
+ return this.settings.statusLine ? { ...this.settings.statusLine } : {};
746
+ }
747
+
748
+ getStatusLinePreset(): StatusLinePreset {
749
+ return this.settings.statusLine?.preset ?? "default";
750
+ }
751
+
752
+ setStatusLinePreset(preset: StatusLinePreset): void {
753
+ if (!this.globalSettings.statusLine) {
754
+ this.globalSettings.statusLine = {};
755
+ }
756
+ if (preset !== "custom") {
757
+ delete this.globalSettings.statusLine.leftSegments;
758
+ delete this.globalSettings.statusLine.rightSegments;
759
+ delete this.globalSettings.statusLine.segmentOptions;
760
+ }
761
+ this.globalSettings.statusLine.preset = preset;
762
+ this.save();
763
+ }
764
+
765
+ getStatusLineSeparator(): StatusLineSeparatorStyle {
766
+ return this.settings.statusLine?.separator ?? "powerline-thin";
767
+ }
768
+
769
+ setStatusLineSeparator(separator: StatusLineSeparatorStyle): void {
770
+ if (!this.globalSettings.statusLine) {
771
+ this.globalSettings.statusLine = {};
772
+ }
773
+ this.globalSettings.statusLine.separator = separator;
774
+ this.save();
775
+ }
776
+
777
+ getStatusLineLeftSegments(): StatusLineSegmentId[] {
778
+ return [...(this.settings.statusLine?.leftSegments ?? [])];
779
+ }
780
+
781
+ setStatusLineLeftSegments(segments: StatusLineSegmentId[]): void {
782
+ if (!this.globalSettings.statusLine) {
783
+ this.globalSettings.statusLine = {};
784
+ }
785
+ this.globalSettings.statusLine.leftSegments = segments;
786
+ // Setting segments explicitly implies custom preset
787
+ if (this.globalSettings.statusLine.preset !== "custom") {
788
+ this.globalSettings.statusLine.preset = "custom";
789
+ }
790
+ this.save();
791
+ }
792
+
793
+ getStatusLineRightSegments(): StatusLineSegmentId[] {
794
+ return [...(this.settings.statusLine?.rightSegments ?? [])];
795
+ }
796
+
797
+ setStatusLineRightSegments(segments: StatusLineSegmentId[]): void {
798
+ if (!this.globalSettings.statusLine) {
799
+ this.globalSettings.statusLine = {};
800
+ }
801
+ this.globalSettings.statusLine.rightSegments = segments;
802
+ // Setting segments explicitly implies custom preset
803
+ if (this.globalSettings.statusLine.preset !== "custom") {
804
+ this.globalSettings.statusLine.preset = "custom";
805
+ }
806
+ this.save();
807
+ }
808
+
809
+ getStatusLineSegmentOptions(): StatusLineSegmentOptions {
810
+ return { ...this.settings.statusLine?.segmentOptions };
811
+ }
812
+
813
+ setStatusLineSegmentOption<K extends keyof StatusLineSegmentOptions>(
814
+ segment: K,
815
+ option: keyof NonNullable<StatusLineSegmentOptions[K]>,
816
+ value: boolean | number | string,
817
+ ): void {
818
+ if (!this.globalSettings.statusLine) {
819
+ this.globalSettings.statusLine = {};
820
+ }
821
+ if (!this.globalSettings.statusLine.segmentOptions) {
822
+ this.globalSettings.statusLine.segmentOptions = {};
823
+ }
824
+ if (!this.globalSettings.statusLine.segmentOptions[segment]) {
825
+ this.globalSettings.statusLine.segmentOptions[segment] = {} as NonNullable<StatusLineSegmentOptions[K]>;
826
+ }
827
+ (this.globalSettings.statusLine.segmentOptions[segment] as Record<string, unknown>)[option as string] = value;
828
+ this.save();
829
+ }
830
+
831
+ clearStatusLineSegmentOption<K extends keyof StatusLineSegmentOptions>(
832
+ segment: K,
833
+ option: keyof NonNullable<StatusLineSegmentOptions[K]>,
834
+ ): void {
835
+ const segmentOptions = this.globalSettings.statusLine?.segmentOptions;
836
+ if (!segmentOptions || !segmentOptions[segment]) {
837
+ return;
838
+ }
839
+ delete (segmentOptions[segment] as Record<string, unknown>)[option as string];
840
+ if (Object.keys(segmentOptions[segment] as Record<string, unknown>).length === 0) {
841
+ delete segmentOptions[segment];
842
+ }
843
+ if (Object.keys(segmentOptions).length === 0) {
844
+ delete this.globalSettings.statusLine?.segmentOptions;
845
+ }
846
+ this.save();
847
+ }
848
+
849
+ getStatusLineShowHookStatus(): boolean {
850
+ return this.settings.statusLine?.showHookStatus ?? true;
851
+ }
852
+
853
+ setStatusLineShowHookStatus(show: boolean): void {
854
+ if (!this.globalSettings.statusLine) {
855
+ this.globalSettings.statusLine = {};
856
+ }
857
+ this.globalSettings.statusLine.showHookStatus = show;
858
+ this.save();
859
+ }
860
+ }