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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/CHANGELOG.md +75 -1
  2. package/dist/types/cli/dry-balance-cli.d.ts +15 -1
  3. package/dist/types/commit/analysis/conventional.d.ts +2 -2
  4. package/dist/types/commit/analysis/summary.d.ts +2 -2
  5. package/dist/types/commit/changelog/generate.d.ts +2 -2
  6. package/dist/types/commit/changelog/index.d.ts +2 -2
  7. package/dist/types/commit/map-reduce/index.d.ts +3 -3
  8. package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
  9. package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
  10. package/dist/types/commit/model-selection.d.ts +10 -4
  11. package/dist/types/config/api-key-resolver.d.ts +34 -0
  12. package/dist/types/config/model-registry.d.ts +17 -1
  13. package/dist/types/config/settings-schema.d.ts +9 -0
  14. package/dist/types/dap/config.d.ts +14 -1
  15. package/dist/types/dap/types.d.ts +10 -0
  16. package/dist/types/lsp/utils.d.ts +3 -2
  17. package/dist/types/modes/components/chat-block.d.ts +64 -0
  18. package/dist/types/modes/components/custom-editor.d.ts +3 -0
  19. package/dist/types/modes/components/overlay-box.d.ts +17 -0
  20. package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
  21. package/dist/types/modes/components/plan-toc.d.ts +41 -0
  22. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  23. package/dist/types/modes/components/transcript-container.d.ts +11 -0
  24. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  25. package/dist/types/modes/controllers/event-controller.d.ts +0 -1
  26. package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
  27. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  28. package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
  29. package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
  30. package/dist/types/modes/interactive-mode.d.ts +15 -5
  31. package/dist/types/modes/theme/theme.d.ts +1 -1
  32. package/dist/types/modes/types.d.ts +18 -5
  33. package/dist/types/modes/utils/copy-targets.d.ts +21 -1
  34. package/dist/types/plan-mode/approved-plan.d.ts +27 -8
  35. package/dist/types/plan-mode/plan-protection.d.ts +4 -4
  36. package/dist/types/sdk.d.ts +2 -0
  37. package/dist/types/session/agent-session.d.ts +21 -0
  38. package/dist/types/session/messages.d.ts +12 -0
  39. package/dist/types/session/session-manager.d.ts +3 -1
  40. package/dist/types/slash-commands/types.d.ts +4 -6
  41. package/dist/types/task/executor.d.ts +7 -0
  42. package/dist/types/task/index.d.ts +1 -0
  43. package/dist/types/task/render.d.ts +3 -2
  44. package/dist/types/tools/archive-reader.d.ts +5 -0
  45. package/dist/types/tools/ast-edit.d.ts +3 -0
  46. package/dist/types/tools/ast-grep.d.ts +3 -0
  47. package/dist/types/tools/bash.d.ts +1 -0
  48. package/dist/types/tools/find.d.ts +8 -4
  49. package/dist/types/tools/grouped-file-output.d.ts +95 -12
  50. package/dist/types/tools/memory-render.d.ts +4 -1
  51. package/dist/types/tools/plan-mode-guard.d.ts +8 -9
  52. package/dist/types/tools/render-utils.d.ts +5 -9
  53. package/dist/types/tools/search.d.ts +4 -0
  54. package/dist/types/tools/sqlite-reader.d.ts +1 -0
  55. package/dist/types/tools/todo.d.ts +3 -2
  56. package/dist/types/tools/write.d.ts +3 -0
  57. package/dist/types/tui/output-block.d.ts +16 -4
  58. package/dist/types/tui/status-line.d.ts +3 -0
  59. package/dist/types/utils/enhanced-paste.d.ts +20 -0
  60. package/dist/types/web/search/providers/kimi.d.ts +1 -1
  61. package/package.json +9 -9
  62. package/src/auto-thinking/classifier.ts +5 -1
  63. package/src/cli/dry-balance-cli.ts +52 -17
  64. package/src/cli/gallery-cli.ts +4 -1
  65. package/src/cli/gallery-fixtures/misc.ts +29 -0
  66. package/src/commit/analysis/conventional.ts +2 -2
  67. package/src/commit/analysis/summary.ts +2 -2
  68. package/src/commit/changelog/generate.ts +2 -2
  69. package/src/commit/changelog/index.ts +2 -2
  70. package/src/commit/map-reduce/index.ts +3 -3
  71. package/src/commit/map-reduce/map-phase.ts +2 -2
  72. package/src/commit/map-reduce/reduce-phase.ts +2 -2
  73. package/src/commit/model-selection.ts +33 -9
  74. package/src/commit/pipeline.ts +4 -4
  75. package/src/config/api-key-resolver.ts +58 -0
  76. package/src/config/model-registry.ts +25 -2
  77. package/src/config/settings-schema.ts +10 -0
  78. package/src/config/settings.ts +20 -2
  79. package/src/dap/config.ts +41 -2
  80. package/src/dap/defaults.json +1 -0
  81. package/src/dap/session.ts +1 -0
  82. package/src/dap/types.ts +10 -0
  83. package/src/debug/index.ts +40 -54
  84. package/src/edit/renderer.ts +82 -78
  85. package/src/eval/__tests__/llm-bridge.test.ts +90 -31
  86. package/src/eval/llm-bridge.ts +8 -3
  87. package/src/goals/tools/goal-tool.ts +36 -26
  88. package/src/internal-urls/docs-index.generated.ts +6 -6
  89. package/src/lsp/utils.ts +3 -2
  90. package/src/main.ts +9 -7
  91. package/src/memories/index.ts +12 -5
  92. package/src/mnemopi/backend.ts +5 -1
  93. package/src/modes/acp/acp-agent.ts +33 -26
  94. package/src/modes/components/assistant-message.ts +2 -9
  95. package/src/modes/components/chat-block.ts +111 -0
  96. package/src/modes/components/copy-selector.ts +1 -44
  97. package/src/modes/components/custom-editor.ts +23 -0
  98. package/src/modes/components/custom-message.ts +1 -3
  99. package/src/modes/components/execution-shared.ts +1 -2
  100. package/src/modes/components/hook-message.ts +1 -3
  101. package/src/modes/components/overlay-box.ts +108 -0
  102. package/src/modes/components/plan-review-overlay.ts +799 -0
  103. package/src/modes/components/plan-toc.ts +138 -0
  104. package/src/modes/components/read-tool-group.ts +20 -4
  105. package/src/modes/components/skill-message.ts +0 -1
  106. package/src/modes/components/tips.txt +1 -0
  107. package/src/modes/components/todo-reminder.ts +0 -2
  108. package/src/modes/components/tool-execution.ts +68 -88
  109. package/src/modes/components/transcript-container.ts +84 -24
  110. package/src/modes/components/user-message.ts +1 -2
  111. package/src/modes/controllers/command-controller-shared.ts +7 -6
  112. package/src/modes/controllers/command-controller.ts +57 -55
  113. package/src/modes/controllers/event-controller.ts +41 -40
  114. package/src/modes/controllers/extension-ui-controller.ts +10 -73
  115. package/src/modes/controllers/input-controller.ts +124 -119
  116. package/src/modes/controllers/mcp-command-controller.ts +69 -60
  117. package/src/modes/controllers/selector-controller.ts +23 -25
  118. package/src/modes/controllers/streaming-reveal.ts +212 -0
  119. package/src/modes/controllers/tan-command-controller.ts +173 -0
  120. package/src/modes/interactive-mode.ts +169 -94
  121. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  122. package/src/modes/theme/theme-schema.json +1 -1
  123. package/src/modes/theme/theme.ts +8 -4
  124. package/src/modes/types.ts +18 -7
  125. package/src/modes/utils/copy-targets.ts +133 -27
  126. package/src/modes/utils/ui-helpers.ts +44 -46
  127. package/src/plan-mode/approved-plan.ts +66 -43
  128. package/src/plan-mode/plan-protection.ts +4 -4
  129. package/src/prompts/system/background-tan-dispatch.md +8 -0
  130. package/src/prompts/system/plan-mode-active.md +67 -58
  131. package/src/prompts/system/plan-mode-approved.md +1 -1
  132. package/src/sdk.ts +11 -37
  133. package/src/session/agent-session.ts +82 -6
  134. package/src/session/messages.ts +26 -0
  135. package/src/session/session-manager.ts +13 -5
  136. package/src/slash-commands/builtin-registry.ts +36 -9
  137. package/src/slash-commands/types.ts +4 -6
  138. package/src/task/executor.ts +5 -2
  139. package/src/task/index.ts +4 -0
  140. package/src/task/render.ts +212 -147
  141. package/src/tools/archive-reader.ts +64 -0
  142. package/src/tools/ask.ts +119 -164
  143. package/src/tools/ast-edit.ts +98 -71
  144. package/src/tools/ast-grep.ts +37 -43
  145. package/src/tools/bash.ts +50 -6
  146. package/src/tools/debug.ts +20 -8
  147. package/src/tools/fetch.ts +297 -7
  148. package/src/tools/find.ts +44 -30
  149. package/src/tools/gh-renderer.ts +81 -42
  150. package/src/tools/grouped-file-output.ts +272 -48
  151. package/src/tools/image-gen.ts +150 -103
  152. package/src/tools/inspect-image-renderer.ts +63 -41
  153. package/src/tools/inspect-image.ts +8 -1
  154. package/src/tools/job.ts +3 -4
  155. package/src/tools/memory-render.ts +4 -1
  156. package/src/tools/plan-mode-guard.ts +21 -39
  157. package/src/tools/read.ts +23 -16
  158. package/src/tools/render-utils.ts +21 -37
  159. package/src/tools/resolve.ts +14 -0
  160. package/src/tools/search-tool-bm25.ts +36 -23
  161. package/src/tools/search.ts +80 -78
  162. package/src/tools/sqlite-reader.ts +9 -12
  163. package/src/tools/todo.ts +118 -52
  164. package/src/tools/write.ts +81 -62
  165. package/src/tui/output-block.ts +60 -13
  166. package/src/tui/status-line.ts +5 -1
  167. package/src/utils/commit-message-generator.ts +9 -1
  168. package/src/utils/enhanced-paste.ts +202 -0
  169. package/src/utils/title-generator.ts +2 -1
  170. package/src/web/search/providers/anthropic.ts +25 -19
  171. package/src/web/search/providers/exa.ts +11 -3
  172. package/src/web/search/providers/kimi.ts +28 -17
  173. package/src/web/search/providers/parallel.ts +35 -24
  174. package/src/web/search/providers/synthetic.ts +8 -6
  175. package/src/web/search/providers/tavily.ts +9 -8
  176. package/src/web/search/providers/zai.ts +8 -6
@@ -1,5 +1,5 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
2
+ import type { Api, ApiKey, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
3
3
  import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
5
  import * as z from "zod/v4";
@@ -25,7 +25,7 @@ export const changelogTool = {
25
25
 
26
26
  export interface ChangelogPromptInput {
27
27
  model: Model<Api>;
28
- apiKey: string;
28
+ apiKey: ApiKey;
29
29
  thinkingLevel?: ThinkingLevel;
30
30
  changelogPath: string;
31
31
  isPackageChangelog: boolean;
@@ -1,6 +1,6 @@
1
1
  import * as path from "node:path";
2
2
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
3
- import type { Api, Model } from "@oh-my-pi/pi-ai";
3
+ import type { Api, ApiKey, Model } from "@oh-my-pi/pi-ai";
4
4
  import { logger } from "@oh-my-pi/pi-utils";
5
5
  import { CHANGELOG_CATEGORIES } from "../../commit/types";
6
6
  import * as git from "../../utils/git";
@@ -15,7 +15,7 @@ const DEFAULT_MAX_DIFF_CHARS = 120_000;
15
15
  export interface ChangelogFlowInput {
16
16
  cwd: string;
17
17
  model: Model<Api>;
18
- apiKey: string;
18
+ apiKey: ApiKey;
19
19
  thinkingLevel?: ThinkingLevel;
20
20
  stagedFiles: string[];
21
21
  dryRun: boolean;
@@ -1,5 +1,5 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import type { Api, Model } from "@oh-my-pi/pi-ai";
2
+ import type { Api, ApiKey, Model } from "@oh-my-pi/pi-ai";
3
3
  import { $env } from "@oh-my-pi/pi-utils";
4
4
  import { parseFileDiffs } from "../../commit/git/diff";
5
5
  import type { ConventionalAnalysis } from "../../commit/types";
@@ -21,10 +21,10 @@ export interface MapReduceSettings {
21
21
 
22
22
  export interface MapReduceInput {
23
23
  model: Model<Api>;
24
- apiKey: string;
24
+ apiKey: ApiKey;
25
25
  thinkingLevel?: ThinkingLevel;
26
26
  smolModel: Model<Api>;
27
- smolApiKey: string;
27
+ smolApiKey: ApiKey;
28
28
  smolThinkingLevel?: ThinkingLevel;
29
29
  diff: string;
30
30
  stat: string;
@@ -1,5 +1,5 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import type { Api, AssistantMessage, Message, Model } from "@oh-my-pi/pi-ai";
2
+ import type { Api, ApiKey, AssistantMessage, Message, Model } from "@oh-my-pi/pi-ai";
3
3
  import { completeSimple } from "@oh-my-pi/pi-ai";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
5
  import fileObserverSystemPrompt from "../../commit/prompts/file-observer-system.md" with { type: "text" };
@@ -18,7 +18,7 @@ const RETRY_BACKOFF_MS = 1000;
18
18
 
19
19
  export interface MapPhaseInput {
20
20
  model: Model<Api>;
21
- apiKey: string;
21
+ apiKey: ApiKey;
22
22
  thinkingLevel?: ThinkingLevel;
23
23
  files: FileDiff[];
24
24
  config?: {
@@ -1,5 +1,5 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import type { Api, Model } from "@oh-my-pi/pi-ai";
2
+ import type { Api, ApiKey, Model } from "@oh-my-pi/pi-ai";
3
3
  import { completeSimple } from "@oh-my-pi/pi-ai";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
5
  import reduceSystemPrompt from "../../commit/prompts/reduce-system.md" with { type: "text" };
@@ -12,7 +12,7 @@ const ReduceTool = createConventionalAnalysisTool("Synthesize file observations
12
12
 
13
13
  export interface ReducePhaseInput {
14
14
  model: Model<Api>;
15
- apiKey: string;
15
+ apiKey: ApiKey;
16
16
  thinkingLevel?: ThinkingLevel;
17
17
  observations: FileObservation[];
18
18
  stat: string;
@@ -1,5 +1,6 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import type { Api, Model } from "@oh-my-pi/pi-ai";
2
+ import type { Api, ApiKey, Model } from "@oh-my-pi/pi-ai";
3
+ import type { ApiKeyResolverRegistry } from "../config/api-key-resolver";
3
4
  import { MODEL_ROLE_IDS } from "../config/model-registry";
4
5
  import {
5
6
  type ModelLookupRegistry,
@@ -12,13 +13,19 @@ import MODEL_PRIO from "../priority.json" with { type: "json" };
12
13
 
13
14
  export interface ResolvedCommitModel {
14
15
  model: Model<Api>;
15
- apiKey: string;
16
+ /**
17
+ * Resolver for the model's bearer: re-resolves on 401 / usage-limit so the
18
+ * whole commit pipeline (analysis, map/reduce, changelog) inherits the
19
+ * central force-refresh + account-rotation policy.
20
+ */
21
+ apiKey: ApiKey;
16
22
  thinkingLevel?: ThinkingLevel;
17
23
  }
18
24
 
19
- type CommitModelRegistry = ModelLookupRegistry & {
20
- getApiKey: (model: Model<Api>) => Promise<string | undefined>;
21
- };
25
+ type CommitModelRegistry = ModelLookupRegistry &
26
+ ApiKeyResolverRegistry & {
27
+ getApiKey: (model: Model<Api>) => Promise<string | undefined>;
28
+ };
22
29
 
23
30
  export async function resolvePrimaryModel(
24
31
  override: string | undefined,
@@ -38,20 +45,32 @@ export async function resolvePrimaryModel(
38
45
  if (!apiKey) {
39
46
  throw new Error(`No API key available for model ${model.provider}/${model.id}`);
40
47
  }
41
- return { model, apiKey, thinkingLevel: resolved?.thinkingLevel };
48
+ return {
49
+ model,
50
+ apiKey: modelRegistry.resolver(model.provider, { baseUrl: model.baseUrl }),
51
+ thinkingLevel: resolved?.thinkingLevel,
52
+ };
42
53
  }
43
54
 
44
55
  export async function resolveSmolModel(
45
56
  settings: Settings,
46
57
  modelRegistry: CommitModelRegistry,
47
58
  fallbackModel: Model<Api>,
48
- fallbackApiKey: string,
59
+ fallbackApiKey: ApiKey,
49
60
  ): Promise<ResolvedCommitModel> {
50
61
  const available = modelRegistry.getAvailable();
51
62
  const resolvedSmol = resolveRoleSelection(["smol"], settings, available, modelRegistry);
52
63
  if (resolvedSmol?.model) {
53
64
  const apiKey = await modelRegistry.getApiKey(resolvedSmol.model);
54
- if (apiKey) return { model: resolvedSmol.model, apiKey, thinkingLevel: resolvedSmol.thinkingLevel };
65
+ if (apiKey) {
66
+ return {
67
+ model: resolvedSmol.model,
68
+ apiKey: modelRegistry.resolver(resolvedSmol.model.provider, {
69
+ baseUrl: resolvedSmol.model.baseUrl,
70
+ }),
71
+ thinkingLevel: resolvedSmol.thinkingLevel,
72
+ };
73
+ }
55
74
  }
56
75
 
57
76
  const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
@@ -59,7 +78,12 @@ export async function resolveSmolModel(
59
78
  const candidate = parseModelPattern(pattern, available, matchPreferences, { modelRegistry }).model;
60
79
  if (!candidate) continue;
61
80
  const apiKey = await modelRegistry.getApiKey(candidate);
62
- if (apiKey) return { model: candidate, apiKey };
81
+ if (apiKey) {
82
+ return {
83
+ model: candidate,
84
+ apiKey: modelRegistry.resolver(candidate.provider, { baseUrl: candidate.baseUrl }),
85
+ };
86
+ }
63
87
  }
64
88
 
65
89
  return { model: fallbackModel, apiKey: fallbackApiKey };
@@ -1,6 +1,6 @@
1
1
  import * as path from "node:path";
2
2
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
3
- import type { Api, Model } from "@oh-my-pi/pi-ai";
3
+ import type { Api, ApiKey, Model } from "@oh-my-pi/pi-ai";
4
4
  import { getProjectDir, logger, prompt } from "@oh-my-pi/pi-utils";
5
5
  import { ModelRegistry } from "../config/model-registry";
6
6
  import { Settings } from "../config/settings";
@@ -145,10 +145,10 @@ async function generateAnalysis(input: {
145
145
  contextFiles: Array<{ path: string; content: string }>;
146
146
  userContext?: string;
147
147
  primaryModel: Model<Api>;
148
- primaryApiKey: string;
148
+ primaryApiKey: ApiKey;
149
149
  primaryThinkingLevel?: ThinkingLevel;
150
150
  smolModel: Model<Api>;
151
- smolApiKey: string;
151
+ smolApiKey: ApiKey;
152
152
  smolThinkingLevel?: ThinkingLevel;
153
153
  commitSettings: {
154
154
  mapReduceEnabled: boolean;
@@ -206,7 +206,7 @@ async function generateSummaryWithRetry(input: {
206
206
  analysis: ConventionalAnalysis;
207
207
  stat: string;
208
208
  model: Model<Api>;
209
- apiKey: string;
209
+ apiKey: ApiKey;
210
210
  thinkingLevel?: ThinkingLevel;
211
211
  userContext?: string;
212
212
  }): Promise<{ summary: string }> {
@@ -0,0 +1,58 @@
1
+ import type { ApiKeyResolver, AuthStorage } from "@oh-my-pi/pi-ai";
2
+
3
+ export interface ApiKeyResolverOptions {
4
+ /** Session id for credential stickiness; read at resolve time by the caller. */
5
+ sessionId?: string;
6
+ /** Provider base URL hint forwarded to the auth-storage cascade. */
7
+ baseUrl?: string;
8
+ }
9
+
10
+ /**
11
+ * Minimal slice of `ModelRegistry` the resolver needs. Typed structurally so
12
+ * narrower registry shells (e.g. the commit pipeline's `CommitModelRegistry`)
13
+ * can build resolvers without depending on the full class.
14
+ */
15
+ export interface ApiKeyResolverRegistry {
16
+ getApiKeyForProvider(
17
+ provider: string,
18
+ sessionId?: string,
19
+ options?: { baseUrl?: string; forceRefresh?: boolean; signal?: AbortSignal },
20
+ ): Promise<string | undefined>;
21
+ authStorage: Pick<AuthStorage, "rotateSessionCredential">;
22
+ /**
23
+ * Build an {@link ApiKeyResolver} implementing the central a/b/c auth-retry
24
+ * policy: initial → resolve; step (b) → force-refresh same account; step (c)
25
+ * → rotate to a sibling credential, then re-resolve.
26
+ *
27
+ * The resolver is stateless (safe to reuse across requests). Callers that
28
+ * need the initial key for a guard can call `resolveApiKeyOnce(resolver)`.
29
+ */
30
+ resolver(provider: string, options?: ApiKeyResolverOptions): ApiKeyResolver;
31
+ }
32
+
33
+ /**
34
+ * Default implementation of {@link ApiKeyResolverRegistry.resolver}.
35
+ * Also usable standalone for structural registries that don't carry the method.
36
+ */
37
+ export function createApiKeyResolver(
38
+ registry: Pick<ApiKeyResolverRegistry, "getApiKeyForProvider" | "authStorage">,
39
+ provider: string,
40
+ options: ApiKeyResolverOptions = {},
41
+ ): ApiKeyResolver {
42
+ const { sessionId, baseUrl } = options;
43
+ return async ({ lastChance, error, signal }) => {
44
+ if (error === undefined) {
45
+ return registry.getApiKeyForProvider(provider, sessionId, { baseUrl });
46
+ }
47
+ if (lastChance) {
48
+ // Account constraint (401 / usage / account-rate-limit): rotate to a
49
+ // sibling credential. We do NOT honor any retry-after here — if a
50
+ // sibling exists we switch immediately; the precise no-sibling backoff
51
+ // is owned by `markUsageLimitReached` (default + server usage-report
52
+ // reset) and the outer whole-turn retry layer.
53
+ await registry.authStorage.rotateSessionCredential(provider, sessionId, { error, signal });
54
+ return registry.getApiKeyForProvider(provider, sessionId, { baseUrl });
55
+ }
56
+ return registry.getApiKeyForProvider(provider, sessionId, { baseUrl, forceRefresh: true, signal });
57
+ };
58
+ }
@@ -95,12 +95,14 @@ const STARTUP_MODEL_CACHE_PROVIDER_IDS: readonly string[] = [
95
95
  ...SPECIAL_MODEL_MANAGER_PROVIDER_IDS,
96
96
  ];
97
97
 
98
+ import type { ApiKeyResolver } from "@oh-my-pi/pi-ai";
98
99
  import { registerOAuthProvider, unregisterOAuthProviders } from "@oh-my-pi/pi-ai/utils/oauth";
99
100
  import type { OAuthCredentials, OAuthLoginCallbacks } from "@oh-my-pi/pi-ai/utils/oauth/types";
100
101
  import { isRecord, logger } from "@oh-my-pi/pi-utils";
101
102
  import { parseModelString, resolveProviderModelReference } from "../config/model-resolver";
102
103
  import { isValidThemeColor, type ThemeColor } from "../modes/theme/theme";
103
104
  import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
105
+ import { type ApiKeyResolverOptions, createApiKeyResolver } from "./api-key-resolver";
104
106
  import { type ConfigError, ConfigFile } from "./config-file";
105
107
  import {
106
108
  buildCanonicalModelIndex,
@@ -2365,12 +2367,33 @@ export class ModelRegistry {
2365
2367
 
2366
2368
  /**
2367
2369
  * Get API key for a provider (e.g., "openai").
2370
+ *
2371
+ * `options.forceRefresh` powers step (b) of the auth-retry policy — it
2372
+ * re-mints the session-sticky OAuth token even when the cached copy still
2373
+ * looks valid. `options.signal` is threaded into any broker-bound refresh.
2368
2374
  */
2369
- async getApiKeyForProvider(provider: string, sessionId?: string, baseUrl?: string): Promise<string | undefined> {
2375
+ async getApiKeyForProvider(
2376
+ provider: string,
2377
+ sessionId?: string,
2378
+ options?: { baseUrl?: string; forceRefresh?: boolean; signal?: AbortSignal },
2379
+ ): Promise<string | undefined> {
2370
2380
  if (this.#keylessProviders.has(provider) && !this.authStorage.hasAuth(provider)) {
2371
2381
  return kNoAuth;
2372
2382
  }
2373
- return this.authStorage.getApiKey(provider, sessionId, { baseUrl });
2383
+ return this.authStorage.getApiKey(provider, sessionId, {
2384
+ baseUrl: options?.baseUrl,
2385
+ forceRefresh: options?.forceRefresh,
2386
+ signal: options?.signal,
2387
+ });
2388
+ }
2389
+
2390
+ /**
2391
+ * Build an {@link ApiKeyResolver} for this provider, implementing the
2392
+ * central a/b/c auth-retry policy. Callers that need the initial key for
2393
+ * a guard can call `resolveApiKeyOnce(resolver)`.
2394
+ */
2395
+ resolver(provider: string, options?: ApiKeyResolverOptions): ApiKeyResolver {
2396
+ return createApiKeyResolver(this, provider, options);
2374
2397
  }
2375
2398
 
2376
2399
  async #peekApiKeyForProvider(provider: string): Promise<string | undefined> {
@@ -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,
@@ -240,11 +240,13 @@ export class Settings {
240
240
  return promise.then(
241
241
  instance => {
242
242
  globalInstance = instance;
243
+ clearBoundSettingsMethods();
243
244
  globalInstancePromise = Promise.resolve(instance);
244
245
  return instance;
245
246
  },
246
247
  error => {
247
248
  globalInstance = null;
249
+ clearBoundSettingsMethods();
248
250
  throw error;
249
251
  },
250
252
  );
@@ -978,6 +980,13 @@ export function onHindsightScopeChanged(cb: () => void): () => void {
978
980
 
979
981
  let globalInstance: Settings | null = null;
980
982
  let globalInstancePromise: Promise<Settings> | null = null;
983
+ let boundSettingsInstance: Settings | null = null;
984
+ let boundSettingsMethods = new Map<PropertyKey, unknown>();
985
+
986
+ function clearBoundSettingsMethods(): void {
987
+ boundSettingsInstance = null;
988
+ boundSettingsMethods = new Map<PropertyKey, unknown>();
989
+ }
981
990
 
982
991
  export function isSettingsInitialized(): boolean {
983
992
  return globalInstance !== null;
@@ -990,6 +999,7 @@ export function isSettingsInitialized(): boolean {
990
999
  export function resetSettingsForTest(): void {
991
1000
  globalInstance = null;
992
1001
  globalInstancePromise = null;
1002
+ clearBoundSettingsMethods();
993
1003
  }
994
1004
 
995
1005
  /**
@@ -1001,9 +1011,17 @@ export const settings = new Proxy({} as Settings, {
1001
1011
  if (!globalInstance) {
1002
1012
  throw new Error("Settings not initialized. Call Settings.init() first.");
1003
1013
  }
1004
- const value = (globalInstance as unknown as Record<string | symbol, unknown>)[prop];
1014
+ if (boundSettingsInstance !== globalInstance) {
1015
+ clearBoundSettingsMethods();
1016
+ boundSettingsInstance = globalInstance;
1017
+ }
1018
+ const value = (globalInstance as unknown as Record<PropertyKey, unknown>)[prop];
1005
1019
  if (typeof value === "function") {
1006
- return value.bind(globalInstance);
1020
+ const cached = boundSettingsMethods.get(prop);
1021
+ if (cached) return cached;
1022
+ const bound = value.bind(globalInstance);
1023
+ boundSettingsMethods.set(prop, bound);
1024
+ return bound;
1007
1025
  }
1008
1026
  return value;
1009
1027
  },
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 {