@gajae-code/coding-agent 0.15.5 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/CHANGELOG.md +85 -1
  2. package/dist/types/async/job-manager.d.ts +83 -0
  3. package/dist/types/cli/args.d.ts +2 -0
  4. package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
  5. package/dist/types/cli/model-presets-cli.d.ts +9 -0
  6. package/dist/types/cli/root-flags.d.ts +14 -0
  7. package/dist/types/cli/update-cli.d.ts +15 -0
  8. package/dist/types/cli.d.ts +7 -0
  9. package/dist/types/commands/auth-gateway.d.ts +3 -0
  10. package/dist/types/commands/gc.d.ts +13 -0
  11. package/dist/types/commands/launch.d.ts +10 -0
  12. package/dist/types/commands/model-presets.d.ts +21 -0
  13. package/dist/types/config/autorouting-tier-map.d.ts +120 -0
  14. package/dist/types/config/file-lock.d.ts +54 -2
  15. package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
  16. package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
  17. package/dist/types/config/keybindings.d.ts +3 -2
  18. package/dist/types/config/model-preset-registry.d.ts +368 -0
  19. package/dist/types/config/model-profile-activation.d.ts +39 -0
  20. package/dist/types/config/model-profiles.d.ts +5 -3
  21. package/dist/types/config/model-registry.d.ts +17 -5
  22. package/dist/types/config/settings-schema.d.ts +19 -1
  23. package/dist/types/config/settings.d.ts +2 -0
  24. package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
  25. package/dist/types/coordinator-mcp/projection-scan.d.ts +54 -0
  26. package/dist/types/crash/record-loader.d.ts +15 -0
  27. package/dist/types/extensibility/extensions/types.d.ts +2 -5
  28. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
  29. package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
  30. package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
  31. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
  32. package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
  33. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +72 -1
  34. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
  35. package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
  36. package/dist/types/index.d.ts +1 -0
  37. package/dist/types/main.d.ts +3 -1
  38. package/dist/types/master-mode/context.d.ts +21 -0
  39. package/dist/types/master-mode/first-request.d.ts +34 -0
  40. package/dist/types/master-mode/snapshot.d.ts +32 -0
  41. package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
  42. package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
  43. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  44. package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
  45. package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
  46. package/dist/types/modes/components/status-line/segments.d.ts +10 -0
  47. package/dist/types/modes/jobs-observer.d.ts +36 -0
  48. package/dist/types/modes/print-mode.d.ts +17 -0
  49. package/dist/types/modes/ui-language.d.ts +7 -1
  50. package/dist/types/sdk/broker/broker.d.ts +80 -1
  51. package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
  52. package/dist/types/sdk/broker/index.d.ts +3 -0
  53. package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
  54. package/dist/types/sdk/broker/master-capability.d.ts +10 -0
  55. package/dist/types/sdk/broker/session-index.d.ts +40 -11
  56. package/dist/types/sdk/broker/session-scope.d.ts +72 -0
  57. package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
  58. package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
  59. package/dist/types/sdk/broker/state-version.d.ts +4 -1
  60. package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
  61. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
  62. package/dist/types/sdk/bus/index.d.ts +6 -0
  63. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
  64. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
  65. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  66. package/dist/types/sdk/cli/index.d.ts +1 -0
  67. package/dist/types/sdk/cli/master-cli.d.ts +63 -0
  68. package/dist/types/sdk/cli/rows.d.ts +4 -3
  69. package/dist/types/sdk/cli/session-cli.d.ts +18 -2
  70. package/dist/types/sdk/client/client.d.ts +3 -2
  71. package/dist/types/sdk/client/discovery.d.ts +1 -0
  72. package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
  73. package/dist/types/sdk/host/control/index.d.ts +1 -1
  74. package/dist/types/sdk/host/control/operations.d.ts +2 -0
  75. package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
  76. package/dist/types/sdk/host/host.d.ts +10 -0
  77. package/dist/types/sdk/host/model-pin.d.ts +7 -2
  78. package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
  79. package/dist/types/sdk/host/session-runtime.d.ts +67 -1
  80. package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
  81. package/dist/types/sdk/lifecycle/service.d.ts +15 -3
  82. package/dist/types/sdk/lifecycle-session.d.ts +2 -0
  83. package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
  84. package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
  85. package/dist/types/sdk/router/session-router.d.ts +11 -0
  86. package/dist/types/sdk/session.d.ts +16 -0
  87. package/dist/types/sdk/startup-capability.d.ts +2 -1
  88. package/dist/types/session/account-inventory.d.ts +1 -0
  89. package/dist/types/session/agent-session.d.ts +24 -7
  90. package/dist/types/session/credential-store-errors.d.ts +1 -0
  91. package/dist/types/session/fold-coordinator.d.ts +201 -0
  92. package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
  93. package/dist/types/session/sdk-run-capability.d.ts +2 -0
  94. package/dist/types/session/session-manager.d.ts +65 -0
  95. package/dist/types/session/yield-queue.d.ts +5 -4
  96. package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
  97. package/dist/types/telemetry/control.d.ts +11 -0
  98. package/dist/types/telemetry/events.d.ts +20 -0
  99. package/dist/types/telemetry/index.d.ts +3 -0
  100. package/dist/types/telemetry/transport.d.ts +22 -0
  101. package/dist/types/tools/bash-interactive.d.ts +23 -0
  102. package/dist/types/tools/bash.d.ts +4 -0
  103. package/dist/types/tools/gh.d.ts +8 -0
  104. package/dist/types/tools/index.d.ts +10 -5
  105. package/dist/types/web/search/providers/base.d.ts +9 -4
  106. package/dist/types/web/search/providers/xai.d.ts +2 -1
  107. package/dist/types/web/search/types.d.ts +14 -0
  108. package/package.json +10 -7
  109. package/scripts/generate-hotkeys-docs.ts +4 -0
  110. package/scripts/generate-sdk-operation-inventory.ts +9 -3
  111. package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
  112. package/src/async/job-manager.ts +671 -39
  113. package/src/cli/accounts-cli.ts +9 -5
  114. package/src/cli/args.ts +17 -0
  115. package/src/cli/auth-broker-cli.ts +40 -12
  116. package/src/cli/auth-gateway-cli.ts +273 -97
  117. package/src/cli/config-cli.ts +2 -2
  118. package/src/cli/fast-help.ts +4 -0
  119. package/src/cli/model-presets-cli.ts +100 -0
  120. package/src/cli/root-flags.ts +5 -0
  121. package/src/cli/update-cli.ts +117 -16
  122. package/src/cli.ts +6 -2
  123. package/src/commands/auth-gateway.ts +12 -8
  124. package/src/commands/gc.ts +19 -0
  125. package/src/commands/launch.ts +86 -18
  126. package/src/commands/model-presets.ts +24 -0
  127. package/src/commands/sdk.ts +113 -21
  128. package/src/config/autorouting-tier-map.ts +44 -0
  129. package/src/config/file-lock-gc.ts +9 -4
  130. package/src/config/file-lock.ts +890 -99
  131. package/src/config/internal/model-preset-registry-test-state.ts +28 -0
  132. package/src/config/internal/model-preset-registry-test-support.ts +11 -0
  133. package/src/config/keybindings.ts +6 -2
  134. package/src/config/model-preset-registry.ts +2795 -0
  135. package/src/config/model-profile-activation.ts +115 -28
  136. package/src/config/model-profile-contract.ts +4 -1
  137. package/src/config/model-profiles.ts +56 -14
  138. package/src/config/model-registry.ts +1044 -122
  139. package/src/config/model-resolver.ts +5 -0
  140. package/src/config/settings-schema.ts +19 -1
  141. package/src/config/settings.ts +11 -1
  142. package/src/coordinator-mcp/model-preset.ts +32 -17
  143. package/src/coordinator-mcp/projection-scan.ts +592 -0
  144. package/src/coordinator-mcp/question-state.ts +9 -11
  145. package/src/coordinator-mcp/server.ts +111 -42
  146. package/src/coordinator-mcp/session-reaper.ts +9 -3
  147. package/src/crash/index-store.ts +2 -0
  148. package/src/crash/record-loader.ts +10 -0
  149. package/src/extensibility/extensions/loader.ts +0 -1
  150. package/src/extensibility/extensions/types.ts +2 -3
  151. package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
  152. package/src/gjc-runtime/empty-delete-gc.ts +301 -0
  153. package/src/gjc-runtime/gc-render.ts +29 -0
  154. package/src/gjc-runtime/gc-runtime.ts +174 -11
  155. package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
  156. package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
  157. package/src/gjc-runtime/session-state-lock.ts +1321 -147
  158. package/src/gjc-runtime/session-state-sidecar.ts +1004 -25
  159. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
  160. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
  161. package/src/gjc-runtime/tmux-sessions.ts +252 -13
  162. package/src/hooks/skill-state.ts +6 -0
  163. package/src/index.ts +1 -0
  164. package/src/internal-urls/docs-index.generated.ts +14 -13
  165. package/src/internal-urls/issue-pr-protocol.ts +4 -2
  166. package/src/lsp/config.ts +4 -3
  167. package/src/lsp/path-trust.ts +12 -6
  168. package/src/main.ts +66 -4
  169. package/src/master-mode/context.ts +50 -0
  170. package/src/master-mode/first-request.ts +65 -0
  171. package/src/master-mode/snapshot.ts +205 -0
  172. package/src/modes/acp/acp-agent.ts +616 -161
  173. package/src/modes/components/footer.ts +4 -2
  174. package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
  175. package/src/modes/components/jobs-overlay-model.ts +99 -16
  176. package/src/modes/components/model-selector.ts +158 -18
  177. package/src/modes/components/oauth-selector.ts +6 -1
  178. package/src/modes/components/settings-selector.ts +1 -0
  179. package/src/modes/components/status-line/model-name.ts +53 -0
  180. package/src/modes/components/status-line/priority-row.ts +107 -0
  181. package/src/modes/components/status-line/segments.ts +29 -10
  182. package/src/modes/components/tool-status-header.ts +151 -15
  183. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  184. package/src/modes/controllers/input-controller.ts +51 -6
  185. package/src/modes/controllers/selector-controller.ts +191 -96
  186. package/src/modes/interactive-mode.ts +14 -1
  187. package/src/modes/jobs-observer.ts +149 -16
  188. package/src/modes/print-mode.ts +64 -0
  189. package/src/modes/runtime-init.ts +1 -1
  190. package/src/modes/ui-language.ts +49 -2
  191. package/src/modes/utils/injected-user-submission.ts +15 -1
  192. package/src/prompts/system/master-mode.md +28 -0
  193. package/src/prompts/tools/bash.md +2 -2
  194. package/src/prompts/tools/fold-receipt.md +3 -0
  195. package/src/prompts/tools/job.md +1 -1
  196. package/src/sdk/acp/adapter.ts +17 -2
  197. package/src/sdk/broker/broker.ts +1671 -34
  198. package/src/sdk/broker/endpoint-authority.ts +86 -0
  199. package/src/sdk/broker/ensure.ts +5 -0
  200. package/src/sdk/broker/index.ts +3 -0
  201. package/src/sdk/broker/lifecycle.ts +217 -46
  202. package/src/sdk/broker/master-capability.ts +232 -0
  203. package/src/sdk/broker/session-index.ts +121 -17
  204. package/src/sdk/broker/session-scope.ts +272 -0
  205. package/src/sdk/broker/spawn-authority.ts +1031 -0
  206. package/src/sdk/broker/spawn-substrate.ts +548 -0
  207. package/src/sdk/broker/state-version.ts +26 -14
  208. package/src/sdk/broker/transport.ts +1 -0
  209. package/src/sdk/broker/worktree-occupancy.ts +90 -0
  210. package/src/sdk/bus/chat-daemon-control.ts +5 -2
  211. package/src/sdk/bus/chat-daemon-runtime.ts +46 -5
  212. package/src/sdk/bus/index.ts +51 -4
  213. package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
  214. package/src/sdk/bus/prompt-reconciliation.ts +28 -4
  215. package/src/sdk/bus/slack-conversation.ts +5 -1
  216. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  217. package/src/sdk/cli/index.ts +1 -0
  218. package/src/sdk/cli/master-cli.ts +259 -0
  219. package/src/sdk/cli/rows.ts +11 -8
  220. package/src/sdk/cli/session-cli.ts +276 -6
  221. package/src/sdk/client/client.ts +14 -8
  222. package/src/sdk/client/discovery.ts +3 -1
  223. package/src/sdk/host/control/dispatch.ts +101 -37
  224. package/src/sdk/host/control/index.ts +2 -0
  225. package/src/sdk/host/control/operations.ts +2 -0
  226. package/src/sdk/host/control/runtime-gate.ts +203 -11
  227. package/src/sdk/host/host.ts +27 -2
  228. package/src/sdk/host/model-pin.ts +185 -25
  229. package/src/sdk/host/sdk-run-capability.ts +1 -0
  230. package/src/sdk/host/session-runtime.test.ts +581 -43
  231. package/src/sdk/host/session-runtime.ts +655 -101
  232. package/src/sdk/lifecycle/broker-client.ts +28 -0
  233. package/src/sdk/lifecycle/service.ts +279 -20
  234. package/src/sdk/lifecycle-session.ts +10 -2
  235. package/src/sdk/prompt-deadline-manager.ts +16 -1
  236. package/src/sdk/protocol/adapter-validation.ts +27 -0
  237. package/src/sdk/protocol/operation-inventory.generated.json +53 -2
  238. package/src/sdk/protocol/operation-registry.ts +17 -4
  239. package/src/sdk/router/session-router.ts +294 -154
  240. package/src/sdk/session.ts +440 -167
  241. package/src/sdk/startup-capability.ts +1 -0
  242. package/src/session/account-inventory.ts +27 -11
  243. package/src/session/agent-session.ts +1292 -180
  244. package/src/session/blob-store.ts +15 -5
  245. package/src/session/credential-store-errors.ts +2 -0
  246. package/src/session/fold-coordinator.ts +433 -0
  247. package/src/session/internal/managed-session-storage.ts +153 -78
  248. package/src/session/sdk-run-capability-internal.ts +15 -0
  249. package/src/session/sdk-run-capability.ts +2 -0
  250. package/src/session/session-manager.ts +342 -28
  251. package/src/session/yield-queue.ts +24 -10
  252. package/src/setup/frictionless-onboarding.ts +2 -0
  253. package/src/setup/provider-presets.json +1 -1
  254. package/src/slash-commands/helpers/credential-switch.ts +6 -1
  255. package/src/task/executor.ts +19 -3
  256. package/src/telemetry/control.ts +26 -0
  257. package/src/telemetry/events.ts +1268 -0
  258. package/src/telemetry/index.ts +3 -0
  259. package/src/telemetry/transport.ts +87 -0
  260. package/src/tools/bash-interactive.ts +182 -85
  261. package/src/tools/bash.ts +1028 -109
  262. package/src/tools/gh.ts +104 -48
  263. package/src/tools/github-cache.ts +2 -2
  264. package/src/tools/index.ts +11 -5
  265. package/src/tools/tool-catalog.generated.ts +1 -1
  266. package/src/web/search/provider.ts +120 -11
  267. package/src/web/search/providers/anthropic.ts +89 -17
  268. package/src/web/search/providers/base.ts +9 -4
  269. package/src/web/search/providers/gemini.ts +87 -15
  270. package/src/web/search/providers/openai-compatible.ts +23 -8
  271. package/src/web/search/providers/xai.ts +98 -12
  272. package/src/web/search/types.ts +16 -0
@@ -9,6 +9,7 @@ import * as git from "../../utils/git";
9
9
  import { sanitizeStatusText } from "../shared";
10
10
  import { getContextUsageLevel, getContextUsageThemeColor } from "./status-line/context-thresholds";
11
11
  import { resolveCurrentBranch } from "./status-line/git-utils";
12
+ import { shortenModelId } from "./status-line/model-name";
12
13
 
13
14
  /**
14
15
  * Footer component that shows pwd, token stats, and context usage
@@ -191,8 +192,9 @@ export class FooterComponent implements Component {
191
192
 
192
193
  let statsLeft = statsParts.join(" ");
193
194
 
194
- // Add model name on the right side, plus thinking level if model supports it
195
- const modelName = state.model?.id || "no-model";
195
+ // Add model name on the right side, plus thinking level if model supports it.
196
+ // Same shortest-recognizable form the status rail renders.
197
+ const modelName = shortenModelId(state.model?.id ?? state.model?.name);
196
198
 
197
199
  // Add thinking level hint when the current model advertises supported efforts
198
200
  let rightSide = modelName;
@@ -1,9 +1,55 @@
1
- import { Container, matchesKey, Spacer, TruncatedText } from "@gajae-code/tui";
1
+ import { Container, matchesKey, type SelectItem, SelectList, Spacer, TruncatedText } from "@gajae-code/tui";
2
2
  import type { OnboardingProfile } from "../../setup/frictionless-onboarding";
3
- import { theme } from "../theme/theme";
3
+ import { getSelectListTheme, theme } from "../theme/theme";
4
+ import { UI_LANGUAGES, type UiLanguage } from "../ui-language";
4
5
  import { matchesSelectCancel } from "../utils/keybinding-matchers";
5
6
  import { DynamicBorder } from "./dynamic-border";
6
7
 
8
+ export const UI_LANGUAGE_CHOICES: readonly { value: UiLanguage; label: string }[] = [
9
+ { value: "en", label: "English" },
10
+ { value: "ko", label: "한국어" },
11
+ { value: "zh", label: "简体中文" },
12
+ { value: "ja", label: "日本語" },
13
+ ];
14
+
15
+ export class InterfaceLanguageSelectorComponent extends Container {
16
+ readonly #selectList: SelectList;
17
+
18
+ constructor(onSelect: (language: UiLanguage) => void, onCancel: () => void) {
19
+ super();
20
+ this.addChild(new DynamicBorder());
21
+ this.addChild(new Spacer(1));
22
+ this.addChild(
23
+ new TruncatedText(
24
+ theme.bold(
25
+ "Choose your interface language / 언어를 선택하세요 / 选择界面语言 / インターフェース言語を選択",
26
+ ),
27
+ ),
28
+ );
29
+ this.addChild(new Spacer(1));
30
+ const items: SelectItem[] = UI_LANGUAGE_CHOICES.map(choice => ({ value: choice.value, label: choice.label }));
31
+ this.#selectList = new SelectList(items, items.length, getSelectListTheme());
32
+ this.#selectList.onSelect = item => {
33
+ if (UI_LANGUAGES.includes(item.value as UiLanguage)) onSelect(item.value as UiLanguage);
34
+ };
35
+ this.#selectList.onCancel = onCancel;
36
+ this.addChild(this.#selectList);
37
+ this.addChild(new Spacer(1));
38
+ this.addChild(new TruncatedText(theme.fg("dim", "↑/↓ select · Enter choose · Esc use English")));
39
+ this.addChild(new DynamicBorder());
40
+ }
41
+
42
+ handleInput(keyData: string): void {
43
+ // The app.exit action is owned by the parent editor and is not a TUI
44
+ // selector key id. Ctrl-D is its default byte when the editor is empty.
45
+ if (keyData === "\x04") {
46
+ this.#selectList.onCancel?.();
47
+ return;
48
+ }
49
+ this.#selectList.handleInput(keyData);
50
+ }
51
+ }
52
+
7
53
  export type FrictionlessOnboardingAction =
8
54
  | "analyze"
9
55
  | "apply"
@@ -6,20 +6,41 @@
6
6
  * SelectItem lists into nested SelectLists (list -> detail -> confirm).
7
7
  */
8
8
  import type { SelectItem } from "@gajae-code/tui";
9
+ import { replaceTabs, shortenPath, TRUNCATE_LENGTHS, truncateToWidth } from "../../tools/render-utils";
9
10
  import type { JobsSnapshot } from "../jobs-observer";
11
+ import { sanitizeStatusText } from "../shared";
10
12
 
11
- export type JobRefKind = "monitor" | "cron";
13
+ export type JobRefKind = "monitor" | "cron" | "folded";
12
14
 
13
15
  export interface JobRef {
14
16
  kind: JobRefKind;
15
17
  id: string;
18
+ generation?: string;
16
19
  }
17
20
 
18
- const PROMPT_PREVIEW_MAX = 60;
21
+ function encodeJobRefPart(value: string): string {
22
+ return encodeURIComponent(value);
23
+ }
24
+
25
+ function decodeJobRefPart(value: string): string | undefined {
26
+ try {
27
+ return decodeURIComponent(value);
28
+ } catch {
29
+ return undefined;
30
+ }
31
+ }
32
+
33
+ function displayText(text: string, max: number = TRUNCATE_LENGTHS.CONTENT): string {
34
+ const sanitized = shortenPath(replaceTabs(sanitizeStatusText(text)));
35
+ return truncateToWidth(sanitized, max);
36
+ }
37
+
38
+ function preview(text: string, max: number = TRUNCATE_LENGTHS.TITLE): string {
39
+ return displayText(text, max);
40
+ }
19
41
 
20
- function preview(text: string, max = PROMPT_PREVIEW_MAX): string {
21
- const oneLine = text.replace(/\s+/g, " ").trim();
22
- return oneLine.length > max ? `${oneLine.slice(0, max - 1)}…` : oneLine;
42
+ function snapshotJobKey(id: string, generation: string | undefined): string {
43
+ return `${id}\u0000${generation ?? ""}`;
23
44
  }
24
45
 
25
46
  /** Compact relative time, e.g. "in 5m", "2m ago", "now". */
@@ -42,30 +63,71 @@ export function parseJobRef(value: string): JobRef | null {
42
63
  if ((kind === "monitor" || kind === "cron") && id.length > 0) {
43
64
  return { kind, id };
44
65
  }
66
+ if (kind === "folded") {
67
+ const generationSep = id.indexOf(":");
68
+ if (generationSep > 0 && generationSep < id.length - 1) {
69
+ const decodedId = decodeJobRefPart(id.slice(0, generationSep));
70
+ const generation = decodeJobRefPart(id.slice(generationSep + 1));
71
+ if (decodedId && generation) return { kind, id: decodedId, generation };
72
+ }
73
+ }
45
74
  return null;
46
75
  }
47
76
 
48
77
  /**
49
78
  * Build the grouped jobs list: monitors first (newest-first), then crons
50
- * (newest-first). The snapshot arrays are already sorted newest-first.
79
+ * (newest-first), then read-only folded jobs. The snapshot arrays are already
80
+ * sorted newest-first where a source has a timestamp.
51
81
  */
52
82
  export function buildJobsListItems(snapshot: JobsSnapshot): SelectItem[] {
53
83
  const items: SelectItem[] = [];
84
+ const foldedJobs = snapshot.foldedJobs ?? [];
85
+ const foldedKeys = new Set(foldedJobs.map(job => snapshotJobKey(job.id, job.generation)));
54
86
  for (const monitor of snapshot.monitors) {
87
+ // A monitor can also be backgrounded; render it only in the folded section
88
+ // so a public job state has exactly one row.
89
+ if (monitor.backgrounded || foldedKeys.has(snapshotJobKey(monitor.id, monitor.generation))) continue;
55
90
  items.push({
56
91
  value: `monitor:${monitor.id}`,
57
- label: `monitor · ${preview(monitor.label, 40)}`,
58
- description: monitor.status,
59
- hint: monitor.status === "failed" ? "failed" : undefined,
92
+ label: `monitor · ${preview(monitor.label, TRUNCATE_LENGTHS.SHORT)}`,
93
+ description: displayText(monitor.status, TRUNCATE_LENGTHS.SHORT),
94
+ hint: monitor.status === "failed" || monitor.deliveryState === "failed-visible" ? "failed" : undefined,
60
95
  });
61
96
  }
62
97
  for (const cron of snapshot.crons) {
63
98
  items.push({
64
99
  value: `cron:${cron.id}`,
65
- label: `cron · ${cron.humanSchedule}`,
100
+ label: `cron · ${preview(cron.humanSchedule, TRUNCATE_LENGTHS.SHORT)}`,
66
101
  description: preview(cron.prompt),
67
102
  });
68
103
  }
104
+ for (const job of foldedJobs) {
105
+ const state = displayText(job.deliveryState, TRUNCATE_LENGTHS.SHORT);
106
+ const kind = preview(job.kind, TRUNCATE_LENGTHS.SHORT);
107
+ const deadLetter = (snapshot.deadLettered ?? []).find(
108
+ entry => snapshotJobKey(entry.jobId, entry.generation) === snapshotJobKey(job.id, job.generation),
109
+ );
110
+ const errorText = job.errorText ?? deadLetter?.lastError;
111
+ const description =
112
+ deadLetter || errorText
113
+ ? displayText(
114
+ [
115
+ state,
116
+ deadLetter ? `attempt ${deadLetter.attempt}` : undefined,
117
+ errorText ? `error: ${errorText}` : undefined,
118
+ ]
119
+ .filter((part): part is string => part !== undefined)
120
+ .join(" · "),
121
+ TRUNCATE_LENGTHS.CONTENT,
122
+ )
123
+ : state;
124
+ items.push({
125
+ value: `folded:${encodeJobRefPart(job.id)}:${encodeJobRefPart(job.generation)}`,
126
+ label: `${kind} · ${preview(job.label, TRUNCATE_LENGTHS.SHORT)}`,
127
+ description,
128
+ hint: job.status === "failed" || job.deliveryState === "failed-visible" ? "failed" : "read-only",
129
+ });
130
+ }
69
131
  return items;
70
132
  }
71
133
 
@@ -80,21 +142,41 @@ export function buildJobDetailItems(snapshot: JobsSnapshot, ref: JobRef, output
80
142
  if (!monitor) return [{ value: "back", label: "Back (job no longer present)" }];
81
143
  const lastOutput = output.trim().split("\n").filter(Boolean).slice(-1)[0] ?? "(no output captured)";
82
144
  return [
83
- { value: "noop", label: "Status", description: monitor.status },
145
+ { value: "noop", label: "Status", description: displayText(monitor.status, TRUNCATE_LENGTHS.SHORT) },
84
146
  { value: "noop", label: "Label", description: preview(monitor.label) },
85
147
  { value: "noop", label: "Started", description: formatRelative(monitor.startTime) },
86
- { value: "noop", label: "Output", description: preview(lastOutput, 80) },
148
+ { value: "noop", label: "Output", description: preview(lastOutput, TRUNCATE_LENGTHS.CONTENT) },
87
149
  { value: "action:cancel", label: "Cancel this monitor", hint: "stops the running job" },
88
150
  { value: "back", label: "Back" },
89
151
  ];
90
152
  }
153
+ if (ref.kind === "folded") {
154
+ const job = snapshot.foldedJobs?.find(
155
+ entry => entry.id === ref.id && (ref.generation === undefined || entry.generation === ref.generation),
156
+ );
157
+ if (!job) return [{ value: "back", label: "Back (job no longer present)" }];
158
+ return [
159
+ { value: "noop", label: "Status", description: displayText(job.status, TRUNCATE_LENGTHS.SHORT) },
160
+ { value: "noop", label: "Kind", description: preview(job.kind, TRUNCATE_LENGTHS.SHORT) },
161
+ { value: "noop", label: "Label", description: preview(job.label) },
162
+ { value: "noop", label: "Generation", description: preview(job.generation, TRUNCATE_LENGTHS.CONTENT) },
163
+ ...(job.errorText
164
+ ? [{ value: "noop", label: "Error", description: preview(job.errorText, TRUNCATE_LENGTHS.CONTENT) }]
165
+ : []),
166
+ { value: "back", label: "Back" },
167
+ ];
168
+ }
91
169
  const cron = snapshot.crons.find(c => c.id === ref.id);
92
170
  if (!cron) return [{ value: "back", label: "Back (job no longer present)" }];
93
171
  return [
94
- { value: "noop", label: "Schedule", description: `${cron.humanSchedule} (${cron.cronExpression})` },
172
+ {
173
+ value: "noop",
174
+ label: "Schedule",
175
+ description: `${preview(cron.humanSchedule, TRUNCATE_LENGTHS.SHORT)} (${preview(cron.cronExpression, TRUNCATE_LENGTHS.SHORT)})`,
176
+ },
95
177
  { value: "noop", label: "Recurring", description: cron.recurring ? "yes" : "no" },
96
178
  { value: "noop", label: "Next fire", description: formatRelative(cron.nextFireAt) },
97
- { value: "noop", label: "Prompt", description: preview(cron.prompt, 80) },
179
+ { value: "noop", label: "Prompt", description: preview(cron.prompt, TRUNCATE_LENGTHS.CONTENT) },
98
180
  { value: "action:delete", label: "Delete this cron", hint: "removes the schedule" },
99
181
  { value: "back", label: "Back" },
100
182
  ];
@@ -102,8 +184,9 @@ export function buildJobDetailItems(snapshot: JobsSnapshot, ref: JobRef, output
102
184
 
103
185
  /** Yes/No confirm items for a destructive action. */
104
186
  export function buildConfirmItems(actionLabel: string): SelectItem[] {
187
+ const safeActionLabel = displayText(actionLabel, TRUNCATE_LENGTHS.TITLE);
105
188
  return [
106
- { value: "no", label: `No, keep it` },
107
- { value: "yes", label: `Yes, ${actionLabel}` },
189
+ { value: "no", label: "No, keep it" },
190
+ { value: "yes", label: `Yes, ${safeActionLabel}` },
108
191
  ];
109
192
  }
@@ -22,9 +22,18 @@ import {
22
22
  normalizeTierMap,
23
23
  validateAutoroutingSetup,
24
24
  } from "../../config/autorouting-contract";
25
+ import {
26
+ getProxyRoutableProviders,
27
+ inspectProxyProviderId,
28
+ requiresQualifiedModelProfileRoleResolution,
29
+ resolveProxyMode,
30
+ rewriteSelectorForProxy,
31
+ tryResolveProxyProviderId,
32
+ } from "../../config/model-profile-activation";
25
33
 
26
34
  import { isModelProfileProviderAvailable } from "../../config/model-profile-contract";
27
35
  import {
36
+ deriveModelProfileMappedProviders,
28
37
  getModelProfilePresentation,
29
38
  groupModelProfilesForPresetLanding,
30
39
  type ModelProfileDefinition,
@@ -399,6 +408,7 @@ export class ModelSelectorComponent extends Container {
399
408
  #closeAfterAssignment = false;
400
409
  #unsubscribeCatalogChanged: () => void = () => {};
401
410
  #unsubscribeProviderOrderChanged: () => void = () => {};
411
+ #unsubscribeAuthGeneration: () => void = () => {};
402
412
  #disposed = false;
403
413
  /** Standalone smart-routing entry: cancel closes the selector instead of falling back to the preset landing. */
404
414
  #smartRoutingOnly = false;
@@ -413,6 +423,7 @@ export class ModelSelectorComponent extends Container {
413
423
  #providerAuthById = new Map<string, boolean>();
414
424
  #bareProfileAuthByName = new Map<string, boolean>();
415
425
  #providerAuthPending: boolean = false;
426
+ #providerAuthRefreshGeneration = 0;
416
427
  #presetLoginHint?: string;
417
428
  #authSessionId?: string;
418
429
  #imageRoleFilter: boolean = false;
@@ -524,6 +535,14 @@ export class ModelSelectorComponent extends Container {
524
535
  if (typeof this.#modelRegistry.onCatalogChanged === "function") {
525
536
  this.#unsubscribeCatalogChanged = this.#modelRegistry.onCatalogChanged(() => {
526
537
  if (this.#disposed) return;
538
+ if (this.#viewMode === "presets") {
539
+ this.#refreshCatalogView();
540
+ this.#clampPresetCursor();
541
+ void this.#refreshProviderAuth();
542
+ this.#renderPresetLanding();
543
+ this.#tui.requestRender();
544
+ return;
545
+ }
527
546
  if (this.#refreshCatalogView()) this.#tui.requestRender();
528
547
  });
529
548
  }
@@ -538,6 +557,11 @@ export class ModelSelectorComponent extends Container {
538
557
  panel.updateProviderOrderHint(this.#providerOrderHintFor(panel.getProviderOrder()));
539
558
  this.#tui.requestRender();
540
559
  });
560
+ this.#unsubscribeAuthGeneration =
561
+ this.#modelRegistry.authStorage?.onGenerationChanged?.(() => {
562
+ if (this.#disposed || this.#viewMode !== "presets") return;
563
+ void this.#refreshProviderAuth();
564
+ }) ?? (() => {});
541
565
 
542
566
  // Load models and do initial render
543
567
  this.#loadModels().then(() => {
@@ -572,8 +596,10 @@ export class ModelSelectorComponent extends Container {
572
596
  override dispose(): void {
573
597
  if (this.#disposed) return;
574
598
  this.#disposed = true;
599
+ this.#providerAuthRefreshGeneration++;
575
600
  this.#unsubscribeCatalogChanged();
576
601
  this.#unsubscribeProviderOrderChanged();
602
+ this.#unsubscribeAuthGeneration();
577
603
  super.dispose();
578
604
  }
579
605
 
@@ -1301,13 +1327,61 @@ export class ModelSelectorComponent extends Container {
1301
1327
  return this.#providerAuthById.get(providerId);
1302
1328
  }
1303
1329
 
1330
+ #getProfileAvailableModels(): Model[] {
1331
+ const getAvailableForProfileActivation = this.#modelRegistry.getAvailableForProfileActivation;
1332
+ return typeof getAvailableForProfileActivation === "function"
1333
+ ? getAvailableForProfileActivation.call(this.#modelRegistry)
1334
+ : this.#modelRegistry.getAvailable();
1335
+ }
1336
+
1337
+ #createProfileResolutionRegistry(availableModels: readonly Model[]) {
1338
+ return {
1339
+ getAvailable: () => availableModels as Model[],
1340
+ getApiKey: this.#modelRegistry.getApiKey?.bind(this.#modelRegistry) ?? (async () => undefined),
1341
+ resolveCanonicalModel: this.#modelRegistry.resolveCanonicalModel?.bind(this.#modelRegistry),
1342
+ getCanonicalVariants: this.#modelRegistry.getCanonicalVariants?.bind(this.#modelRegistry),
1343
+ getCanonicalId: this.#modelRegistry.getCanonicalId?.bind(this.#modelRegistry),
1344
+ resolveModelByLookupAlias: this.#modelRegistry.resolveModelByLookupAlias?.bind(this.#modelRegistry),
1345
+ lookupAliasExists: this.#modelRegistry.lookupAliasExists?.bind(this.#modelRegistry),
1346
+ clearCanonicalVariant: this.#modelRegistry.clearCanonicalVariant?.bind(this.#modelRegistry),
1347
+ };
1348
+ }
1349
+
1350
+ #getProfileAuthenticatedProviders(profile: ModelProfileDefinition): Set<string> {
1351
+ if (profile.source !== "user" && inspectProxyProviderId(this.#settings).status === "invalid") return new Set();
1352
+ const authenticated = new Set(
1353
+ [...this.#providerAuthById]
1354
+ .filter(([, providerAuthenticated]) => providerAuthenticated)
1355
+ .map(([provider]) => provider),
1356
+ );
1357
+ const proxyProvider = profile.source === "user" ? undefined : tryResolveProxyProviderId(this.#settings);
1358
+ if (proxyProvider !== undefined && this.#isProviderAuthenticated(proxyProvider) === true) {
1359
+ for (const provider of getProxyRoutableProviders(profile)) authenticated.add(provider);
1360
+ }
1361
+ return authenticated;
1362
+ }
1363
+
1364
+ #rewriteProfileSelectorForProxy(profile: ModelProfileDefinition, selector: string): string {
1365
+ if (profile.source === "user") return selector;
1366
+ const proxyProvider = tryResolveProxyProviderId(this.#settings);
1367
+ if (proxyProvider === undefined || this.#isProviderAuthenticated(proxyProvider) !== true) return selector;
1368
+ return rewriteSelectorForProxy(
1369
+ selector,
1370
+ proxyProvider,
1371
+ resolveProxyMode(this.#settings),
1372
+ this.#getProfileAvailableModels(),
1373
+ new Set(
1374
+ [...this.#providerAuthById].filter(([, authenticated]) => authenticated).map(([provider]) => provider),
1375
+ ),
1376
+ getProxyRoutableProviders(profile),
1377
+ );
1378
+ }
1379
+
1304
1380
  #getMissingProviders(profileOrProfiles: ModelProfileDefinition | ModelProfileDefinition[]): string[] {
1305
1381
  const profiles = Array.isArray(profileOrProfiles) ? profileOrProfiles : [profileOrProfiles];
1306
1382
  const missing = new Set<string>();
1307
1383
  for (const profile of profiles) {
1308
- const authenticated = new Set(
1309
- profileRequiredProviders(profile).filter(provider => this.#isProviderAuthenticated(provider) === true),
1310
- );
1384
+ const authenticated = this.#getProfileAuthenticatedProviders(profile);
1311
1385
  if (isModelProfileProviderAvailable(profile, authenticated)) continue;
1312
1386
  const alternativeGroups = profile.alternativeProviderGroups ?? [];
1313
1387
  const alternativeProviders = new Set(alternativeGroups.flat());
@@ -1325,18 +1399,52 @@ export class ModelSelectorComponent extends Container {
1325
1399
  #isPresetAuthenticated(profileOrProfiles: ModelProfileDefinition | ModelProfileDefinition[]): boolean {
1326
1400
  const profiles = Array.isArray(profileOrProfiles) ? profileOrProfiles : [profileOrProfiles];
1327
1401
  return profiles.every(profile => {
1402
+ if (profile.source !== "user") {
1403
+ if (inspectProxyProviderId(this.#settings).status === "invalid") return false;
1404
+ const proxyProvider = tryResolveProxyProviderId(this.#settings);
1405
+ if (
1406
+ proxyProvider !== undefined &&
1407
+ !(this.#modelRegistry.getConfiguredProviderIds?.() ?? []).includes(proxyProvider)
1408
+ )
1409
+ return false;
1410
+ try {
1411
+ if (resolveProxyMode(this.#settings) === "always") {
1412
+ const configuredProviders = this.#modelRegistry.getConfiguredProviderIds?.() ?? [];
1413
+ if (
1414
+ proxyProvider === undefined ||
1415
+ this.#isProviderAuthenticated(proxyProvider) !== true ||
1416
+ !configuredProviders.includes(proxyProvider)
1417
+ )
1418
+ return false;
1419
+ }
1420
+ } catch {
1421
+ return false;
1422
+ }
1423
+ }
1328
1424
  if (this.#getMissingProviders(profile).length > 0) return false;
1329
1425
  const bindings = resolveProfileBindings(profile);
1330
- const values = [
1331
- ...(bindings.defaultSelector ? [bindings.defaultSelector] : []),
1332
- ...Object.values(bindings.modelRoles),
1333
- ...Object.values(bindings.agentModelOverrides),
1426
+ const assignments = [
1427
+ ...(bindings.defaultSelector !== undefined ? [{ value: bindings.defaultSelector, isDefault: true }] : []),
1428
+ ...Object.values(bindings.modelRoles).map(value => ({ value, isDefault: false })),
1429
+ ...Object.values(bindings.agentModelOverrides).map(value => ({ value, isDefault: false })),
1334
1430
  ];
1335
1431
  const bareAssignmentsAvailable = this.#bareProfileAuthByName.get(profile.name) ?? true;
1336
- return values.every(value =>
1337
- normalizeModelSelectorValue(value).some(selector => {
1432
+ const availableModels = this.#getProfileAvailableModels();
1433
+ return assignments.every(assignment => {
1434
+ let selectors: string[];
1435
+ try {
1436
+ selectors = normalizeModelSelectorValue(assignment.value).map(selector =>
1437
+ this.#rewriteProfileSelectorForProxy(profile, selector),
1438
+ );
1439
+ } catch {
1440
+ return false;
1441
+ }
1442
+ const hasBareSelector = selectors.some(selector => !selector.includes("/"));
1443
+ if (!assignment.isDefault && !requiresQualifiedModelProfileRoleResolution(profile) && !hasBareSelector)
1444
+ return true;
1445
+ return selectors.some(selector => {
1338
1446
  if (!selector.includes("/")) return bareAssignmentsAvailable;
1339
- const resolved = resolveModelRoleValue(selector, this.#modelRegistry.getAvailable(), {
1447
+ const resolved = resolveModelRoleValue(selector, availableModels, {
1340
1448
  settings: this.#settings,
1341
1449
  modelRegistry: this.#modelRegistry,
1342
1450
  aliasIntent: "preset-equivalent",
@@ -1357,7 +1465,7 @@ export class ModelSelectorComponent extends Container {
1357
1465
  return (
1358
1466
  resolveModelRoleValue(
1359
1467
  replacementSelector,
1360
- this.#modelRegistry.getAvailable().filter(model => model.provider === provider),
1468
+ availableModels.filter(model => model.provider === provider),
1361
1469
  {
1362
1470
  settings: this.#settings,
1363
1471
  modelRegistry: this.#modelRegistry,
@@ -1368,8 +1476,8 @@ export class ModelSelectorComponent extends Container {
1368
1476
  );
1369
1477
  }) ?? false
1370
1478
  );
1371
- }),
1372
- );
1479
+ });
1480
+ });
1373
1481
  });
1374
1482
  }
1375
1483
 
@@ -1383,10 +1491,23 @@ export class ModelSelectorComponent extends Container {
1383
1491
  }
1384
1492
 
1385
1493
  async #refreshProviderAuth(): Promise<void> {
1494
+ const refreshGeneration = ++this.#providerAuthRefreshGeneration;
1495
+ this.#providerAuthById = new Map();
1496
+ this.#bareProfileAuthByName = new Map();
1497
+ const availableModels = this.#getProfileAvailableModels();
1498
+ const resolutionRegistry = this.#createProfileResolutionRegistry(availableModels);
1386
1499
  const providers = new Set<string>();
1387
1500
  for (const profiles of this.#getPresetGroups().values()) {
1388
1501
  for (const profile of profiles) {
1389
1502
  for (const provider of profileRequiredProviders(profile)) providers.add(provider);
1503
+ for (const group of profile.alternativeProviderGroups ?? []) {
1504
+ for (const provider of group) providers.add(provider);
1505
+ }
1506
+ for (const provider of deriveModelProfileMappedProviders(profile)) providers.add(provider);
1507
+ if (profile.source !== "user") {
1508
+ const proxyProvider = tryResolveProxyProviderId(this.#settings);
1509
+ if (proxyProvider !== undefined) providers.add(proxyProvider);
1510
+ }
1390
1511
  const bindings = resolveProfileBindings(profile);
1391
1512
  const values = [
1392
1513
  ...(bindings.defaultSelector ? [bindings.defaultSelector] : []),
@@ -1395,7 +1516,7 @@ export class ModelSelectorComponent extends Container {
1395
1516
  ];
1396
1517
  for (const value of values) {
1397
1518
  for (const selector of normalizeModelSelectorValue(value)) {
1398
- const resolved = resolveModelRoleValue(selector, this.#modelRegistry.getAvailable(), {
1519
+ const resolved = resolveModelRoleValue(selector, availableModels, {
1399
1520
  settings: this.#settings,
1400
1521
  modelRegistry: this.#modelRegistry,
1401
1522
  aliasIntent: "preset-equivalent",
@@ -1419,6 +1540,12 @@ export class ModelSelectorComponent extends Container {
1419
1540
  }
1420
1541
  }),
1421
1542
  );
1543
+ if (
1544
+ this.#disposed ||
1545
+ this.#viewMode !== "presets" ||
1546
+ refreshGeneration !== this.#providerAuthRefreshGeneration
1547
+ )
1548
+ return;
1422
1549
  this.#providerAuthById = new Map(entries);
1423
1550
  const profileAuthEntries = await Promise.all(
1424
1551
  [...this.#getPresetGroups().values()].flat().map(async profile => {
@@ -1436,7 +1563,7 @@ export class ModelSelectorComponent extends Container {
1436
1563
  try {
1437
1564
  const resolution = await resolveModelChainWithAuth(
1438
1565
  normalizeModelSelectorValue(value),
1439
- this.#modelRegistry,
1566
+ resolutionRegistry,
1440
1567
  this.#settings,
1441
1568
  this.#authSessionId,
1442
1569
  {
@@ -1455,11 +1582,23 @@ export class ModelSelectorComponent extends Container {
1455
1582
  return [profile.name, available.every(Boolean)] as const;
1456
1583
  }),
1457
1584
  );
1585
+ if (
1586
+ this.#disposed ||
1587
+ this.#viewMode !== "presets" ||
1588
+ refreshGeneration !== this.#providerAuthRefreshGeneration
1589
+ )
1590
+ return;
1458
1591
  this.#bareProfileAuthByName = new Map(profileAuthEntries);
1459
1592
  } finally {
1460
- this.#providerAuthPending = false;
1461
- this.#renderPresetLanding();
1462
- this.#tui.requestRender();
1593
+ if (
1594
+ !this.#disposed &&
1595
+ this.#viewMode === "presets" &&
1596
+ refreshGeneration === this.#providerAuthRefreshGeneration
1597
+ ) {
1598
+ this.#providerAuthPending = false;
1599
+ this.#renderPresetLanding();
1600
+ this.#tui.requestRender();
1601
+ }
1463
1602
  }
1464
1603
  }
1465
1604
 
@@ -1676,6 +1815,7 @@ export class ModelSelectorComponent extends Container {
1676
1815
  this.#smartRoutingPanel = undefined;
1677
1816
  this.#viewMode = "presets";
1678
1817
  this.#presetCursor = Math.min(this.#presetCursor, Math.max(0, this.#getPresetRows().length - 1));
1818
+ void this.#refreshProviderAuth();
1679
1819
  this.#renderPresetLanding();
1680
1820
  this.#tui.requestRender();
1681
1821
  }
@@ -56,6 +56,7 @@ export class OAuthSelectorComponent extends Container {
56
56
  #onCancelCallback: () => void;
57
57
  #statusMessage: string | undefined;
58
58
  #validateAuthCallback?: (providerId: string) => Promise<boolean>;
59
+ #onValidationError?: (error: unknown) => boolean;
59
60
  #requestRenderCallback?: () => void;
60
61
  #authState: Map<string, "checking" | "valid" | "invalid"> = new Map();
61
62
  #externalCredentialCandidates: ImportableCredential[] = [];
@@ -76,6 +77,7 @@ export class OAuthSelectorComponent extends Container {
76
77
  onCancel: () => void,
77
78
  options?: {
78
79
  validateAuth?: (providerId: string) => Promise<boolean>;
80
+ onValidationError?: (error: unknown) => boolean;
79
81
  requestRender?: () => void;
80
82
  externalCredentialCandidates?: ImportableCredential[];
81
83
  } & Partial<OAuthSelectorAccountOptions>,
@@ -86,6 +88,7 @@ export class OAuthSelectorComponent extends Container {
86
88
  this.#onSelectCallback = onSelect;
87
89
  this.#onCancelCallback = onCancel;
88
90
  this.#validateAuthCallback = options?.validateAuth;
91
+ this.#onValidationError = options?.onValidationError;
89
92
  this.#requestRenderCallback = options?.requestRender;
90
93
  this.#externalCredentialCandidates = options?.externalCredentialCandidates ?? [];
91
94
  this.#accountProviderId = options?.accountProviderId;
@@ -194,7 +197,9 @@ export class OAuthSelectorComponent extends Container {
194
197
  let isValid = false;
195
198
  try {
196
199
  isValid = await this.#validateAuthCallback(providerId);
197
- } catch {
200
+ } catch (error) {
201
+ if (generation !== this.#validationGeneration) return;
202
+ if (this.#onValidationError?.(error)) return;
198
203
  isValid = false;
199
204
  }
200
205
  if (generation !== this.#validationGeneration) return;
@@ -1009,6 +1009,7 @@ export class SettingsSelectorComponent extends Container {
1009
1009
  options = [
1010
1010
  { value: "en", label: uiString(language, "settings.language.english") },
1011
1011
  { value: "ko", label: uiString(language, "settings.language.korean") },
1012
+ { value: "zh", label: uiString(language, "settings.language.chinese") },
1012
1013
  { value: "ja", label: uiString(language, "settings.language.japanese") },
1013
1014
  ];
1014
1015
  }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Shortest recognizable model label, derived purely from the model id.
3
+ *
4
+ * The rail and the footer both render this form, so a narrow terminal spends
5
+ * its cells on the part of the id that actually identifies the model:
6
+ *
7
+ * `anthropic/claude-sonnet-4-5-20250929` -> `sonnet-4.5`
8
+ * `openai/gpt-4o-2024-05-13` -> `gpt-4o`
9
+ * `google/gemini-3-pro` -> `gemini-3-pro`
10
+ * `qwen2.5:7b` -> `qwen2.5:7b`
11
+ *
12
+ * Deliberately a pure heuristic with no curated override table: every provider
13
+ * in `models.json` must degrade sanely without per-model maintenance. The
14
+ * function never returns an empty string — an id that the heuristic would
15
+ * strip to nothing falls back to the previous, longer form.
16
+ */
17
+
18
+ /**
19
+ * Vendor family words that carry no information once the provider prefix is
20
+ * gone. Deliberately minimal: `gpt` and `gemini` are part of how those models
21
+ * are named and are kept.
22
+ */
23
+ const VENDOR_FAMILY_WORDS: ReadonlySet<string> = new Set(["claude"]);
24
+
25
+ /** Trailing snapshot dates and build stamps: `-20250929`, `-2024-05-13`, `-v2`. */
26
+ const DATE_OR_BUILD_SUFFIX = /-(?:\d{4}-\d{2}-\d{2}|\d{8}|\d{6}|v\d+(?:\.\d+)*)$/;
27
+
28
+ /** Split version parts read as one number: `sonnet-4-5` -> `sonnet-4.5`. */
29
+ const SPLIT_VERSION = /(\d)-(\d)/g;
30
+
31
+ export const NO_MODEL_LABEL = "no-model";
32
+
33
+ export function shortenModelId(rawId: string | undefined | null): string {
34
+ const raw = (rawId ?? "").trim();
35
+ if (!raw) return NO_MODEL_LABEL;
36
+
37
+ // Provider prefix: `anthropic/claude-…`, `openrouter/anthropic/claude-…`.
38
+ const afterProvider = raw.slice(raw.lastIndexOf("/") + 1).trim() || raw;
39
+
40
+ const withoutDate = afterProvider.replace(DATE_OR_BUILD_SUFFIX, "") || afterProvider;
41
+
42
+ let name = withoutDate;
43
+ const dashIndex = name.indexOf("-");
44
+ if (dashIndex > 0) {
45
+ const head = name.slice(0, dashIndex).toLowerCase();
46
+ const rest = name.slice(dashIndex + 1);
47
+ if (rest && VENDOR_FAMILY_WORDS.has(head)) name = rest;
48
+ }
49
+
50
+ name = name.replace(SPLIT_VERSION, "$1.$2");
51
+
52
+ return name || withoutDate || afterProvider;
53
+ }