@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,7 +17,7 @@ import {
17
17
  import { formatNumber } from "@oh-my-pi/pi-utils";
18
18
  import type { ModelRegistry } from "../../config/model-registry";
19
19
  import { getKnownRoleIds, getRoleInfo, MODEL_ROLE_IDS, MODEL_ROLES } from "../../config/model-registry";
20
- import { resolveModelRoleValue } from "../../config/model-resolver";
20
+ import { getModelMatchPreferences, resolveModelRoleValue } from "../../config/model-resolver";
21
21
  import type { Settings } from "../../config/settings";
22
22
  import { type ThemeColor, theme } from "../../modes/theme/theme";
23
23
  import { matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
@@ -31,6 +31,25 @@ function makeInvertedBadge(label: string, color: ThemeColor): string {
31
31
  return `${bgAnsi}\x1b[30m ${label} \x1b[39m\x1b[49m`;
32
32
  }
33
33
 
34
+ function makeAutoSelectedBadge(label: string, color: ThemeColor): string {
35
+ return `${theme.fg("dim", "[")}${theme.fg(color, label)}${theme.fg("dim", " auto]")}`;
36
+ }
37
+
38
+ function makeRoleBadgeToken(label: string, color: ThemeColor, assigned: RoleAssignment): string {
39
+ if (assigned.autoSelected) {
40
+ const badge = makeAutoSelectedBadge(label, color);
41
+ if (assigned.thinkingLevel === ThinkingLevel.Inherit) {
42
+ return badge;
43
+ }
44
+ const thinkingLabel = getConfiguredThinkingLevelMetadata(assigned.thinkingLevel).label;
45
+ return `${badge} ${theme.fg("dim", `(${thinkingLabel})`)}`;
46
+ }
47
+
48
+ const badge = makeInvertedBadge(label, color);
49
+ const thinkingLabel = getConfiguredThinkingLevelMetadata(assigned.thinkingLevel).label;
50
+ return `${badge} ${theme.fg("dim", `(${thinkingLabel})`)}`;
51
+ }
52
+
34
53
  function normalizeSearchText(value: string): string {
35
54
  return value
36
55
  .toLowerCase()
@@ -86,6 +105,7 @@ interface ScopedModelItem {
86
105
  interface RoleAssignment {
87
106
  model: Model;
88
107
  thinkingLevel: ConfiguredThinkingLevel;
108
+ autoSelected: boolean;
89
109
  }
90
110
 
91
111
  type RoleSelectCallback = (
@@ -271,12 +291,17 @@ export class ModelSelectorComponent extends Container {
271
291
  });
272
292
  }
273
293
 
274
- #loadRoleModels(): void {
294
+ #loadRoleModels(autoCandidateModels?: ReadonlyArray<Model>): void {
295
+ const nextRoles = {} as Record<string, RoleAssignment | undefined>;
275
296
  const allModels = this.#modelRegistry.getAll();
276
- const matchPreferences = { usageOrder: this.#settings.getStorage()?.getModelUsageOrder() };
277
- for (const role of getKnownRoleIds(this.#settings)) {
297
+ const matchPreferences = getModelMatchPreferences(this.#settings);
298
+ const knownRoles = getKnownRoleIds(this.#settings);
299
+ const configuredRoles = new Set<string>();
300
+
301
+ for (const role of knownRoles) {
278
302
  const roleValue = this.#settings.getModelRole(role);
279
303
  if (!roleValue) continue;
304
+ configuredRoles.add(role);
280
305
 
281
306
  const resolved = resolveModelRoleValue(roleValue, allModels, {
282
307
  settings: this.#settings,
@@ -284,15 +309,39 @@ export class ModelSelectorComponent extends Container {
284
309
  modelRegistry: this.#modelRegistry,
285
310
  });
286
311
  if (resolved.model) {
287
- this.#roles[role] = {
312
+ nextRoles[role] = {
288
313
  model: resolved.model,
289
314
  thinkingLevel:
290
315
  resolved.explicitThinkingLevel && resolved.thinkingLevel !== undefined
291
316
  ? resolved.thinkingLevel
292
317
  : ThinkingLevel.Inherit,
318
+ autoSelected: false,
293
319
  };
294
320
  }
295
321
  }
322
+
323
+ if (autoCandidateModels && autoCandidateModels.length > 0) {
324
+ const candidates = [...autoCandidateModels];
325
+ for (const role of knownRoles) {
326
+ if (configuredRoles.has(role)) continue;
327
+ const resolved = resolveModelRoleValue(`pi/${role}`, candidates, {
328
+ settings: this.#settings,
329
+ matchPreferences,
330
+ modelRegistry: this.#modelRegistry,
331
+ });
332
+ if (!resolved.model) continue;
333
+ nextRoles[role] = {
334
+ model: resolved.model,
335
+ thinkingLevel:
336
+ resolved.explicitThinkingLevel && resolved.thinkingLevel !== undefined
337
+ ? resolved.thinkingLevel
338
+ : ThinkingLevel.Inherit,
339
+ autoSelected: true,
340
+ };
341
+ }
342
+ }
343
+
344
+ this.#roles = nextRoles;
296
345
  }
297
346
 
298
347
  /**
@@ -427,6 +476,7 @@ export class ModelSelectorComponent extends Container {
427
476
  }
428
477
 
429
478
  const candidates = models.map(item => item.model);
479
+ this.#loadRoleModels(candidates);
430
480
  const canonicalRecords = this.#modelRegistry.getCanonicalModels({
431
481
  availableOnly: this.#scopedModels.length === 0,
432
482
  candidates,
@@ -871,25 +921,21 @@ export class ModelSelectorComponent extends Container {
871
921
  const isDisabled = this.#isItemDisabled(item);
872
922
  const disabledSuffix = this.#formatContextLimitSuffix(item.model);
873
923
 
874
- // Build role badges (inverted: color as background, black text)
924
+ // Build role badges. Solid badges are configured; outlined badges are auto-selected defaults.
875
925
  const roleBadgeTokens: string[] = [];
876
926
  for (const role of MODEL_ROLE_IDS) {
877
927
  const { tag, color } = getRoleInfo(role, this.#settings);
878
928
  const assigned = this.#roles[role];
879
929
  if (!tag || !assigned || !modelsAreEqual(assigned.model, item.model)) continue;
880
930
 
881
- const badge = makeInvertedBadge(tag, color ?? "success");
882
- const thinkingLabel = getConfiguredThinkingLevelMetadata(assigned.thinkingLevel).label;
883
- roleBadgeTokens.push(`${badge} ${theme.fg("dim", `(${thinkingLabel})`)}`);
931
+ roleBadgeTokens.push(makeRoleBadgeToken(tag, color ?? "success", assigned));
884
932
  }
885
933
  // Custom role badges
886
934
  for (const [role, assigned] of Object.entries(this.#roles)) {
887
935
  if (role in MODEL_ROLES || !assigned || !modelsAreEqual(assigned.model, item.model)) continue;
888
936
  const roleInfo = getRoleInfo(role, this.#settings);
889
937
  const badgeLabel = roleInfo.tag ?? roleInfo.name;
890
- const badge = makeInvertedBadge(badgeLabel, roleInfo.color ?? "muted");
891
- const thinkingLabel = getConfiguredThinkingLevelMetadata(assigned.thinkingLevel).label;
892
- roleBadgeTokens.push(`${badge} ${theme.fg("dim", `(${thinkingLabel})`)}`);
938
+ roleBadgeTokens.push(makeRoleBadgeToken(badgeLabel, roleInfo.color ?? "muted", assigned));
893
939
  }
894
940
  const badgeText = roleBadgeTokens.length > 0 ? ` ${roleBadgeTokens.join(" ")}` : "";
895
941
 
@@ -1184,7 +1230,7 @@ export class ModelSelectorComponent extends Container {
1184
1230
  const selectedThinkingLevel = thinkingLevel ?? this.#getCurrentRoleThinkingLevel(role);
1185
1231
 
1186
1232
  // Update local state for UI
1187
- this.#roles[role] = { model: item.model, thinkingLevel: selectedThinkingLevel };
1233
+ this.#roles[role] = { model: item.model, thinkingLevel: selectedThinkingLevel, autoSelected: false };
1188
1234
 
1189
1235
  // Notify caller (for updating agent state if needed)
1190
1236
  this.#onSelectCallback(item.model, role, selectedThinkingLevel, item.selector);
@@ -11,10 +11,20 @@ import {
11
11
  } from "@oh-my-pi/pi-tui";
12
12
  import { theme } from "../../modes/theme/theme";
13
13
  import { matchesSelectCancel, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
14
- import type { AuthStorage } from "../../session/auth-storage";
14
+ import type { AuthStorage, CredentialOriginKind } from "../../session/auth-storage";
15
15
  import { DynamicBorder } from "./dynamic-border";
16
16
 
17
17
  const OAUTH_SELECTOR_MAX_VISIBLE = 10;
18
+
19
+ /** Compact, human-readable tag for each credential-origin leg. */
20
+ const ORIGIN_LABELS: Record<CredentialOriginKind, string> = {
21
+ runtime: "--api-key",
22
+ config: "config",
23
+ oauth: "login",
24
+ api_key: "api key",
25
+ env: "env",
26
+ fallback: "custom provider",
27
+ };
18
28
  /**
19
29
  * Component that renders an OAuth provider selector.
20
30
  */
@@ -146,20 +156,34 @@ export class OAuthSelectorComponent extends Container {
146
156
  }
147
157
  }
148
158
 
159
+ /**
160
+ * Muted provenance suffix (" (env: COPILOT_GITHUB_TOKEN)", " (login)", …) so
161
+ * the list distinguishes a real login from an env var aliasing the provider.
162
+ */
163
+ #getSourceLabel(providerId: string): string {
164
+ const origin = this.#authStorage.getCredentialOrigin(providerId);
165
+ if (!origin) return "";
166
+ const detail = origin.kind === "env" && origin.envVar ? `env: ${origin.envVar}` : ORIGIN_LABELS[origin.kind];
167
+ return theme.fg("muted", ` (${detail})`);
168
+ }
169
+
149
170
  #getStatusIndicator(providerId: string): string {
150
171
  const state = this.#authState.get(providerId);
172
+ const source = this.#getSourceLabel(providerId);
151
173
  if (state === "checking") {
152
174
  const frameCount = theme.spinnerFrames.length;
153
175
  const spinner = frameCount > 0 ? theme.spinnerFrames[this.#spinnerFrame % frameCount] : theme.status.pending;
154
- return theme.fg("warning", ` ${spinner} checking`);
176
+ return theme.fg("warning", ` ${spinner} checking`) + source;
155
177
  }
156
178
  if (state === "invalid") {
157
- return theme.fg("error", ` ${theme.status.error} invalid`);
179
+ return theme.fg("error", ` ${theme.status.error} invalid`) + source;
158
180
  }
159
181
  if (state === "valid") {
160
- return theme.fg("success", ` ${theme.status.success} logged in`);
182
+ return theme.fg("success", ` ${theme.status.success} logged in`) + source;
161
183
  }
162
- return this.#hasSelectableAuth(providerId) ? theme.fg("success", ` ${theme.status.success} logged in`) : "";
184
+ return this.#hasSelectableAuth(providerId)
185
+ ? theme.fg("success", ` ${theme.status.success} logged in`) + source
186
+ : "";
163
187
  }
164
188
 
165
189
  #isSearchEnabled(): boolean {
@@ -178,8 +202,10 @@ export class OAuthSelectorComponent extends Container {
178
202
 
179
203
  #getProviderSearchText(provider: OAuthProviderInfo): string {
180
204
  let text = `${provider.name} ${provider.id}`;
181
- if (this.#hasSelectableAuth(provider.id)) {
182
- text += " logged in authenticated";
205
+ const origin = this.#authStorage.getCredentialOrigin(provider.id);
206
+ if (origin) {
207
+ text += ` logged in authenticated ${ORIGIN_LABELS[origin.kind]}`;
208
+ if (origin.envVar) text += ` ${origin.envVar}`;
183
209
  }
184
210
  if (!provider.available) {
185
211
  text += " unavailable";
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Shared box-drawing chrome for fullscreen overlays (the `/copy` picker, the
3
+ * plan-review overlay, …). Every helper paints with `theme.boxSharp` glyphs and
4
+ * the `border`/`accent` theme colors so all outlined overlays read identically.
5
+ */
6
+ import { padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
7
+ import { theme } from "../theme/theme";
8
+
9
+ /** Pad or truncate a (possibly ANSI-styled) string to exactly `width` columns. */
10
+ export function fit(text: string, width: number): string {
11
+ if (width <= 0) return "";
12
+ const w = visibleWidth(text);
13
+ if (w === width) return text;
14
+ if (w < width) return text + padding(width - w);
15
+ const cut = truncateToWidth(text, width);
16
+ const cw = visibleWidth(cut);
17
+ return cw < width ? cut + padding(width - cw) : cut;
18
+ }
19
+
20
+ function paint(s: string): string {
21
+ return theme.fg("border", s);
22
+ }
23
+
24
+ /** Top border with an optional accent-colored title inset into the rule. */
25
+ export function topBorder(width: number, title: string): string {
26
+ const box = theme.boxSharp;
27
+ const inner = Math.max(0, width - 2);
28
+ if (!title) return paint(box.topLeft + box.horizontal.repeat(inner) + box.topRight);
29
+ const shown = truncateToWidth(` ${title} `, Math.max(0, inner - 2));
30
+ const fillWidth = Math.max(0, inner - 1 - visibleWidth(shown));
31
+ return (
32
+ paint(box.topLeft + box.horizontal) +
33
+ theme.bold(theme.fg("accent", shown)) +
34
+ paint(box.horizontal.repeat(fillWidth) + box.topRight)
35
+ );
36
+ }
37
+
38
+ /** A horizontal rule with left/right tees, splitting overlay sections. */
39
+ export function divider(width: number): string {
40
+ const box = theme.boxSharp;
41
+ return paint(box.teeRight + box.horizontal.repeat(Math.max(0, width - 2)) + box.teeLeft);
42
+ }
43
+
44
+ export function bottomBorder(width: number): string {
45
+ const box = theme.boxSharp;
46
+ return paint(box.bottomLeft + box.horizontal.repeat(Math.max(0, width - 2)) + box.bottomRight);
47
+ }
48
+
49
+ /** Wrap pre-styled content in vertical borders with single-column insets. */
50
+ export function row(content: string, width: number): string {
51
+ const box = theme.boxSharp;
52
+ return `${paint(box.vertical)} ${fit(content, Math.max(0, width - 4))} ${paint(box.vertical)}`;
53
+ }
54
+
55
+ /**
56
+ * Column index (0-based) of the inner divider for a two-column layout whose
57
+ * sidebar content area is `sidebarWidth` columns wide. The layout is
58
+ * `│ sidebar │ body │` with a single-column inset on every side, so the divider
59
+ * vertical sits at `sidebarWidth + 3` and the body content area is
60
+ * {@link splitBodyWidth} columns.
61
+ */
62
+ function splitDividerCol(sidebarWidth: number): number {
63
+ return sidebarWidth + 3;
64
+ }
65
+
66
+ /** Body content width for a two-column overlay of total `width`. */
67
+ export function splitBodyWidth(width: number, sidebarWidth: number): number {
68
+ return Math.max(0, width - sidebarWidth - 7);
69
+ }
70
+
71
+ /** Top border carrying the title, split by a `┬` over the column divider. */
72
+ export function topBorderSplit(width: number, title: string, sidebarWidth: number): string {
73
+ const box = theme.boxSharp;
74
+ const dividerCol = splitDividerCol(sidebarWidth);
75
+ const leftLen = Math.max(0, dividerCol - 1);
76
+ const rightLen = Math.max(0, width - 2 - dividerCol);
77
+ let left: string;
78
+ if (!title) {
79
+ left = paint(box.topLeft + box.horizontal.repeat(leftLen));
80
+ } else {
81
+ const shown = truncateToWidth(` ${title} `, Math.max(0, leftLen - 1));
82
+ const fillWidth = Math.max(0, leftLen - 1 - visibleWidth(shown));
83
+ left =
84
+ paint(box.topLeft + box.horizontal) +
85
+ theme.bold(theme.fg("accent", shown)) +
86
+ paint(box.horizontal.repeat(fillWidth));
87
+ }
88
+ return left + paint(box.teeDown + box.horizontal.repeat(rightLen) + box.topRight);
89
+ }
90
+
91
+ /** Section rule that closes the sidebar column with a `┴` over the divider. */
92
+ export function dividerSplit(width: number, sidebarWidth: number): string {
93
+ const box = theme.boxSharp;
94
+ const dividerCol = splitDividerCol(sidebarWidth);
95
+ const leftLen = Math.max(0, dividerCol - 1);
96
+ const rightLen = Math.max(0, width - 2 - dividerCol);
97
+ return paint(
98
+ box.teeRight + box.horizontal.repeat(leftLen) + box.teeUp + box.horizontal.repeat(rightLen) + box.teeLeft,
99
+ );
100
+ }
101
+
102
+ /** A two-column content row: `│ sidebar │ body │`, each inset by one column. */
103
+ export function splitRow(sidebar: string, body: string, width: number, sidebarWidth: number): string {
104
+ const box = theme.boxSharp;
105
+ const bodyWidth = splitBodyWidth(width, sidebarWidth);
106
+ const bar = paint(box.vertical);
107
+ return `${bar} ${fit(sidebar, sidebarWidth)} ${bar} ${fit(body, bodyWidth)} ${bar}`;
108
+ }