@oh-my-pi/pi-coding-agent 15.10.0 → 15.10.2

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 (238) hide show
  1. package/CHANGELOG.md +142 -1
  2. package/dist/types/cli/dry-balance-cli.d.ts +15 -1
  3. package/dist/types/cli/startup-cwd.d.ts +2 -0
  4. package/dist/types/commands/launch.d.ts +3 -0
  5. package/dist/types/commit/analysis/conventional.d.ts +2 -2
  6. package/dist/types/commit/analysis/summary.d.ts +2 -2
  7. package/dist/types/commit/changelog/generate.d.ts +2 -2
  8. package/dist/types/commit/changelog/index.d.ts +2 -2
  9. package/dist/types/commit/map-reduce/index.d.ts +3 -3
  10. package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
  11. package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
  12. package/dist/types/commit/model-selection.d.ts +10 -4
  13. package/dist/types/config/api-key-resolver.d.ts +34 -0
  14. package/dist/types/config/keybindings.d.ts +2 -2
  15. package/dist/types/config/model-provider-priority.d.ts +1 -0
  16. package/dist/types/config/model-registry.d.ts +17 -1
  17. package/dist/types/config/model-resolver.d.ts +4 -1
  18. package/dist/types/config/settings-schema.d.ts +9 -0
  19. package/dist/types/config/settings.d.ts +7 -2
  20. package/dist/types/dap/config.d.ts +14 -1
  21. package/dist/types/dap/types.d.ts +10 -0
  22. package/dist/types/debug/report-bundle.d.ts +3 -0
  23. package/dist/types/edit/file-snapshot-store.d.ts +18 -10
  24. package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
  25. package/dist/types/extensibility/extensions/types.d.ts +4 -1
  26. package/dist/types/lsp/client.d.ts +10 -0
  27. package/dist/types/lsp/utils.d.ts +3 -2
  28. package/dist/types/main.d.ts +3 -9
  29. package/dist/types/mcp/tool-bridge.d.ts +2 -0
  30. package/dist/types/modes/components/chat-block.d.ts +64 -0
  31. package/dist/types/modes/components/custom-editor.d.ts +4 -1
  32. package/dist/types/modes/components/overlay-box.d.ts +17 -0
  33. package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
  34. package/dist/types/modes/components/plan-toc.d.ts +41 -0
  35. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  36. package/dist/types/modes/components/status-line.d.ts +2 -0
  37. package/dist/types/modes/components/transcript-container.d.ts +11 -0
  38. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  39. package/dist/types/modes/controllers/event-controller.d.ts +17 -1
  40. package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
  41. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  42. package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
  43. package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
  44. package/dist/types/modes/interactive-mode.d.ts +16 -5
  45. package/dist/types/modes/magic-keywords.d.ts +1 -1
  46. package/dist/types/modes/markdown-prose.d.ts +1 -1
  47. package/dist/types/modes/theme/theme.d.ts +1 -1
  48. package/dist/types/modes/types.d.ts +21 -5
  49. package/dist/types/modes/utils/copy-targets.d.ts +21 -1
  50. package/dist/types/modes/workflow.d.ts +3 -3
  51. package/dist/types/plan-mode/approved-plan.d.ts +27 -8
  52. package/dist/types/plan-mode/plan-protection.d.ts +4 -4
  53. package/dist/types/sdk.d.ts +2 -0
  54. package/dist/types/session/agent-session.d.ts +21 -0
  55. package/dist/types/session/auth-storage.d.ts +1 -1
  56. package/dist/types/session/messages.d.ts +12 -0
  57. package/dist/types/session/session-manager.d.ts +8 -3
  58. package/dist/types/slash-commands/types.d.ts +4 -6
  59. package/dist/types/task/executor.d.ts +17 -0
  60. package/dist/types/task/index.d.ts +1 -0
  61. package/dist/types/task/render.d.ts +3 -2
  62. package/dist/types/tools/archive-reader.d.ts +5 -0
  63. package/dist/types/tools/ast-edit.d.ts +3 -0
  64. package/dist/types/tools/ast-grep.d.ts +3 -0
  65. package/dist/types/tools/bash.d.ts +1 -0
  66. package/dist/types/tools/eval.d.ts +8 -0
  67. package/dist/types/tools/find.d.ts +8 -4
  68. package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
  69. package/dist/types/tools/github-cache.d.ts +12 -0
  70. package/dist/types/tools/grouped-file-output.d.ts +95 -12
  71. package/dist/types/tools/memory-render.d.ts +4 -1
  72. package/dist/types/tools/path-utils.d.ts +8 -0
  73. package/dist/types/tools/plan-mode-guard.d.ts +8 -9
  74. package/dist/types/tools/render-utils.d.ts +5 -9
  75. package/dist/types/tools/search.d.ts +6 -2
  76. package/dist/types/tools/sqlite-reader.d.ts +1 -0
  77. package/dist/types/tools/todo.d.ts +3 -2
  78. package/dist/types/tools/write.d.ts +3 -0
  79. package/dist/types/tools/yield.d.ts +8 -0
  80. package/dist/types/tui/output-block.d.ts +16 -4
  81. package/dist/types/tui/status-line.d.ts +3 -0
  82. package/dist/types/utils/enhanced-paste.d.ts +20 -0
  83. package/dist/types/web/search/providers/kimi.d.ts +1 -1
  84. package/package.json +9 -9
  85. package/src/auto-thinking/classifier.ts +5 -1
  86. package/src/cli/args.ts +3 -1
  87. package/src/cli/dry-balance-cli.ts +54 -21
  88. package/src/cli/gallery-cli.ts +4 -1
  89. package/src/cli/gallery-fixtures/misc.ts +29 -0
  90. package/src/cli/startup-cwd.ts +68 -0
  91. package/src/commands/launch.ts +3 -0
  92. package/src/commit/analysis/conventional.ts +2 -2
  93. package/src/commit/analysis/summary.ts +2 -2
  94. package/src/commit/changelog/generate.ts +2 -2
  95. package/src/commit/changelog/index.ts +2 -2
  96. package/src/commit/map-reduce/index.ts +3 -3
  97. package/src/commit/map-reduce/map-phase.ts +2 -2
  98. package/src/commit/map-reduce/reduce-phase.ts +2 -2
  99. package/src/commit/model-selection.ts +36 -11
  100. package/src/commit/pipeline.ts +4 -4
  101. package/src/config/api-key-resolver.ts +58 -0
  102. package/src/config/model-provider-priority.ts +55 -0
  103. package/src/config/model-registry.ts +29 -24
  104. package/src/config/model-resolver.ts +39 -7
  105. package/src/config/settings-schema.ts +10 -0
  106. package/src/config/settings.ts +106 -43
  107. package/src/dap/config.ts +41 -2
  108. package/src/dap/defaults.json +1 -0
  109. package/src/dap/session.ts +1 -0
  110. package/src/dap/types.ts +10 -0
  111. package/src/debug/index.ts +47 -53
  112. package/src/debug/raw-sse-buffer.ts +7 -4
  113. package/src/debug/report-bundle.ts +9 -0
  114. package/src/edit/file-snapshot-store.ts +33 -1
  115. package/src/edit/hashline/filesystem.ts +2 -1
  116. package/src/edit/renderer.ts +82 -78
  117. package/src/eval/__tests__/llm-bridge.test.ts +110 -31
  118. package/src/eval/js/context-manager.ts +32 -15
  119. package/src/eval/llm-bridge.ts +22 -6
  120. package/src/eval/py/__tests__/prelude.test.ts +19 -0
  121. package/src/eval/py/executor.ts +23 -11
  122. package/src/eval/py/prelude.py +1 -1
  123. package/src/extensibility/extensions/types.ts +10 -1
  124. package/src/goals/tools/goal-tool.ts +36 -26
  125. package/src/internal-urls/docs-index.generated.ts +8 -8
  126. package/src/lsp/client.ts +23 -11
  127. package/src/lsp/config.ts +11 -1
  128. package/src/lsp/index.ts +61 -9
  129. package/src/lsp/utils.ts +3 -2
  130. package/src/main.ts +100 -72
  131. package/src/mcp/tool-bridge.ts +2 -0
  132. package/src/memories/index.ts +14 -7
  133. package/src/mnemopi/backend.ts +5 -1
  134. package/src/modes/acp/acp-agent.ts +33 -26
  135. package/src/modes/components/assistant-message.ts +2 -9
  136. package/src/modes/components/chat-block.ts +111 -0
  137. package/src/modes/components/copy-selector.ts +1 -44
  138. package/src/modes/components/custom-editor.ts +164 -109
  139. package/src/modes/components/custom-message.ts +1 -3
  140. package/src/modes/components/execution-shared.ts +1 -2
  141. package/src/modes/components/hook-message.ts +1 -3
  142. package/src/modes/components/model-selector.ts +59 -13
  143. package/src/modes/components/oauth-selector.ts +33 -7
  144. package/src/modes/components/overlay-box.ts +108 -0
  145. package/src/modes/components/plan-review-overlay.ts +799 -0
  146. package/src/modes/components/plan-toc.ts +138 -0
  147. package/src/modes/components/read-tool-group.ts +20 -4
  148. package/src/modes/components/skill-message.ts +0 -1
  149. package/src/modes/components/status-line.ts +19 -4
  150. package/src/modes/components/tips.txt +2 -1
  151. package/src/modes/components/todo-reminder.ts +0 -2
  152. package/src/modes/components/tool-execution.ts +68 -88
  153. package/src/modes/components/transcript-container.ts +84 -24
  154. package/src/modes/components/user-message.ts +2 -3
  155. package/src/modes/controllers/command-controller-shared.ts +7 -6
  156. package/src/modes/controllers/command-controller.ts +57 -55
  157. package/src/modes/controllers/event-controller.ts +67 -40
  158. package/src/modes/controllers/extension-ui-controller.ts +10 -73
  159. package/src/modes/controllers/input-controller.ts +170 -126
  160. package/src/modes/controllers/mcp-command-controller.ts +69 -60
  161. package/src/modes/controllers/selector-controller.ts +23 -25
  162. package/src/modes/controllers/streaming-reveal.ts +212 -0
  163. package/src/modes/controllers/tan-command-controller.ts +173 -0
  164. package/src/modes/interactive-mode.ts +274 -112
  165. package/src/modes/magic-keywords.ts +1 -1
  166. package/src/modes/markdown-prose.ts +1 -1
  167. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  168. package/src/modes/theme/shimmer.ts +20 -9
  169. package/src/modes/theme/theme-schema.json +1 -1
  170. package/src/modes/theme/theme.ts +8 -4
  171. package/src/modes/types.ts +21 -7
  172. package/src/modes/utils/copy-targets.ts +133 -27
  173. package/src/modes/utils/ui-helpers.ts +44 -46
  174. package/src/modes/workflow.ts +10 -10
  175. package/src/plan-mode/approved-plan.ts +66 -43
  176. package/src/plan-mode/plan-protection.ts +4 -4
  177. package/src/prompts/system/background-tan-dispatch.md +8 -0
  178. package/src/prompts/system/plan-mode-active.md +67 -58
  179. package/src/prompts/system/plan-mode-approved.md +1 -1
  180. package/src/prompts/system/workflow-notice.md +1 -1
  181. package/src/prompts/tools/bash.md +9 -0
  182. package/src/prompts/tools/browser.md +1 -1
  183. package/src/prompts/tools/eval.md +2 -1
  184. package/src/prompts/tools/read.md +2 -2
  185. package/src/sdk.ts +37 -46
  186. package/src/session/agent-session.ts +119 -18
  187. package/src/session/auth-storage.ts +2 -0
  188. package/src/session/messages.ts +26 -0
  189. package/src/session/session-manager.ts +109 -28
  190. package/src/slash-commands/builtin-registry.ts +36 -9
  191. package/src/slash-commands/types.ts +4 -6
  192. package/src/task/executor.ts +76 -38
  193. package/src/task/index.ts +4 -0
  194. package/src/task/render.ts +211 -147
  195. package/src/tools/archive-reader.ts +64 -0
  196. package/src/tools/ask.ts +119 -164
  197. package/src/tools/ast-edit.ts +98 -71
  198. package/src/tools/ast-grep.ts +37 -43
  199. package/src/tools/bash.ts +57 -6
  200. package/src/tools/browser/tab-supervisor.ts +13 -1
  201. package/src/tools/browser/tab-worker.ts +33 -4
  202. package/src/tools/debug.ts +20 -8
  203. package/src/tools/eval.ts +13 -2
  204. package/src/tools/fetch.ts +297 -7
  205. package/src/tools/find.ts +51 -30
  206. package/src/tools/gh-cache-invalidation.ts +200 -0
  207. package/src/tools/gh-renderer.ts +81 -42
  208. package/src/tools/github-cache.ts +25 -0
  209. package/src/tools/grouped-file-output.ts +272 -48
  210. package/src/tools/image-gen.ts +150 -103
  211. package/src/tools/inspect-image-renderer.ts +63 -41
  212. package/src/tools/inspect-image.ts +10 -3
  213. package/src/tools/job.ts +3 -4
  214. package/src/tools/memory-render.ts +4 -1
  215. package/src/tools/path-utils.ts +28 -2
  216. package/src/tools/plan-mode-guard.ts +66 -39
  217. package/src/tools/read.ts +48 -28
  218. package/src/tools/render-utils.ts +21 -37
  219. package/src/tools/resolve.ts +14 -0
  220. package/src/tools/search-tool-bm25.ts +36 -23
  221. package/src/tools/search.ts +118 -81
  222. package/src/tools/sqlite-reader.ts +9 -12
  223. package/src/tools/todo.ts +118 -52
  224. package/src/tools/write.ts +83 -64
  225. package/src/tools/yield.ts +10 -1
  226. package/src/tui/output-block.ts +60 -13
  227. package/src/tui/status-line.ts +5 -1
  228. package/src/utils/commit-message-generator.ts +11 -3
  229. package/src/utils/enhanced-paste.ts +230 -0
  230. package/src/utils/title-generator.ts +2 -1
  231. package/src/web/search/providers/anthropic.ts +25 -19
  232. package/src/web/search/providers/codex.ts +37 -8
  233. package/src/web/search/providers/exa.ts +11 -3
  234. package/src/web/search/providers/kimi.ts +28 -17
  235. package/src/web/search/providers/parallel.ts +35 -24
  236. package/src/web/search/providers/synthetic.ts +8 -6
  237. package/src/web/search/providers/tavily.ts +9 -8
  238. package/src/web/search/providers/zai.ts +8 -6
@@ -17,6 +17,7 @@ import { logger } from "@oh-my-pi/pi-utils";
17
17
  import chalk from "chalk";
18
18
  import MODEL_PRIO from "../priority.json" with { type: "json" };
19
19
  import { parseThinkingLevel, resolveThinkingLevelForModel } from "../thinking";
20
+ import { buildModelProviderPriorityRank } from "./model-provider-priority";
20
21
  import { isAuthenticated, kNoAuth, MODEL_ROLE_IDS, type ModelRegistry, type ModelRole } from "./model-registry";
21
22
  import type { Settings } from "./settings";
22
23
 
@@ -179,7 +180,9 @@ export function resolveProviderModelReference(
179
180
  export interface ModelMatchPreferences {
180
181
  /** Most-recently-used model keys (provider/modelId) to prefer when ambiguous. */
181
182
  usageOrder?: string[];
182
- /** Providers to deprioritize when no recent usage is available. */
183
+ /** Provider precedence used for ambiguous unqualified model patterns. */
184
+ providerOrder?: readonly string[];
185
+ /** Providers to deprioritize when no recent usage or provider priority is available. */
183
186
  deprioritizeProviders?: string[];
184
187
  }
185
188
 
@@ -194,6 +197,7 @@ type RestorableModelRegistry = Pick<ModelRegistry, "getAvailable" | "find" | "ge
194
197
  interface ModelPreferenceContext {
195
198
  modelUsageRank: Map<string, number>;
196
199
  providerUsageRank: Map<string, number>;
200
+ providerPriorityRank: Map<string, number>;
197
201
  deprioritizedProviders: Set<string>;
198
202
  modelOrder: Map<string, number>;
199
203
  }
@@ -215,14 +219,35 @@ function buildPreferenceContext(
215
219
  providerUsageRank.set(parsed.provider, i);
216
220
  }
217
221
  }
218
-
219
- const deprioritizedProviders = new Set(preferences?.deprioritizeProviders ?? ["openrouter"]);
222
+ const providerPriorityRank = buildModelProviderPriorityRank(preferences?.providerOrder);
223
+ const deprioritizedProviders = new Set(preferences?.deprioritizeProviders ?? []);
220
224
  const modelOrder = new Map<string, number>();
221
225
  for (let i = 0; i < availableModels.length; i += 1) {
222
226
  modelOrder.set(formatModelString(availableModels[i]), i);
223
227
  }
224
228
 
225
- return { modelUsageRank, providerUsageRank, deprioritizedProviders, modelOrder };
229
+ return { modelUsageRank, providerUsageRank, providerPriorityRank, deprioritizedProviders, modelOrder };
230
+ }
231
+
232
+ export function getModelMatchPreferences(
233
+ settings?: Partial<Pick<Settings, "get" | "getStorage">>,
234
+ ): ModelMatchPreferences {
235
+ return {
236
+ usageOrder: settings?.getStorage?.()?.getModelUsageOrder(),
237
+ providerOrder: settings?.get?.("modelProviderOrder"),
238
+ };
239
+ }
240
+
241
+ function mergeModelMatchPreferences(
242
+ settings: Settings | undefined,
243
+ preferences: ModelMatchPreferences | undefined,
244
+ ): ModelMatchPreferences {
245
+ const settingsPreferences = getModelMatchPreferences(settings);
246
+ return {
247
+ usageOrder: preferences?.usageOrder ?? settingsPreferences.usageOrder,
248
+ providerOrder: preferences?.providerOrder ?? settingsPreferences.providerOrder,
249
+ deprioritizeProviders: preferences?.deprioritizeProviders,
250
+ };
226
251
  }
227
252
 
228
253
  function pickPreferredModel(candidates: Model<Api>[], context: ModelPreferenceContext): Model<Api> {
@@ -236,6 +261,12 @@ function pickPreferredModel(candidates: Model<Api>[], context: ModelPreferenceCo
236
261
  return (aUsage ?? Number.POSITIVE_INFINITY) - (bUsage ?? Number.POSITIVE_INFINITY);
237
262
  }
238
263
 
264
+ const aProviderPriority = context.providerPriorityRank.get(a.provider.toLowerCase());
265
+ const bProviderPriority = context.providerPriorityRank.get(b.provider.toLowerCase());
266
+ if (aProviderPriority !== undefined || bProviderPriority !== undefined) {
267
+ return (aProviderPriority ?? Number.POSITIVE_INFINITY) - (bProviderPriority ?? Number.POSITIVE_INFINITY);
268
+ }
269
+
239
270
  const aProviderUsage = context.providerUsageRank.get(a.provider);
240
271
  const bProviderUsage = context.providerUsageRank.get(b.provider);
241
272
  if (aProviderUsage !== undefined || bProviderUsage !== undefined) {
@@ -618,8 +649,9 @@ export function resolveModelRoleValue(
618
649
  }
619
650
 
620
651
  let warning: string | undefined;
652
+ const matchPreferences = mergeModelMatchPreferences(options?.settings, options?.matchPreferences);
621
653
  for (const effectivePattern of effectivePatterns) {
622
- const resolved = parseModelPattern(effectivePattern, availableModels, options?.matchPreferences, {
654
+ const resolved = parseModelPattern(effectivePattern, availableModels, matchPreferences, {
623
655
  modelRegistry: options?.modelRegistry,
624
656
  });
625
657
  if (resolved.model) {
@@ -720,7 +752,7 @@ export function resolveModelOverride(
720
752
  ): { model?: Model<Api>; thinkingLevel?: ThinkingLevel; explicitThinkingLevel: boolean } {
721
753
  if (modelPatterns.length === 0) return { explicitThinkingLevel: false };
722
754
  const availableModels = modelRegistry.getAvailable();
723
- const matchPreferences = { usageOrder: settings?.getStorage()?.getModelUsageOrder() };
755
+ const matchPreferences = getModelMatchPreferences(settings);
724
756
  for (const pattern of modelPatterns) {
725
757
  const { model, thinkingLevel, explicitThinkingLevel } = resolveModelRoleValue(pattern, availableModels, {
726
758
  settings,
@@ -800,7 +832,7 @@ export function resolveRoleSelection(
800
832
  availableModels: Model<Api>[],
801
833
  modelRegistry?: CanonicalModelRegistry,
802
834
  ): { model: Model<Api>; thinkingLevel?: ThinkingLevel } | undefined {
803
- const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
835
+ const matchPreferences = getModelMatchPreferences(settings);
804
836
  for (const role of roles) {
805
837
  const resolved = resolveModelRoleValue(settings.getModelRole(role), availableModels, {
806
838
  settings,
@@ -660,6 +660,16 @@ export const SETTINGS_SCHEMA = {
660
660
  },
661
661
  },
662
662
 
663
+ "display.smoothStreaming": {
664
+ type: "boolean",
665
+ default: true,
666
+ ui: {
667
+ tab: "appearance",
668
+ label: "Smooth Streaming",
669
+ description: "Reveal assistant text smoothly while streamed chunks arrive",
670
+ },
671
+ },
672
+
663
673
  "display.showTokenUsage": {
664
674
  type: "boolean",
665
675
  default: false,
@@ -72,7 +72,7 @@ export interface SettingsOptions {
72
72
  /**
73
73
  * Get a nested value from an object by path segments.
74
74
  */
75
- function getByPath(obj: RawSettings, segments: string[]): unknown {
75
+ function getByPath(obj: RawSettings, segments: readonly string[]): unknown {
76
76
  let current: unknown = obj;
77
77
  for (const segment of segments) {
78
78
  if (current === null || current === undefined || typeof current !== "object") {
@@ -83,6 +83,10 @@ function getByPath(obj: RawSettings, segments: string[]): unknown {
83
83
  return current;
84
84
  }
85
85
 
86
+ const SETTING_PATH_SEGMENTS: Record<SettingPath, readonly string[]> = Object.fromEntries(
87
+ (Object.keys(SETTINGS_SCHEMA) as SettingPath[]).map(settingPath => [settingPath, settingPath.split(".")]),
88
+ ) as unknown as Record<SettingPath, readonly string[]>;
89
+
86
90
  /**
87
91
  * Set a nested value in an object by path segments.
88
92
  * Creates intermediate objects as needed.
@@ -196,6 +200,8 @@ export class Settings {
196
200
  #overrides: RawSettings = {};
197
201
  /** Merged view (global + project + overrides) */
198
202
  #merged: RawSettings = {};
203
+ /** Cached resolved values from the merged view, including defaults/path scoping */
204
+ #resolvedCache = new Map<SettingPath, unknown>();
199
205
 
200
206
  /** Paths modified during this session (for partial save) */
201
207
  #modified = new Set<string>();
@@ -240,11 +246,13 @@ export class Settings {
240
246
  return promise.then(
241
247
  instance => {
242
248
  globalInstance = instance;
249
+ clearBoundSettingsMethods();
243
250
  globalInstancePromise = Promise.resolve(instance);
244
251
  return instance;
245
252
  },
246
253
  error => {
247
254
  globalInstance = null;
255
+ clearBoundSettingsMethods();
248
256
  throw error;
249
257
  },
250
258
  );
@@ -280,13 +288,15 @@ export class Settings {
280
288
  * Returns the merged value from global + project + overrides, or the default.
281
289
  */
282
290
  get<P extends SettingPath>(path: P): SettingValue<P> {
283
- const segments = path.split(".");
284
- const value = getByPath(this.#merged, segments);
285
- if (value !== undefined) {
286
- const pathScopedValue = resolvePathScopedStringArray(path, value, this.#cwd);
287
- return (pathScopedValue ?? value) as SettingValue<P>;
291
+ if (this.#resolvedCache.has(path)) {
292
+ return this.#resolvedCache.get(path) as SettingValue<P>;
288
293
  }
289
- return getDefault(path);
294
+
295
+ const value = getByPath(this.#merged, SETTING_PATH_SEGMENTS[path]);
296
+ const resolved =
297
+ value !== undefined ? (resolvePathScopedStringArray(path, value, this.#cwd) ?? value) : getDefault(path);
298
+ this.#resolvedCache.set(path, resolved);
299
+ return resolved as SettingValue<P>;
290
300
  }
291
301
 
292
302
  /**
@@ -300,6 +310,7 @@ export class Settings {
300
310
  setByPath(this.#global, segments, value);
301
311
  this.#modified.add(path);
302
312
  this.#rebuildMerged();
313
+ const next = this.get(path);
303
314
  this.#queueSave();
304
315
 
305
316
  // Trigger hook if exists
@@ -307,21 +318,25 @@ export class Settings {
307
318
  if (hook) {
308
319
  hook(value, prev);
309
320
  }
321
+ this.#fireEffectiveSettingChanged(path, next, prev);
310
322
  }
311
323
 
312
324
  /**
313
325
  * Apply runtime overrides (not persisted).
314
326
  */
315
327
  override<P extends SettingPath>(path: P, value: SettingValue<P>): void {
328
+ const prev = this.get(path);
316
329
  const segments = path.split(".");
317
330
  setByPath(this.#overrides, segments, value);
318
331
  this.#rebuildMerged();
332
+ this.#fireEffectiveSettingChanged(path, this.get(path), prev);
319
333
  }
320
334
 
321
335
  /**
322
336
  * Clear a runtime override.
323
337
  */
324
338
  clearOverride(path: SettingPath): void {
339
+ const prev = this.get(path);
325
340
  const segments = path.split(".");
326
341
  let current = this.#overrides;
327
342
  for (let i = 0; i < segments.length - 1; i++) {
@@ -331,6 +346,14 @@ export class Settings {
331
346
  }
332
347
  delete current[segments[segments.length - 1]];
333
348
  this.#rebuildMerged();
349
+ this.#fireEffectiveSettingChanged(path, this.get(path), prev);
350
+ }
351
+
352
+ #fireEffectiveSettingChanged(path: SettingPath, value: unknown, prev: unknown): void {
353
+ if (Object.is(value, prev)) return;
354
+ if (path === "statusLine.sessionAccent") {
355
+ statusLineSessionAccentSignal.fire();
356
+ }
334
357
  }
335
358
 
336
359
  /**
@@ -840,6 +863,7 @@ export class Settings {
840
863
  #rebuildMerged(): void {
841
864
  this.#merged = this.#deepMerge(this.#deepMerge({}, this.#global), this.#project);
842
865
  this.#merged = this.#deepMerge(this.#merged, this.#overrides);
866
+ this.#resolvedCache.clear();
843
867
  }
844
868
 
845
869
  #fireAllHooks(): void {
@@ -883,6 +907,45 @@ export class Settings {
883
907
 
884
908
  type SettingHook<P extends SettingPath> = (value: SettingValue<P>, prev: SettingValue<P>) => void;
885
909
 
910
+ /**
911
+ * Minimal change-notification primitive backing the exported `on*Changed`
912
+ * subscriptions. Holds a listener set, hands out unsubscribe closures, and
913
+ * isolates errors so a single throwing listener can't abort the rest or bubble
914
+ * out of `Settings.set()`.
915
+ *
916
+ * @typeParam A - argument tuple forwarded to each listener on `fire`.
917
+ */
918
+ class SettingSignal<A extends unknown[] = []> {
919
+ #listeners = new Set<(...args: A) => void>();
920
+
921
+ constructor(private readonly label: string) {}
922
+
923
+ /** Subscribe `cb`; returns an unsubscribe function. */
924
+ on(cb: (...args: A) => void): () => void {
925
+ this.#listeners.add(cb);
926
+ return () => {
927
+ this.#listeners.delete(cb);
928
+ };
929
+ }
930
+
931
+ /**
932
+ * Invoke every listener with `args`. Iterates a snapshot so a listener may
933
+ * (un)subscribe mid-fire without re-entrancy — the Hindsight backend
934
+ * re-registers the fresh state's listener on every rebuild — and wraps each
935
+ * call so a throwing listener is logged and skipped instead of aborting the
936
+ * rest.
937
+ */
938
+ fire(...args: A): void {
939
+ for (const cb of [...this.#listeners]) {
940
+ try {
941
+ cb(...args);
942
+ } catch (err) {
943
+ logger.warn(`Settings: ${this.label} hook failed`, { error: String(err) });
944
+ }
945
+ }
946
+ }
947
+ }
948
+
886
949
  const SETTING_HOOKS: Partial<Record<SettingPath, SettingHook<any>>> = {
887
950
  "theme.dark": value => {
888
951
  if (typeof value === "string") {
@@ -915,45 +978,34 @@ const SETTING_HOOKS: Partial<Record<SettingPath, SettingHook<any>>> = {
915
978
  },
916
979
  "provider.appendOnlyContext": value => {
917
980
  if (typeof value === "string") {
918
- for (const cb of appendOnlyModeCallbacks) cb(value);
981
+ appendOnlyModeSignal.fire(value);
919
982
  }
920
983
  },
921
- "hindsight.bankId": () => fireHindsightScopeChanged(),
922
- "hindsight.bankIdPrefix": () => fireHindsightScopeChanged(),
923
- "hindsight.scoping": () => fireHindsightScopeChanged(),
984
+ "hindsight.bankId": () => hindsightScopeSignal.fire(),
985
+ "hindsight.bankIdPrefix": () => hindsightScopeSignal.fire(),
986
+ "hindsight.scoping": () => hindsightScopeSignal.fire(),
924
987
  };
925
- /** Callbacks invoked when `provider.appendOnlyContext` changes at runtime. */
926
- const appendOnlyModeCallbacks = new Set<(value: string) => void>();
988
+ /** Fires when `provider.appendOnlyContext` changes at runtime. */
989
+ const appendOnlyModeSignal = new SettingSignal<[value: string]>("provider.appendOnlyContext");
927
990
 
928
991
  /**
929
992
  * Subscribe to append-only mode setting changes.
930
993
  * Returns an unsubscribe function. Multiple sessions (main + subagents)
931
994
  * can register independently without overwriting each other.
932
995
  */
933
- export function onAppendOnlyModeChanged(cb: (value: string) => void): () => void {
934
- appendOnlyModeCallbacks.add(cb);
935
- return () => {
936
- appendOnlyModeCallbacks.delete(cb);
937
- };
938
- }
996
+ export const onAppendOnlyModeChanged = (cb: (value: string) => void) => appendOnlyModeSignal.on(cb);
939
997
 
940
- /** Callbacks fired when any `hindsight.bankId` / `bankIdPrefix` / `scoping` value changes. */
941
- const hindsightScopeCallbacks = new Set<() => void>();
998
+ /** Fires when `statusLine.sessionAccent` changes at runtime. */
999
+ const statusLineSessionAccentSignal = new SettingSignal("statusLine.sessionAccent");
942
1000
 
943
- function fireHindsightScopeChanged(): void {
944
- // Snapshot the callback set before invoking — a callback's body is allowed
945
- // to subscribe a NEW callback (the Hindsight backend re-registers the
946
- // fresh state's listener on every rebuild). Iterating the live Set would
947
- // re-invoke those just-added callbacks within the same fire, which spins
948
- // in place: subscribe → invoke → subscribe → invoke → …
949
- for (const cb of [...hindsightScopeCallbacks]) {
950
- try {
951
- cb();
952
- } catch (err) {
953
- logger.warn("Settings: hindsight scope hook failed", { error: String(err) });
954
- }
955
- }
956
- }
1001
+ /**
1002
+ * Subscribe to session-accent setting changes.
1003
+ * Returns an unsubscribe function. Callers should re-read settings in the callback.
1004
+ */
1005
+ export const onStatusLineSessionAccentChanged = (cb: () => void) => statusLineSessionAccentSignal.on(cb);
1006
+
1007
+ /** Fires when any `hindsight.bankId` / `bankIdPrefix` / `scoping` value changes. */
1008
+ const hindsightScopeSignal = new SettingSignal("hindsight scope");
957
1009
 
958
1010
  /**
959
1011
  * Subscribe to changes in the Hindsight bank-scoping settings. Lets the
@@ -965,12 +1017,7 @@ function fireHindsightScopeChanged(): void {
965
1017
  * Returns an unsubscribe function. The callback receives no arguments — the
966
1018
  * caller is expected to re-read the relevant settings via `Settings.get`.
967
1019
  */
968
- export function onHindsightScopeChanged(cb: () => void): () => void {
969
- hindsightScopeCallbacks.add(cb);
970
- return () => {
971
- hindsightScopeCallbacks.delete(cb);
972
- };
973
- }
1020
+ export const onHindsightScopeChanged = (cb: () => void) => hindsightScopeSignal.on(cb);
974
1021
 
975
1022
  // ═══════════════════════════════════════════════════════════════════════════
976
1023
  // Global Singleton
@@ -978,6 +1025,13 @@ export function onHindsightScopeChanged(cb: () => void): () => void {
978
1025
 
979
1026
  let globalInstance: Settings | null = null;
980
1027
  let globalInstancePromise: Promise<Settings> | null = null;
1028
+ let boundSettingsInstance: Settings | null = null;
1029
+ let boundSettingsMethods = new Map<PropertyKey, unknown>();
1030
+
1031
+ function clearBoundSettingsMethods(): void {
1032
+ boundSettingsInstance = null;
1033
+ boundSettingsMethods = new Map<PropertyKey, unknown>();
1034
+ }
981
1035
 
982
1036
  export function isSettingsInitialized(): boolean {
983
1037
  return globalInstance !== null;
@@ -990,6 +1044,7 @@ export function isSettingsInitialized(): boolean {
990
1044
  export function resetSettingsForTest(): void {
991
1045
  globalInstance = null;
992
1046
  globalInstancePromise = null;
1047
+ clearBoundSettingsMethods();
993
1048
  }
994
1049
 
995
1050
  /**
@@ -1001,9 +1056,17 @@ export const settings = new Proxy({} as Settings, {
1001
1056
  if (!globalInstance) {
1002
1057
  throw new Error("Settings not initialized. Call Settings.init() first.");
1003
1058
  }
1004
- const value = (globalInstance as unknown as Record<string | symbol, unknown>)[prop];
1059
+ if (boundSettingsInstance !== globalInstance) {
1060
+ clearBoundSettingsMethods();
1061
+ boundSettingsInstance = globalInstance;
1062
+ }
1063
+ const value = (globalInstance as unknown as Record<PropertyKey, unknown>)[prop];
1005
1064
  if (typeof value === "function") {
1006
- return value.bind(globalInstance);
1065
+ const cached = boundSettingsMethods.get(prop);
1066
+ if (cached) return cached;
1067
+ const bound = value.bind(globalInstance);
1068
+ boundSettingsMethods.set(prop, bound);
1069
+ return bound;
1007
1070
  }
1008
1071
  return value;
1009
1072
  },
package/src/dap/config.ts CHANGED
@@ -27,6 +27,7 @@ function normalizeAdapterConfig(config: unknown): DapAdapterConfig | null {
27
27
  rootMarkers: normalizeStringArray(config.rootMarkers),
28
28
  launchDefaults: normalizeObject(config.launchDefaults),
29
29
  attachDefaults: normalizeObject(config.attachDefaults),
30
+ acceptsDirectoryProgram: config.acceptsDirectoryProgram === true,
30
31
  ...(connectMode ? { connectMode } : {}),
31
32
  };
32
33
  }
@@ -64,6 +65,7 @@ export function resolveAdapter(adapterName: string, cwd: string): DapResolvedAda
64
65
  launchDefaults: config.launchDefaults ?? {},
65
66
  attachDefaults: config.attachDefaults ?? {},
66
67
  connectMode: config.connectMode ?? "stdio",
68
+ acceptsDirectoryProgram: config.acceptsDirectoryProgram === true,
67
69
  };
68
70
  }
69
71
 
@@ -124,12 +126,19 @@ function sortAdaptersForLaunch(program: string, cwd: string, adapters: DapResolv
124
126
  return rootAware.map(entry => entry.adapter);
125
127
  }
126
128
 
127
- export function selectLaunchAdapter(program: string, cwd: string, adapterName?: string): DapResolvedAdapter | null {
129
+ export function selectLaunchAdapter(
130
+ program: string,
131
+ cwd: string,
132
+ adapterName?: string,
133
+ programKind: LaunchProgramKind = "file",
134
+ ): DapResolvedAdapter | null {
128
135
  if (adapterName) {
129
136
  return resolveAdapter(adapterName, cwd);
130
137
  }
131
138
  const matches = getMatchingAdapters(program, cwd);
132
- const sorted = sortAdaptersForLaunch(program, cwd, matches);
139
+ const candidates =
140
+ programKind === "directory" ? matches.filter(adapter => adapter.acceptsDirectoryProgram) : matches;
141
+ const sorted = sortAdaptersForLaunch(program, cwd, candidates.length > 0 ? candidates : matches);
133
142
  return sorted[0] ?? null;
134
143
  }
135
144
 
@@ -148,3 +157,33 @@ export function selectAttachAdapter(cwd: string, adapterName?: string, port?: nu
148
157
  }
149
158
  return available[0] ?? null;
150
159
  }
160
+
161
+ /** How the launch `program` resolves on disk. `"missing"` is reserved for
162
+ * programs the adapter creates on demand (rare); we treat them like files. */
163
+ export type LaunchProgramKind = "file" | "directory" | "missing";
164
+
165
+ /** Compute adapter-specific launch arguments that depend on the resolved
166
+ * program. Returned values are spread over `adapter.launchDefaults` so they
167
+ * take precedence over the static defaults but can still be overridden by
168
+ * the fields `DapSessionManager.launch` sets explicitly (program, cwd, args).
169
+ *
170
+ * Currently scoped to dlv, where `mode` selects how the program path is
171
+ * interpreted: directories and `.go` source files debug as a Go package
172
+ * (`mode=debug`), anything else is treated as a compiled binary (`mode=exec`).
173
+ */
174
+ export function resolveLaunchOverrides(
175
+ adapter: DapResolvedAdapter,
176
+ program: string,
177
+ programKind: LaunchProgramKind,
178
+ ): Record<string, unknown> {
179
+ if (adapter.name === "dlv") {
180
+ const extension = path.extname(program).toLowerCase();
181
+ if (programKind === "directory" || extension === ".go") {
182
+ return { mode: "debug" };
183
+ }
184
+ if (programKind === "file") {
185
+ return { mode: "exec" };
186
+ }
187
+ }
188
+ return {};
189
+ }
@@ -65,6 +65,7 @@
65
65
  "languages": ["go"],
66
66
  "fileTypes": [".go"],
67
67
  "rootMarkers": ["go.mod", "go.sum"],
68
+ "acceptsDirectoryProgram": true,
68
69
  "launchDefaults": {
69
70
  "request": "launch",
70
71
  "mode": "debug",
@@ -259,6 +259,7 @@ export class DapSessionManager {
259
259
  session.needsConfigurationDone = session.capabilities.supportsConfigurationDoneRequest === true;
260
260
  const launchArguments: DapLaunchArguments = {
261
261
  ...options.adapter.launchDefaults,
262
+ ...(options.extraLaunchArguments ?? {}),
262
263
  program: options.program,
263
264
  cwd: options.cwd,
264
265
  args: options.args,
package/src/dap/types.ts CHANGED
@@ -488,6 +488,10 @@ export interface DapAdapterConfig {
488
488
  * On Linux, connects via a unix domain socket.
489
489
  * On macOS, the adapter dials into a local TCP listener (--client-addr). */
490
490
  connectMode?: "stdio" | "socket";
491
+ /** When true, the adapter accepts a directory as the launch `program`
492
+ * (e.g. dlv treats it as a Go package path). When false/undefined, the
493
+ * debug tool rejects directory programs upfront. */
494
+ acceptsDirectoryProgram?: boolean;
491
495
  }
492
496
 
493
497
  export interface DapResolvedAdapter {
@@ -501,6 +505,7 @@ export interface DapResolvedAdapter {
501
505
  launchDefaults: Record<string, unknown>;
502
506
  attachDefaults: Record<string, unknown>;
503
507
  connectMode: "stdio" | "socket";
508
+ acceptsDirectoryProgram: boolean;
504
509
  }
505
510
 
506
511
  export interface DapBreakpointRecord {
@@ -589,6 +594,11 @@ export interface DapLaunchSessionOptions {
589
594
  program: string;
590
595
  args?: string[];
591
596
  cwd: string;
597
+ /** Per-launch overrides merged over `adapter.launchDefaults`. Used to
598
+ * inject adapter-specific values that depend on the resolved program
599
+ * (e.g. dlv's `mode` switches between `debug` and `exec` based on
600
+ * whether `program` is a Go package path or a compiled binary). */
601
+ extraLaunchArguments?: Record<string, unknown>;
592
602
  }
593
603
 
594
604
  export interface DapAttachSessionOptions {