@gajae-code/coding-agent 0.12.21 → 0.13.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 (222) hide show
  1. package/CHANGELOG.md +67 -2
  2. package/dist/types/cli/args.d.ts +5 -2
  3. package/dist/types/cli/root-flags.d.ts +246 -0
  4. package/dist/types/cli.d.ts +11 -19
  5. package/dist/types/commands/daemon.d.ts +0 -30
  6. package/dist/types/commands/launch.d.ts +11 -19
  7. package/dist/types/commands/quick-lane.d.ts +32 -0
  8. package/dist/types/commands/sdk.d.ts +21 -15
  9. package/dist/types/config/file-lock-gc.d.ts +2 -0
  10. package/dist/types/config/model-equivalence.d.ts +20 -0
  11. package/dist/types/config/model-profile-activation.d.ts +62 -17
  12. package/dist/types/config/model-profiles.d.ts +10 -0
  13. package/dist/types/config/model-registry.d.ts +68 -1
  14. package/dist/types/config/model-resolver.d.ts +15 -4
  15. package/dist/types/config/models-config-schema.d.ts +7 -2
  16. package/dist/types/config/provider-selection-policy.d.ts +62 -0
  17. package/dist/types/config/settings-schema.d.ts +36 -78
  18. package/dist/types/config/settings.d.ts +2 -0
  19. package/dist/types/edit/renderer.d.ts +5 -2
  20. package/dist/types/edit/streaming.d.ts +13 -0
  21. package/dist/types/extensibility/custom-tools/types.d.ts +2 -0
  22. package/dist/types/extensibility/extensions/compact-handler.d.ts +2 -1
  23. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  24. package/dist/types/gjc-runtime/gc-runtime.d.ts +19 -5
  25. package/dist/types/internal-urls/local-protocol.d.ts +1 -0
  26. package/dist/types/modes/components/model-selector.d.ts +0 -2
  27. package/dist/types/modes/components/provider-order-context.d.ts +45 -0
  28. package/dist/types/modes/components/provider-order-editor.d.ts +24 -0
  29. package/dist/types/modes/components/settings-defs.d.ts +5 -1
  30. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  31. package/dist/types/quick-lane/classify.d.ts +40 -0
  32. package/dist/types/runtime/stt-settings-setup.d.ts +18 -0
  33. package/dist/types/sdk/broker/broker.d.ts +56 -2
  34. package/dist/types/sdk/broker/operation-receipt-ledger.d.ts +25 -0
  35. package/dist/types/sdk/broker/session-index.d.ts +83 -2
  36. package/dist/types/sdk/broker/state-version.d.ts +1 -1
  37. package/dist/types/sdk/bus/notification-service.d.ts +6 -0
  38. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +8 -1
  39. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
  40. package/dist/types/sdk/bus/telegram-daemon.d.ts +26 -0
  41. package/dist/types/sdk/cli/rows.d.ts +98 -0
  42. package/dist/types/sdk/cli/session-cli.d.ts +35 -3
  43. package/dist/types/sdk/elevation/allowlist.d.ts +20 -0
  44. package/dist/types/sdk/elevation/audit.d.ts +93 -0
  45. package/dist/types/sdk/elevation/capability.d.ts +3 -0
  46. package/dist/types/sdk/elevation/digest.d.ts +72 -0
  47. package/dist/types/sdk/elevation/dispatch-receipt.d.ts +85 -0
  48. package/dist/types/sdk/elevation/grant-ledger.d.ts +163 -0
  49. package/dist/types/sdk/elevation/owner.d.ts +48 -0
  50. package/dist/types/sdk/guides/cache.d.ts +98 -0
  51. package/dist/types/sdk/guides/catalog.d.ts +156 -0
  52. package/dist/types/sdk/guides/cli.d.ts +39 -0
  53. package/dist/types/sdk/guides/manifest.d.ts +66 -0
  54. package/dist/types/sdk/guides/verify.d.ts +66 -0
  55. package/dist/types/sdk/host/control/dispatch.d.ts +8 -0
  56. package/dist/types/sdk/host/control/index.d.ts +1 -1
  57. package/dist/types/sdk/host/control/operations.d.ts +3 -2
  58. package/dist/types/sdk/host/query/cursor.d.ts +10 -0
  59. package/dist/types/sdk/host/query/handlers.d.ts +21 -0
  60. package/dist/types/sdk/host/session-runtime.d.ts +9 -0
  61. package/dist/types/sdk/mcp/server.d.ts +2 -2
  62. package/dist/types/sdk/prompt-status.d.ts +11 -0
  63. package/dist/types/sdk/session.d.ts +4 -0
  64. package/dist/types/sdk/transport/index.d.ts +1 -0
  65. package/dist/types/sdk/transport/relay.d.ts +1 -0
  66. package/dist/types/sdk/transport/serve-cli.d.ts +20 -1
  67. package/dist/types/session/agent-session.d.ts +32 -18
  68. package/dist/types/session/blob-store.d.ts +13 -7
  69. package/dist/types/session/fallback-chain-controller.d.ts +15 -0
  70. package/dist/types/session/internal/managed-session-storage.d.ts +25 -1
  71. package/dist/types/session/internal/session-memory-sidecar.d.ts +5 -5
  72. package/dist/types/session/session-manager.d.ts +139 -10
  73. package/dist/types/session/session-storage.d.ts +48 -0
  74. package/dist/types/setup/provider-onboarding.d.ts +7 -1
  75. package/dist/types/slash-commands/builtin-registry.d.ts +14 -0
  76. package/dist/types/stt/setup.d.ts +2 -0
  77. package/dist/types/task/executor.d.ts +10 -2
  78. package/dist/types/tools/browser/runtime-diagnostics.d.ts +65 -0
  79. package/dist/types/tools/browser/tab-protocol.d.ts +4 -0
  80. package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
  81. package/dist/types/tools/browser/tab-worker.d.ts +2 -0
  82. package/dist/types/tools/browser.d.ts +2 -0
  83. package/dist/types/tools/image-gen.d.ts +14 -18
  84. package/dist/types/tools/index.d.ts +2 -0
  85. package/dist/types/tools/resource-gc.d.ts +2 -0
  86. package/dist/types/utils/pasted-image-path.d.ts +6 -0
  87. package/package.json +9 -9
  88. package/scripts/generate-sdk-operation-inventory.ts +8 -0
  89. package/scripts/run-test-manifest.ts +61 -21
  90. package/scripts/verify-gjc-sdk-canonicalization.ts +9 -4
  91. package/src/cli/args.ts +62 -9
  92. package/src/cli/completion-cli.ts +1 -0
  93. package/src/cli/fast-help.ts +1 -0
  94. package/src/cli/root-flags.ts +100 -0
  95. package/src/cli/setup-cli.ts +6 -2
  96. package/src/cli.ts +42 -65
  97. package/src/commands/acp.ts +1 -1
  98. package/src/commands/daemon.ts +2 -31
  99. package/src/commands/gc.ts +1 -1
  100. package/src/commands/launch.ts +8 -129
  101. package/src/commands/quick-lane.ts +51 -0
  102. package/src/commands/sdk.ts +200 -26
  103. package/src/commands/setup.ts +4 -2
  104. package/src/config/file-lock-gc.ts +4 -1
  105. package/src/config/model-equivalence.ts +76 -7
  106. package/src/config/model-profile-activation.ts +1115 -265
  107. package/src/config/model-profiles.ts +134 -2
  108. package/src/config/model-registry.ts +352 -50
  109. package/src/config/model-resolver.ts +289 -38
  110. package/src/config/models-config-schema.ts +11 -4
  111. package/src/config/provider-selection-policy.ts +146 -0
  112. package/src/config/settings-schema.ts +50 -77
  113. package/src/config/settings.ts +40 -0
  114. package/src/coordinator-mcp/server.ts +32 -2
  115. package/src/edit/renderer.ts +175 -223
  116. package/src/edit/streaming.ts +108 -1
  117. package/src/export/html/index.ts +57 -7
  118. package/src/extensibility/custom-tools/types.ts +2 -0
  119. package/src/extensibility/extensions/compact-handler.ts +3 -2
  120. package/src/extensibility/extensions/runner.ts +6 -0
  121. package/src/extensibility/extensions/types.ts +3 -0
  122. package/src/gjc-runtime/gc-runtime.ts +379 -20
  123. package/src/gjc-runtime/launch-worktree.ts +5 -1
  124. package/src/gjc-runtime/team-runtime.ts +59 -2
  125. package/src/gjc-runtime/tmux-owner-isolation.ts +19 -0
  126. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +16 -1
  127. package/src/internal-urls/docs-index.generated.ts +17 -13
  128. package/src/internal-urls/local-protocol.ts +1 -0
  129. package/src/main.ts +40 -3
  130. package/src/memories/index.ts +4 -2
  131. package/src/modes/acp/acp-agent.ts +235 -91
  132. package/src/modes/components/model-selector.ts +206 -42
  133. package/src/modes/components/provider-order-context.ts +197 -0
  134. package/src/modes/components/provider-order-editor.ts +441 -0
  135. package/src/modes/components/settings-defs.ts +21 -1
  136. package/src/modes/components/settings-selector.ts +45 -2
  137. package/src/modes/components/tool-execution.ts +47 -15
  138. package/src/modes/controllers/command-controller.ts +6 -3
  139. package/src/modes/controllers/extension-ui-controller.ts +2 -0
  140. package/src/modes/controllers/input-controller.ts +73 -2
  141. package/src/modes/controllers/selector-controller.ts +260 -153
  142. package/src/modes/prompt-suggestion-controller.ts +1 -1
  143. package/src/modes/runtime-init.ts +1 -0
  144. package/src/modes/utils/ui-helpers.ts +18 -0
  145. package/src/prompts/tools/browser.md +1 -0
  146. package/src/quick-lane/classify.ts +257 -0
  147. package/src/rlm/index.ts +1 -1
  148. package/src/runtime/stt-settings-setup.ts +57 -0
  149. package/src/sdk/acp/adapter.ts +2 -1
  150. package/src/sdk/broker/broker.ts +780 -12
  151. package/src/sdk/broker/lifecycle.ts +70 -29
  152. package/src/sdk/broker/operation-receipt-ledger.ts +144 -0
  153. package/src/sdk/broker/session-index.ts +463 -48
  154. package/src/sdk/broker/state-version.ts +13 -9
  155. package/src/sdk/broker/transport.ts +14 -1
  156. package/src/sdk/bus/index.ts +34 -13
  157. package/src/sdk/bus/kind-aware-reconciliation.ts +21 -2
  158. package/src/sdk/bus/notification-service.ts +47 -1
  159. package/src/sdk/bus/prompt-reconciliation.ts +22 -3
  160. package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
  161. package/src/sdk/bus/telegram-daemon.ts +54 -3
  162. package/src/sdk/cli/rows.ts +247 -0
  163. package/src/sdk/cli/session-cli.ts +930 -79
  164. package/src/sdk/elevation/allowlist.ts +54 -0
  165. package/src/sdk/elevation/audit.ts +208 -0
  166. package/src/sdk/elevation/capability.ts +41 -0
  167. package/src/sdk/elevation/digest.ts +129 -0
  168. package/src/sdk/elevation/dispatch-receipt.ts +184 -0
  169. package/src/sdk/elevation/grant-ledger.ts +1142 -0
  170. package/src/sdk/elevation/owner.ts +109 -0
  171. package/src/sdk/guides/cache.ts +363 -0
  172. package/src/sdk/guides/catalog.ts +554 -0
  173. package/src/sdk/guides/cli.ts +235 -0
  174. package/src/sdk/guides/manifest.ts +152 -0
  175. package/src/sdk/guides/verify.ts +178 -0
  176. package/src/sdk/host/control/dispatch.ts +49 -1
  177. package/src/sdk/host/control/index.ts +1 -0
  178. package/src/sdk/host/control/operations.ts +8 -1
  179. package/src/sdk/host/query/cursor.ts +47 -3
  180. package/src/sdk/host/query/handlers.ts +155 -2
  181. package/src/sdk/host/sdk-surface-parity.test.ts +1 -0
  182. package/src/sdk/host/session-runtime.test.ts +391 -1
  183. package/src/sdk/host/session-runtime.ts +308 -73
  184. package/src/sdk/host/surface-policy.ts +3 -0
  185. package/src/sdk/host/websocket-transport.ts +7 -1
  186. package/src/sdk/mcp/server.ts +88 -15
  187. package/src/sdk/prompt-status.ts +8 -0
  188. package/src/sdk/protocol/operation-inventory.generated.json +62 -0
  189. package/src/sdk/protocol/operation-registry.ts +3 -2
  190. package/src/sdk/session.ts +87 -39
  191. package/src/sdk/transport/index.ts +1 -0
  192. package/src/sdk/transport/relay.ts +3 -0
  193. package/src/sdk/transport/serve-cli.ts +111 -24
  194. package/src/secrets/obfuscator.ts +92 -12
  195. package/src/session/agent-session.ts +322 -93
  196. package/src/session/blob-store.ts +60 -8
  197. package/src/session/fallback-chain-controller.ts +35 -0
  198. package/src/session/internal/managed-session-scope.ts +25 -3
  199. package/src/session/internal/managed-session-storage.ts +574 -73
  200. package/src/session/internal/session-memory-sidecar.ts +19 -29
  201. package/src/session/session-manager.ts +2166 -743
  202. package/src/session/session-storage.ts +715 -48
  203. package/src/setup/provider-onboarding.ts +19 -3
  204. package/src/setup/provider-presets.json +3 -1
  205. package/src/slash-commands/builtin-registry.ts +85 -3
  206. package/src/stt/downloader.ts +2 -3
  207. package/src/stt/setup.ts +29 -4
  208. package/src/task/executor.ts +22 -18
  209. package/src/task/index.ts +24 -1
  210. package/src/tools/archive-reader.ts +11 -2
  211. package/src/tools/browser/runtime-diagnostics.ts +249 -0
  212. package/src/tools/browser/tab-protocol.ts +4 -0
  213. package/src/tools/browser/tab-supervisor.ts +5 -0
  214. package/src/tools/browser/tab-worker.ts +36 -1
  215. package/src/tools/browser.ts +7 -0
  216. package/src/tools/image-gen.ts +110 -166
  217. package/src/tools/index.ts +2 -0
  218. package/src/tools/read.ts +1 -1
  219. package/src/tools/resource-gc.ts +6 -1
  220. package/src/tools/tool-catalog.generated.ts +5 -1
  221. package/src/utils/pasted-image-path.ts +93 -0
  222. package/src/web/search/index.ts +3 -3
@@ -0,0 +1,441 @@
1
+ /**
2
+ * Ordered provider priority editor for `modelProviderOrder`.
3
+ *
4
+ * Full-screen custom settings editor (Container + row list, mirroring the
5
+ * StatusLine custom editor layout) with add/remove/move-up/move-down/reset
6
+ * actions that persist to settings immediately. Rows are rebuilt from the live
7
+ * `ProviderOrderContext` snapshot before every render pass, keeping the
8
+ * previously selected provider highlighted when the underlying order or auth
9
+ * state changes. Unavailable persisted providers are retained and dimmed.
10
+ *
11
+ * The editor owns the `ProviderOrderContext` lifecycle: both normal close
12
+ * (`#close`) and external teardown (`dispose()`) release the context
13
+ * subscriptions and dispose the container, idempotently.
14
+ */
15
+
16
+ import { Container, getKeybindings, matchesKey, Spacer, Text } from "@gajae-code/tui";
17
+ import { sanitizeText } from "@gajae-code/utils";
18
+ import { theme } from "../../modes/theme/theme";
19
+ import {
20
+ formatProviderOrderLabel,
21
+ type ProviderOrderContext,
22
+ type ProviderOrderEntry,
23
+ type ProviderOrderSnapshot,
24
+ } from "./provider-order-context";
25
+
26
+ type EditorPage = "main" | "add";
27
+
28
+ type EditorRowKind = "action" | "provider" | "providerAction" | "candidate" | "back" | "empty";
29
+
30
+ interface EditorRow {
31
+ /** Stable row identity used to restore the selection after a rebuild. */
32
+ id: string;
33
+ kind: EditorRowKind;
34
+ /** Sanitized display text (no ANSI / control chars). */
35
+ label: string;
36
+ /** Right-side status text (sanitized). */
37
+ status?: string;
38
+ /** Normalized provider id when the row belongs to a provider. */
39
+ providerId?: string;
40
+ /** Render the label dimmed (e.g. unavailable providers). */
41
+ dimmer?: boolean;
42
+ }
43
+
44
+ function entryStatus(entry: ProviderOrderEntry | undefined): string {
45
+ if (!entry?.available) return "unavailable";
46
+ return entry.authenticated ? "logged in" : "not logged in";
47
+ }
48
+
49
+ function entryDetail(entry: ProviderOrderEntry | undefined, adding = false): string {
50
+ const base = entry?.available
51
+ ? entry.authenticated
52
+ ? "Credentials are available for this provider."
53
+ : "No credentials yet; it still wins priority once configured."
54
+ : "Unavailable right now (not in the model catalog); retained and skipped at runtime.";
55
+ return adding ? `${base} Adds this provider to the end of the order.` : base;
56
+ }
57
+
58
+ export class ProviderOrderEditorComponent extends Container {
59
+ #context: ProviderOrderContext;
60
+ #onClose: () => void;
61
+ #onError: ((message: string) => void) | undefined;
62
+ #page: EditorPage = "main";
63
+ #rows: EditorRow[] = [];
64
+ #selectedIndex = 0;
65
+ #selectedRowId: string | undefined;
66
+ #selectedProviderId: string | undefined;
67
+ #rowsContainer: Container;
68
+ #detailText: Text;
69
+ #hintText: Text;
70
+ #closed = false;
71
+ #persisting = false;
72
+
73
+ constructor(context: ProviderOrderContext, onClose: () => void, onError?: (message: string) => void) {
74
+ super();
75
+ this.#context = context;
76
+ this.#onClose = onClose;
77
+ this.#onError = onError;
78
+
79
+ this.addChild(new Text(theme.bold(theme.fg("accent", "Provider Priority Order")), 0, 0));
80
+ this.addChild(new Spacer(1));
81
+ this.addChild(
82
+ new Text(
83
+ theme.fg(
84
+ "muted",
85
+ "Providers listed earlier win ties during automatic model resolution. Unavailable saved entries are kept but skipped at runtime.",
86
+ ),
87
+ 0,
88
+ 0,
89
+ ),
90
+ );
91
+ this.addChild(new Spacer(1));
92
+ this.#rowsContainer = new Container();
93
+ this.addChild(this.#rowsContainer);
94
+ this.#detailText = new Text("", 0, 0);
95
+ this.addChild(new Spacer(1));
96
+ this.addChild(this.#detailText);
97
+ this.addChild(new Spacer(1));
98
+ this.#hintText = new Text("", 0, 0);
99
+ this.addChild(this.#hintText);
100
+
101
+ try {
102
+ this.#rebuild();
103
+ } catch (error) {
104
+ // The context subscribed to settings/auth in its constructor; if the
105
+ // initial snapshot/rebuild fails, release those subscriptions before
106
+ // rethrowing so a construction failure cannot leak listeners.
107
+ this.dispose();
108
+ throw error;
109
+ }
110
+ }
111
+
112
+ /** Rebuild rows from the current snapshot. Called by the controller on context changes. */
113
+ refresh(): void {
114
+ if (this.#closed) return;
115
+ this.#rebuild();
116
+ }
117
+
118
+ #rebuild(): void {
119
+ const snapshot = this.#context.snapshot();
120
+ const preserveRowId = this.#selectedRowId;
121
+ const preserveProviderId = this.#selectedProviderId;
122
+
123
+ this.#rows = this.#page === "add" ? this.#buildAddRows(snapshot) : this.#buildMainRows(snapshot);
124
+
125
+ let nextIndex = 0;
126
+ if (preserveRowId) {
127
+ const exact = this.#rows.findIndex(row => row.id === preserveRowId);
128
+ if (exact >= 0) {
129
+ nextIndex = exact;
130
+ } else {
131
+ const byProvider = preserveProviderId
132
+ ? this.#rows.findIndex(row => row.providerId === preserveProviderId)
133
+ : -1;
134
+ nextIndex =
135
+ byProvider >= 0 ? byProvider : Math.min(this.#selectedIndex, Math.max(0, this.#rows.length - 1));
136
+ }
137
+ } else {
138
+ nextIndex = Math.min(this.#selectedIndex, Math.max(0, this.#rows.length - 1));
139
+ }
140
+
141
+ this.#selectedIndex = nextIndex;
142
+ this.#captureSelection();
143
+ this.#renderRows();
144
+ this.#renderFooter();
145
+ }
146
+
147
+ #captureSelection(): void {
148
+ const row = this.#rows[this.#selectedIndex];
149
+ this.#selectedRowId = row?.id;
150
+ this.#selectedProviderId = row?.providerId;
151
+ }
152
+
153
+ #buildMainRows(snapshot: ProviderOrderSnapshot): EditorRow[] {
154
+ const rows: EditorRow[] = [
155
+ { id: "action.add", kind: "action", label: "Add provider", status: "+" },
156
+ { id: "action.reset", kind: "action", label: "Reset to default order", status: "reset" },
157
+ ];
158
+
159
+ if (snapshot.order.length === 0) {
160
+ rows.push({
161
+ id: "empty",
162
+ kind: "empty",
163
+ label: "No providers in priority order yet. Use Add provider to build the order.",
164
+ });
165
+ } else {
166
+ for (let index = 0; index < snapshot.order.length; index++) {
167
+ const providerId = snapshot.order[index]!;
168
+ const entry = snapshot.entries.find(candidate => candidate.id === providerId);
169
+ const label = sanitizeText(entry?.label ?? formatProviderOrderLabel(providerId));
170
+ rows.push({
171
+ id: `provider.${providerId}`,
172
+ kind: "provider",
173
+ label: `#${index + 1} ${label}`,
174
+ status: entryStatus(entry),
175
+ providerId,
176
+ dimmer: entry ? !entry.available : true,
177
+ });
178
+ rows.push({
179
+ id: `moveup.${providerId}`,
180
+ kind: "providerAction",
181
+ label: `Move up: ${label}`,
182
+ status: "up",
183
+ providerId,
184
+ });
185
+ rows.push({
186
+ id: `movedown.${providerId}`,
187
+ kind: "providerAction",
188
+ label: `Move down: ${label}`,
189
+ status: "down",
190
+ providerId,
191
+ });
192
+ rows.push({
193
+ id: `remove.${providerId}`,
194
+ kind: "providerAction",
195
+ label: `Remove: ${label}`,
196
+ status: "remove",
197
+ providerId,
198
+ });
199
+ }
200
+ }
201
+
202
+ rows.push({ id: "action.done", kind: "action", label: "Done", status: "done" });
203
+ return rows;
204
+ }
205
+
206
+ #buildAddRows(snapshot: ProviderOrderSnapshot): EditorRow[] {
207
+ const rows: EditorRow[] = [{ id: "back", kind: "back", label: "Back to order" }];
208
+ const candidates = snapshot.entries.filter(entry => !entry.inOrder);
209
+ if (candidates.length === 0) {
210
+ rows.push({
211
+ id: "empty",
212
+ kind: "empty",
213
+ label: "Every known provider is already in the priority order.",
214
+ });
215
+ } else {
216
+ for (const entry of candidates) {
217
+ rows.push({
218
+ id: `candidate.${entry.id}`,
219
+ kind: "candidate",
220
+ label: sanitizeText(entry.label),
221
+ status: entryStatus(entry),
222
+ providerId: entry.id,
223
+ dimmer: !entry.available,
224
+ });
225
+ }
226
+ }
227
+ return rows;
228
+ }
229
+
230
+ #renderRows(): void {
231
+ this.#rowsContainer.clear();
232
+ for (let index = 0; index < this.#rows.length; index++) {
233
+ const row = this.#rows[index];
234
+ if (!row) continue;
235
+ this.#rowsContainer.addChild(new Text(this.#renderRow(row, index === this.#selectedIndex), 0, 0));
236
+ }
237
+ }
238
+
239
+ #renderRow(row: EditorRow, selected: boolean): string {
240
+ const prefix = selected ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
241
+ let label = row.label;
242
+ if (selected) {
243
+ label = theme.fg("accent", label);
244
+ } else if (row.dimmer) {
245
+ label = theme.fg("dim", label);
246
+ }
247
+ let line = `${prefix}${label}`;
248
+ if (row.status) {
249
+ const status =
250
+ selected || row.kind === "providerAction" || row.kind === "action"
251
+ ? theme.fg("dim", row.status)
252
+ : row.status === "logged in"
253
+ ? theme.fg("success", row.status)
254
+ : theme.fg("dim", row.status);
255
+ line += ` ${status}`;
256
+ }
257
+ return line;
258
+ }
259
+
260
+ #renderFooter(): void {
261
+ const row = this.#rows[this.#selectedIndex];
262
+ let detail = "";
263
+ if (row) {
264
+ switch (row.kind) {
265
+ case "provider": {
266
+ const entry = this.#context.snapshot().entries.find(candidate => candidate.id === row.providerId);
267
+ detail = entryDetail(entry);
268
+ break;
269
+ }
270
+ case "candidate": {
271
+ const entry = this.#context.snapshot().entries.find(candidate => candidate.id === row.providerId);
272
+ detail = entryDetail(entry, true);
273
+ break;
274
+ }
275
+ case "providerAction": {
276
+ detail = row.id.startsWith("moveup.")
277
+ ? "Move this provider earlier in the priority order."
278
+ : row.id.startsWith("movedown.")
279
+ ? "Move this provider later in the priority order."
280
+ : "Remove this provider from the priority order.";
281
+ break;
282
+ }
283
+ case "action":
284
+ detail =
285
+ row.id === "action.add"
286
+ ? "Append a provider to the end of the priority order."
287
+ : row.id === "action.reset"
288
+ ? "Clear the saved order so default provider ranking applies."
289
+ : "Close the editor. Changes already saved take effect immediately.";
290
+ break;
291
+ case "back":
292
+ detail = "Return to the priority order without adding a provider.";
293
+ break;
294
+ default:
295
+ detail = "";
296
+ break;
297
+ }
298
+ }
299
+ this.#detailText.setText(theme.fg("muted", detail));
300
+ this.#hintText.setText(
301
+ theme.fg(
302
+ "dim",
303
+ this.#page === "add"
304
+ ? " Up/Down navigate · Enter add · Esc back"
305
+ : " Up/Down navigate · Enter select · Esc close",
306
+ ),
307
+ );
308
+ }
309
+
310
+ #moveSelection(delta: 1 | -1): void {
311
+ if (this.#rows.length === 0) return;
312
+ this.#selectedIndex = (this.#selectedIndex + delta + this.#rows.length) % this.#rows.length;
313
+ this.#captureSelection();
314
+ this.#rebuild();
315
+ }
316
+
317
+ handleInput(data: string): void {
318
+ if (this.#closed || this.#persisting) return;
319
+ if (matchesKey(data, "up")) {
320
+ this.#moveSelection(-1);
321
+ return;
322
+ }
323
+ if (matchesKey(data, "down")) {
324
+ this.#moveSelection(1);
325
+ return;
326
+ }
327
+ if (matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n") {
328
+ this.#activate();
329
+ return;
330
+ }
331
+ if (getKeybindings().matches(data, "tui.select.cancel")) {
332
+ if (this.#page === "add") {
333
+ this.#page = "main";
334
+ this.#selectedIndex = 0;
335
+ this.#captureSelection();
336
+ this.#rebuild();
337
+ } else {
338
+ this.#close();
339
+ }
340
+ }
341
+ }
342
+
343
+ #activate(): void {
344
+ const row = this.#rows[this.#selectedIndex];
345
+ if (!row) return;
346
+
347
+ if (this.#page === "add") {
348
+ if (row.kind === "back") {
349
+ this.#page = "main";
350
+ this.#selectedIndex = 0;
351
+ this.#captureSelection();
352
+ this.#rebuild();
353
+ return;
354
+ }
355
+ if (row.kind === "candidate" && row.providerId) {
356
+ const order = this.#context.snapshot().order;
357
+ void this.#persist([...order, row.providerId]);
358
+ return;
359
+ }
360
+ return;
361
+ }
362
+
363
+ if (row.kind === "provider") return;
364
+
365
+ if (row.kind === "providerAction" && row.providerId) {
366
+ this.#mutateOrder(row);
367
+ return;
368
+ }
369
+
370
+ switch (row.id) {
371
+ case "action.add":
372
+ this.#page = "add";
373
+ this.#selectedIndex = 0;
374
+ this.#captureSelection();
375
+ this.#rebuild();
376
+ return;
377
+ case "action.reset":
378
+ void this.#persistReset();
379
+ return;
380
+ case "action.done":
381
+ this.#close();
382
+ return;
383
+ }
384
+ }
385
+
386
+ #mutateOrder(row: EditorRow): void {
387
+ const providerId = row.providerId;
388
+ if (!providerId) return;
389
+ const order = [...this.#context.snapshot().order];
390
+ const index = order.indexOf(providerId);
391
+ if (index < 0) return;
392
+ if (row.id.startsWith("moveup.")) {
393
+ if (index === 0) return;
394
+ order.splice(index - 1, 2, providerId, order[index - 1]!);
395
+ } else if (row.id.startsWith("movedown.")) {
396
+ if (index === order.length - 1) return;
397
+ order.splice(index, 2, order[index + 1]!, providerId);
398
+ } else if (row.id.startsWith("remove.")) {
399
+ order.splice(index, 1);
400
+ } else {
401
+ return;
402
+ }
403
+ void this.#persist(order);
404
+ }
405
+
406
+ async #persist(order: readonly string[]): Promise<void> {
407
+ this.#persisting = true;
408
+ try {
409
+ await this.#context.persistOrder(order);
410
+ } catch (error) {
411
+ this.#onError?.(error instanceof Error ? error.message : String(error));
412
+ } finally {
413
+ this.#persisting = false;
414
+ if (!this.#closed) this.#rebuild();
415
+ }
416
+ }
417
+
418
+ async #persistReset(): Promise<void> {
419
+ this.#persisting = true;
420
+ try {
421
+ await this.#context.resetOrder();
422
+ } catch (error) {
423
+ this.#onError?.(error instanceof Error ? error.message : String(error));
424
+ } finally {
425
+ this.#persisting = false;
426
+ if (!this.#closed) this.#rebuild();
427
+ }
428
+ }
429
+
430
+ #close(): void {
431
+ if (this.#closed) return;
432
+ this.dispose();
433
+ this.#onClose();
434
+ }
435
+ override dispose(): void {
436
+ if (this.#closed) return;
437
+ this.#closed = true;
438
+ this.#context.dispose();
439
+ super.dispose();
440
+ }
441
+ }
@@ -64,7 +64,17 @@ export interface TextInputSettingDef extends BaseSettingDef {
64
64
  type: "text";
65
65
  }
66
66
 
67
- export type SettingDef = BooleanSettingDef | EnumSettingDef | SubmenuSettingDef | TextInputSettingDef;
67
+ /** Array setting rendered by a dedicated ordered-list editor (currently only `modelProviderOrder`). */
68
+ export interface ProviderOrderSettingDef extends BaseSettingDef {
69
+ type: "providerOrder";
70
+ }
71
+
72
+ export type SettingDef =
73
+ | BooleanSettingDef
74
+ | EnumSettingDef
75
+ | SubmenuSettingDef
76
+ | TextInputSettingDef
77
+ | ProviderOrderSettingDef;
68
78
 
69
79
  // ═══════════════════════════════════════════════════════════════════════════
70
80
  // Condition Functions
@@ -131,6 +141,16 @@ function pathToSettingDef(path: SettingPath): SettingDef | null {
131
141
  return { ...base, type: "text" };
132
142
  }
133
143
 
144
+ if (schemaType === "array") {
145
+ // Arrays render through dedicated custom editors rather than the generic
146
+ // value list. `modelProviderOrder` is the only array with a provider-order
147
+ // editor today; other arrays with a `ui` block stay hidden here.
148
+ if (path === "modelProviderOrder") {
149
+ return { ...base, type: "providerOrder" };
150
+ }
151
+ return null;
152
+ }
153
+
134
154
  return null;
135
155
  }
136
156
 
@@ -34,6 +34,7 @@ import {
34
34
  } from "./notifications-settings-editor";
35
35
  import { createPetSelectItems, getPetUnavailableWarning, isPetAvailable } from "./pet-capability";
36
36
  import { handleInputOrEscape, PluginSettingsComponent } from "./plugin-settings";
37
+ import { normalizeProviderOrder } from "./provider-order-context";
37
38
  import { getSettingsForTab, type SettingDef } from "./settings-defs";
38
39
  import { getPreset } from "./status-line/presets";
39
40
  import { ALL_SEGMENT_IDS } from "./status-line/segments";
@@ -724,6 +725,8 @@ export interface SettingsCallbacks {
724
725
  onPluginsChanged?: () => void;
725
726
  /** Called when asynchronously rebuilt settings content needs a repaint. */
726
727
  onRenderRequested?: () => void;
728
+ /** Create the ordered provider-priority editor for `modelProviderOrder`. */
729
+ createProviderOrderEditor?: (done: () => void) => Container;
727
730
  /** Called when an interactive setting cannot be committed. */
728
731
  onError?: (message: string) => void;
729
732
  /** Called when settings panel is closed */
@@ -762,6 +765,7 @@ export class SettingsSelectorComponent extends Container {
762
765
  #statusPreviewText: Text | null = null;
763
766
  #currentTabId: SettingTab | "plugins" | "gjc-bundles" = "appearance";
764
767
  #textInputActive = false;
768
+ #activeProviderOrderEditor: Container | null = null;
765
769
 
766
770
  constructor(
767
771
  private readonly context: SettingsRuntimeContext,
@@ -795,6 +799,11 @@ export class SettingsSelectorComponent extends Container {
795
799
  if (this.#currentTabId === "notifications" && tabId !== "notifications" && !this.#disposeNotificationsEditor()) {
796
800
  return;
797
801
  }
802
+ // Release an open provider-order editor (and its context subscriptions)
803
+ // before switching tabs; the submenu's done() never runs on tab change,
804
+ // so without this the abandoned editor's listeners would survive.
805
+ this.#activeProviderOrderEditor?.dispose();
806
+ this.#activeProviderOrderEditor = null;
798
807
  this.#currentTabId = tabId;
799
808
 
800
809
  // Remove current content
@@ -884,6 +893,29 @@ export class SettingsSelectorComponent extends Container {
884
893
  submenu: (cv, done) => this.#createSubmenu(def, cv, done),
885
894
  };
886
895
 
896
+ case "providerOrder": {
897
+ const createEditor = this.callbacks.createProviderOrderEditor;
898
+ if (!createEditor) return null;
899
+ return {
900
+ id: def.path,
901
+ label: def.label,
902
+ description: def.description,
903
+ currentValue: `${normalizeProviderOrder(settings.getGlobal("modelProviderOrder") ?? []).length} configured`,
904
+ submenu: (_currentValue, done) => {
905
+ const editor = createEditor(() => {
906
+ this.#activeProviderOrderEditor = null;
907
+ // Rebuild the parent list from current settings so the
908
+ // `${count} configured` summary reflects the editor's
909
+ // persistence before the submenu closes.
910
+ this.#refreshCurrentTabItems();
911
+ done();
912
+ });
913
+ this.#activeProviderOrderEditor = editor;
914
+ return editor;
915
+ },
916
+ };
917
+ }
918
+
887
919
  case "text":
888
920
  return {
889
921
  id: def.path,
@@ -1035,6 +1067,11 @@ export class SettingsSelectorComponent extends Container {
1035
1067
  options,
1036
1068
  currentValue,
1037
1069
  value => {
1070
+ if (def.path === "modelProfile.default") {
1071
+ this.callbacks.onChange(def.path, value);
1072
+ done(value);
1073
+ return;
1074
+ }
1038
1075
  if (def.path === "pet.mode") {
1039
1076
  // The shared pet commit policy rechecks capability immediately
1040
1077
  // before mutation and persists only on acceptance; the settings
@@ -1264,9 +1301,11 @@ export class SettingsSelectorComponent extends Container {
1264
1301
  }
1265
1302
 
1266
1303
  /** Re-evaluate condition gates against the current settings and refresh the active list. */
1267
- #refreshCurrentTabItems(defs: SettingDef[]): void {
1304
+ #refreshCurrentTabItems(defs?: SettingDef[]): void {
1268
1305
  if (this.#currentTabId === "plugins" || this.#currentTabId === "gjc-bundles" || !this.#currentList) return;
1269
- this.#currentList.setItems(this.#buildItemsForTab(defs, this.#currentTabId));
1306
+ this.#currentList.setItems(
1307
+ this.#buildItemsForTab(defs ?? getSettingsForTab(this.#currentTabId), this.#currentTabId),
1308
+ );
1270
1309
  }
1271
1310
 
1272
1311
  /**
@@ -1335,6 +1374,10 @@ export class SettingsSelectorComponent extends Container {
1335
1374
 
1336
1375
  override dispose(): void {
1337
1376
  if (this.#notificationsEditor?.navigationLocked) return;
1377
+ // Release an open provider-order editor (and its context subscriptions)
1378
+ // when the whole selector is torn down without a normal close.
1379
+ this.#activeProviderOrderEditor?.dispose();
1380
+ this.#activeProviderOrderEditor = null;
1338
1381
  this.#notificationsEditor?.dispose();
1339
1382
  this.#notificationsEditor = null;
1340
1383
  this.#gjcBundleComponent?.dispose();