@gajae-code/coding-agent 0.7.7 → 0.7.9

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 (96) hide show
  1. package/CHANGELOG.md +38 -1
  2. package/README.md +14 -0
  3. package/dist/types/cli/local-provider-smoke.d.ts +59 -0
  4. package/dist/types/cli/notify-cli.d.ts +1 -1
  5. package/dist/types/cli/update-cli.d.ts +24 -1
  6. package/dist/types/commands/local-provider.d.ts +34 -0
  7. package/dist/types/config/keybindings.d.ts +1 -1
  8. package/dist/types/config/model-profile-activation.d.ts +19 -2
  9. package/dist/types/config/model-profiles.d.ts +1 -0
  10. package/dist/types/config/model-registry.d.ts +9 -0
  11. package/dist/types/config/models-config-schema.d.ts +5 -0
  12. package/dist/types/config/settings-schema.d.ts +45 -0
  13. package/dist/types/config/settings.d.ts +7 -0
  14. package/dist/types/coordinator-mcp/server.d.ts +8 -0
  15. package/dist/types/gjc-runtime/launch-tmux.d.ts +14 -5
  16. package/dist/types/gjc-runtime/team-runtime.d.ts +1 -1
  17. package/dist/types/gjc-runtime/tmux-common.d.ts +12 -0
  18. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +3 -1
  19. package/dist/types/modes/components/custom-model-preset-wizard.d.ts +1 -1
  20. package/dist/types/modes/components/hook-input.d.ts +1 -0
  21. package/dist/types/modes/components/model-selector.d.ts +11 -0
  22. package/dist/types/modes/components/settings-selector.d.ts +4 -1
  23. package/dist/types/modes/components/status-line/types.d.ts +5 -7
  24. package/dist/types/modes/components/status-line.d.ts +7 -3
  25. package/dist/types/modes/controllers/extension-ui-controller.d.ts +3 -1
  26. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  27. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  28. package/dist/types/modes/interactive-mode.d.ts +3 -1
  29. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -0
  30. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +21 -1
  31. package/dist/types/modes/tmux-scroll.d.ts +18 -0
  32. package/dist/types/modes/types.d.ts +3 -1
  33. package/dist/types/modes/utils/terminal-bell.d.ts +3 -0
  34. package/dist/types/notifications/daemon-paths.d.ts +9 -0
  35. package/dist/types/notifications/telegram-daemon-cli.d.ts +21 -4
  36. package/dist/types/notifications/telegram-daemon.d.ts +1 -10
  37. package/dist/types/tools/ask.d.ts +24 -0
  38. package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -2
  39. package/package.json +7 -7
  40. package/src/cli/local-provider-smoke.ts +632 -0
  41. package/src/cli/notify-cli.ts +5 -3
  42. package/src/cli/update-cli.ts +155 -13
  43. package/src/cli.ts +19 -0
  44. package/src/commands/launch.ts +46 -1
  45. package/src/commands/local-provider.ts +62 -0
  46. package/src/commands/notify.ts +1 -1
  47. package/src/config/keybindings.ts +1 -1
  48. package/src/config/model-profile-activation.ts +92 -11
  49. package/src/config/model-profiles.ts +15 -2
  50. package/src/config/model-registry.ts +136 -6
  51. package/src/config/models-config-schema.ts +9 -0
  52. package/src/config/settings-schema.ts +48 -0
  53. package/src/config/settings.ts +11 -0
  54. package/src/coordinator-mcp/server.ts +280 -36
  55. package/src/deep-interview/render-middleware.ts +1 -0
  56. package/src/defaults/gjc/skills/deep-interview/SKILL.md +14 -6
  57. package/src/defaults/gjc/skills/ralplan/SKILL.md +14 -7
  58. package/src/gjc-runtime/deep-interview-runtime.ts +1 -1
  59. package/src/gjc-runtime/launch-tmux.ts +321 -38
  60. package/src/gjc-runtime/launch-worktree.ts +16 -2
  61. package/src/gjc-runtime/team-runtime.ts +87 -7
  62. package/src/gjc-runtime/tmux-common.ts +16 -0
  63. package/src/gjc-runtime/tmux-sessions.ts +13 -9
  64. package/src/gjc-runtime/ultragoal-guard.ts +11 -5
  65. package/src/internal-urls/docs-index.generated.ts +10 -9
  66. package/src/modes/components/agent-dashboard.ts +2 -1
  67. package/src/modes/components/custom-model-preset-wizard.ts +31 -192
  68. package/src/modes/components/hook-editor.ts +7 -4
  69. package/src/modes/components/hook-input.ts +4 -0
  70. package/src/modes/components/model-selector.ts +260 -12
  71. package/src/modes/components/session-observer-overlay.ts +3 -3
  72. package/src/modes/components/settings-selector.ts +506 -5
  73. package/src/modes/components/status-line/segments.ts +16 -19
  74. package/src/modes/components/status-line/types.ts +6 -2
  75. package/src/modes/components/status-line.ts +70 -35
  76. package/src/modes/controllers/event-controller.ts +98 -4
  77. package/src/modes/controllers/extension-ui-controller.ts +5 -0
  78. package/src/modes/controllers/input-controller.ts +15 -0
  79. package/src/modes/controllers/selector-controller.ts +142 -11
  80. package/src/modes/interactive-mode.ts +7 -2
  81. package/src/modes/prompt-action-autocomplete.ts +8 -0
  82. package/src/modes/shared/agent-wire/deep-interview-gate.ts +11 -4
  83. package/src/modes/tmux-scroll.ts +92 -0
  84. package/src/modes/types.ts +6 -1
  85. package/src/modes/utils/terminal-bell.ts +48 -0
  86. package/src/notifications/daemon-paths.ts +22 -0
  87. package/src/notifications/telegram-daemon-cli.ts +125 -13
  88. package/src/notifications/telegram-daemon.ts +6 -21
  89. package/src/prompts/system/system-prompt.md +5 -0
  90. package/src/sdk.ts +10 -1
  91. package/src/session/agent-session.ts +84 -6
  92. package/src/slash-commands/builtin-registry.ts +106 -13
  93. package/src/tools/ask.ts +11 -1
  94. package/src/tools/eval.ts +2 -2
  95. package/src/tools/render-utils.ts +2 -0
  96. package/src/tools/ultragoal-ask-guard.ts +39 -5
@@ -28,6 +28,7 @@ import {
28
28
  resolveModelRoleValue,
29
29
  type ScopedModelSelection,
30
30
  } from "../../config/model-resolver";
31
+ import type { ModelProfileConfig } from "../../config/models-config-schema";
31
32
  import type { Settings } from "../../config/settings";
32
33
  import { type ThemeColor, theme } from "../../modes/theme/theme";
33
34
  import { formatModelOnboardingInlineHint } from "../../setup/model-onboarding-guidance";
@@ -105,6 +106,15 @@ export type ModelSelectorSelection =
105
106
  }
106
107
  | {
107
108
  kind: "createProfile";
109
+ profile: ModelProfileConfig;
110
+ }
111
+ | {
112
+ kind: "renameProfile";
113
+ profileName: string;
114
+ }
115
+ | {
116
+ kind: "deleteProfile";
117
+ profileName: string;
108
118
  };
109
119
 
110
120
  interface PendingThinkingChoice {
@@ -155,11 +165,27 @@ interface PresetCreateRow {
155
165
  kind: "create";
156
166
  }
157
167
 
168
+ interface PresetCreateUnavailableRow {
169
+ kind: "createUnavailable";
170
+ label: string;
171
+ }
172
+
173
+ interface PresetAlreadySavedRow {
174
+ kind: "alreadySaved";
175
+ profile: ModelProfileDefinition;
176
+ }
177
+
158
178
  interface PresetBrowseRow {
159
179
  kind: "browse";
160
180
  }
161
181
 
162
- type PresetLandingRow = PresetGroupRow | PresetProfileRow | PresetCreateRow | PresetBrowseRow;
182
+ type PresetLandingRow =
183
+ | PresetGroupRow
184
+ | PresetProfileRow
185
+ | PresetCreateRow
186
+ | PresetCreateUnavailableRow
187
+ | PresetAlreadySavedRow
188
+ | PresetBrowseRow;
163
189
 
164
190
  // Stable logical identity for a preset landing row, independent of its current
165
191
  // list position. Used to relocate the cursor after the expanded group changes so
@@ -174,6 +200,10 @@ function presetRowIdentity(row: PresetLandingRow): string {
174
200
  return "browse";
175
201
  case "create":
176
202
  return "create";
203
+ case "createUnavailable":
204
+ return "createUnavailable";
205
+ case "alreadySaved":
206
+ return `alreadySaved:${row.profile.name}`;
177
207
  }
178
208
  }
179
209
 
@@ -185,6 +215,7 @@ const PROFILE_ROLE_PREVIEW_ORDER: GjcModelAssignmentTargetId[] = [
185
215
  "architect",
186
216
  ];
187
217
  const PRESET_SCOPE_LABELS = ["Apply for this session", "Set as default"];
218
+ const CUSTOM_PRESET_SCOPE_LABELS = ["Apply for this session", "Set as default", "Rename", "Delete"];
188
219
 
189
220
  function isPrintableCharacter(keyData: string): boolean {
190
221
  return keyData.length === 1 && keyData >= " " && keyData !== "\x7f";
@@ -193,6 +224,68 @@ function isPrintableCharacter(keyData: string): boolean {
193
224
  function profileRequiredProviders(profile: ModelProfileDefinition): string[] {
194
225
  return [...new Set(profile.requiredProviders)].sort((a, b) => a.localeCompare(b));
195
226
  }
227
+
228
+ function isInheritedRoleSelector(value: string | undefined): boolean {
229
+ const normalized = value?.trim();
230
+ return !normalized || normalized === "default" || normalized === "pi/default";
231
+ }
232
+
233
+ function getDefaultAliasThinkingLevel(value: string | undefined): ThinkingLevel | undefined {
234
+ const normalized = value?.trim();
235
+ if (!normalized?.startsWith("pi/default:")) return undefined;
236
+ return parseThinkingLevel(normalized.slice("pi/default:".length));
237
+ }
238
+
239
+ function getSelectorProvider(selector: string): string | undefined {
240
+ const slashIndex = selector.indexOf("/");
241
+ return slashIndex > 0 ? selector.slice(0, slashIndex) : undefined;
242
+ }
243
+
244
+ function deriveRequiredProviders(modelMapping: ModelProfileConfig["model_mapping"]): string[] {
245
+ const providers = new Set<string>();
246
+ for (const selector of Object.values(modelMapping)) {
247
+ const provider = getSelectorProvider(selector);
248
+ if (provider) providers.add(provider);
249
+ }
250
+ return [...providers].sort((a, b) => a.localeCompare(b));
251
+ }
252
+
253
+ function sameStringRecord(
254
+ left: Readonly<Record<string, string | undefined>>,
255
+ right: Readonly<Record<string, string | undefined>>,
256
+ ): boolean {
257
+ const leftEntries = Object.entries(left)
258
+ .filter((entry): entry is [string, string] => entry[1] !== undefined)
259
+ .sort(([a], [b]) => a.localeCompare(b));
260
+ const rightEntries = Object.entries(right)
261
+ .filter((entry): entry is [string, string] => entry[1] !== undefined)
262
+ .sort(([a], [b]) => a.localeCompare(b));
263
+ if (leftEntries.length !== rightEntries.length) return false;
264
+ for (let i = 0; i < leftEntries.length; i++) {
265
+ const leftEntry = leftEntries[i];
266
+ const rightEntry = rightEntries[i];
267
+ if (!leftEntry || !rightEntry || leftEntry[0] !== rightEntry[0] || leftEntry[1] !== rightEntry[1]) {
268
+ return false;
269
+ }
270
+ }
271
+ return true;
272
+ }
273
+
274
+ function sameStringArray(left: readonly string[], right: readonly string[]): boolean {
275
+ if (left.length !== right.length) return false;
276
+ for (let i = 0; i < left.length; i++) {
277
+ if (left[i] !== right[i]) return false;
278
+ }
279
+ return true;
280
+ }
281
+
282
+ function hasPersistableProfileSnapshot(snapshot: ModelProfileConfig): boolean {
283
+ return Object.keys(snapshot.model_mapping).length > 0 && snapshot.required_providers.length > 0;
284
+ }
285
+
286
+ function isCustomUserProfile(profile: ModelProfileDefinition): boolean {
287
+ return profile.source === "user";
288
+ }
196
289
  /**
197
290
  * Component that renders a canonical model selector with provider tabs.
198
291
  * - Preset landing Left/Right: Collapse/expand selected provider
@@ -733,6 +826,67 @@ export class ModelSelectorComponent extends Container {
733
826
  return groupModelProfilesForPresetLanding(this.#modelRegistry.getModelProfiles?.() ?? new Map());
734
827
  }
735
828
 
829
+ #formatCurrentModelSelector(
830
+ thinkingLevel: ThinkingLevel | undefined = this.#currentThinkingLevel,
831
+ ): string | undefined {
832
+ if (!this.#currentModel) return undefined;
833
+ return formatModelSelectorValue(`${this.#currentModel.provider}/${this.#currentModel.id}`, thinkingLevel);
834
+ }
835
+
836
+ #resolveProfileModelSelector(value: string | undefined): string | undefined {
837
+ const normalized = value?.trim();
838
+ if (isInheritedRoleSelector(normalized)) return undefined;
839
+
840
+ const resolved = resolveModelRoleValue(normalized, this.#modelRegistry.getAll(), {
841
+ settings: this.#settings,
842
+ matchPreferences: { usageOrder: this.#settings.getStorage()?.getModelUsageOrder() },
843
+ modelRegistry: this.#modelRegistry,
844
+ });
845
+ if (resolved.model) {
846
+ return formatModelSelectorValue(`${resolved.model.provider}/${resolved.model.id}`, resolved.thinkingLevel);
847
+ }
848
+
849
+ const inheritedDefaultThinkingLevel = getDefaultAliasThinkingLevel(normalized);
850
+ if (inheritedDefaultThinkingLevel) return this.#formatCurrentModelSelector(inheritedDefaultThinkingLevel);
851
+ return undefined;
852
+ }
853
+
854
+ #buildCustomModelProfileSnapshot(): ModelProfileConfig {
855
+ const modelMapping: ModelProfileConfig["model_mapping"] = {};
856
+ const currentModelSelector = this.#formatCurrentModelSelector();
857
+ if (currentModelSelector) {
858
+ modelMapping.default = currentModelSelector;
859
+ } else {
860
+ const defaultRole = this.#settings.getModelRole("default");
861
+ const defaultSelector = this.#resolveProfileModelSelector(defaultRole);
862
+ if (defaultSelector) modelMapping.default = defaultSelector;
863
+ }
864
+
865
+ const agentOverrides = this.#settings.get("task.agentModelOverrides");
866
+ for (const role of GJC_MODEL_ASSIGNMENT_TARGET_IDS) {
867
+ if (role === "default") continue;
868
+ const selector = this.#resolveProfileModelSelector(agentOverrides[role]);
869
+ if (selector) modelMapping[role] = selector;
870
+ }
871
+
872
+ return {
873
+ required_providers: deriveRequiredProviders(modelMapping),
874
+ model_mapping: modelMapping,
875
+ };
876
+ }
877
+
878
+ #findDuplicateGeneratedProfile(snapshot: ModelProfileConfig): ModelProfileDefinition | undefined {
879
+ for (const profile of this.#modelRegistry.getModelProfiles?.().values() ?? []) {
880
+ if (
881
+ sameStringRecord(profile.modelMapping, snapshot.model_mapping) &&
882
+ sameStringArray(profile.requiredProviders, snapshot.required_providers)
883
+ ) {
884
+ return profile;
885
+ }
886
+ }
887
+ return undefined;
888
+ }
889
+
736
890
  #getPresetRows(): PresetLandingRow[] {
737
891
  const rows: PresetLandingRow[] = [];
738
892
  for (const [groupId, profiles] of this.#getPresetGroups()) {
@@ -741,7 +895,13 @@ export class ModelSelectorComponent extends Container {
741
895
  for (const profile of profiles) rows.push({ kind: "profile", groupId, profile });
742
896
  }
743
897
  }
744
- rows.push({ kind: "create" });
898
+ const snapshot = this.#buildCustomModelProfileSnapshot();
899
+ if (hasPersistableProfileSnapshot(snapshot)) {
900
+ const duplicateProfile = this.#findDuplicateGeneratedProfile(snapshot);
901
+ rows.push(duplicateProfile ? { kind: "alreadySaved", profile: duplicateProfile } : { kind: "create" });
902
+ } else {
903
+ rows.push({ kind: "createUnavailable", label: "Select a model before creating a custom preset" });
904
+ }
745
905
  rows.push({ kind: "browse" });
746
906
  return rows;
747
907
  }
@@ -812,10 +972,24 @@ export class ModelSelectorComponent extends Container {
812
972
  this.#presetCursor = relocated;
813
973
  return true;
814
974
  }
815
-
975
+ #relocatePresetCursorForProfile(profileName: string): boolean {
976
+ for (const [groupId, profiles] of this.#getPresetGroups()) {
977
+ if (!profiles.some(profile => profile.name === profileName)) continue;
978
+ this.#expandedPresetProviderId = groupId;
979
+ return this.#relocatePresetCursor(`profile:${groupId}:${profileName}`);
980
+ }
981
+ return false;
982
+ }
816
983
  #expandSelectedPresetProvider(): void {
817
984
  const selected = this.#getSelectedPresetRow();
818
- if (!selected || selected.kind === "browse" || selected.kind === "create") return;
985
+ if (
986
+ !selected ||
987
+ selected.kind === "browse" ||
988
+ selected.kind === "create" ||
989
+ selected.kind === "createUnavailable" ||
990
+ selected.kind === "alreadySaved"
991
+ )
992
+ return;
819
993
  if (this.#expandedPresetProviderId === selected.groupId) return;
820
994
  const targetIdentity = presetRowIdentity(selected);
821
995
  this.#expandedPresetProviderId = selected.groupId;
@@ -824,7 +998,14 @@ export class ModelSelectorComponent extends Container {
824
998
 
825
999
  #collapseSelectedPresetProvider(): void {
826
1000
  const selected = this.#getSelectedPresetRow();
827
- if (!selected || selected.kind === "browse" || selected.kind === "create") return;
1001
+ if (
1002
+ !selected ||
1003
+ selected.kind === "browse" ||
1004
+ selected.kind === "create" ||
1005
+ selected.kind === "createUnavailable" ||
1006
+ selected.kind === "alreadySaved"
1007
+ )
1008
+ return;
828
1009
  if (this.#expandedPresetProviderId !== selected.groupId) return;
829
1010
  const targetIdentity = selected.kind === "profile" ? `group:${selected.groupId}` : presetRowIdentity(selected);
830
1011
  this.#expandedPresetProviderId = undefined;
@@ -860,6 +1041,18 @@ export class ModelSelectorComponent extends Container {
860
1041
  this.#listContainer.addChild(new Text(`${prefix}${selected ? theme.fg("accent", label) : label}`, 0, 0));
861
1042
  continue;
862
1043
  }
1044
+ if (row.kind === "createUnavailable") {
1045
+ const renderedLabel = selected ? theme.fg("accent", row.label) : theme.fg("dim", row.label);
1046
+ this.#listContainer.addChild(new Text(`${prefix}${renderedLabel}`, 0, 0));
1047
+ continue;
1048
+ }
1049
+ if (row.kind === "alreadySaved") {
1050
+ const presentation = getModelProfilePresentation(row.profile);
1051
+ const label = `Already saved as ${presentation.displayName}`;
1052
+ const renderedLabel = selected ? theme.fg("accent", label) : theme.fg("dim", label);
1053
+ this.#listContainer.addChild(new Text(`${prefix}${renderedLabel}`, 0, 0));
1054
+ continue;
1055
+ }
863
1056
  if (row.kind === "browse") {
864
1057
  const label = "Browse all models";
865
1058
  this.#listContainer.addChild(new Text(`${prefix}${selected ? theme.fg("accent", label) : label}`, 0, 0));
@@ -908,8 +1101,9 @@ export class ModelSelectorComponent extends Container {
908
1101
  }
909
1102
  this.#listContainer.addChild(new Spacer(1));
910
1103
  if (this.#presetScopeMenuOpen) {
911
- for (let i = 0; i < PRESET_SCOPE_LABELS.length; i++) {
912
- const label = PRESET_SCOPE_LABELS[i] ?? "";
1104
+ const actionLabels = this.#getPresetScopeLabels(profile);
1105
+ for (let i = 0; i < actionLabels.length; i++) {
1106
+ const label = actionLabels[i] ?? "";
913
1107
  const prefix = i === this.#presetScopeIndex ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
914
1108
  this.#listContainer.addChild(
915
1109
  new Text(`${prefix}${i === this.#presetScopeIndex ? theme.fg("accent", label) : label}`, 0, 0),
@@ -1220,8 +1414,8 @@ export class ModelSelectorComponent extends Container {
1220
1414
  const rows = this.#getPresetRows();
1221
1415
  if (rows.length === 0) return;
1222
1416
  if (this.#presetScopeMenuOpen) {
1223
- this.#presetScopeIndex =
1224
- this.#presetScopeIndex === 0 ? PRESET_SCOPE_LABELS.length - 1 : this.#presetScopeIndex - 1;
1417
+ const actionCount = this.#getPreviewPresetScopeLabels().length;
1418
+ this.#presetScopeIndex = this.#presetScopeIndex === 0 ? actionCount - 1 : this.#presetScopeIndex - 1;
1225
1419
  } else {
1226
1420
  this.#presetCursor = this.#presetCursor === 0 ? rows.length - 1 : this.#presetCursor - 1;
1227
1421
  this.#previewProfileName = undefined;
@@ -1235,7 +1429,7 @@ export class ModelSelectorComponent extends Container {
1235
1429
  const rows = this.#getPresetRows();
1236
1430
  if (rows.length === 0) return;
1237
1431
  if (this.#presetScopeMenuOpen) {
1238
- this.#presetScopeIndex = (this.#presetScopeIndex + 1) % PRESET_SCOPE_LABELS.length;
1432
+ this.#presetScopeIndex = (this.#presetScopeIndex + 1) % this.#getPreviewPresetScopeLabels().length;
1239
1433
  } else {
1240
1434
  this.#presetCursor = (this.#presetCursor + 1) % rows.length;
1241
1435
  this.#previewProfileName = undefined;
@@ -1290,6 +1484,22 @@ export class ModelSelectorComponent extends Container {
1290
1484
 
1291
1485
  #handlePresetEnter(): void {
1292
1486
  if (this.#presetScopeMenuOpen && this.#previewProfileName) {
1487
+ const profile = this.#getProfileByName(this.#previewProfileName);
1488
+ if (!profile) return;
1489
+ if (this.#presetScopeIndex === 2 && isCustomUserProfile(profile)) {
1490
+ this.#onSelectCallback({ kind: "renameProfile", profileName: this.#previewProfileName });
1491
+ return;
1492
+ }
1493
+ if (this.#presetScopeIndex === 3 && isCustomUserProfile(profile)) {
1494
+ this.#onSelectCallback({ kind: "deleteProfile", profileName: this.#previewProfileName });
1495
+ return;
1496
+ }
1497
+ const missing = this.#getMissingProviders(profile);
1498
+ if (missing.length > 0) {
1499
+ this.#presetLoginHint = `Run ${missing.map(provider => `/login ${provider}`).join(", ")}`;
1500
+ this.#renderPresetLanding();
1501
+ return;
1502
+ }
1293
1503
  this.#onSelectCallback({
1294
1504
  kind: "profile",
1295
1505
  profileName: this.#previewProfileName,
@@ -1306,7 +1516,10 @@ export class ModelSelectorComponent extends Container {
1306
1516
  const row = this.#getSelectedPresetRow();
1307
1517
  if (!row) return;
1308
1518
  if (row.kind === "create") {
1309
- this.#onSelectCallback({ kind: "createProfile" });
1519
+ this.#onSelectCallback({ kind: "createProfile", profile: this.#buildCustomModelProfileSnapshot() });
1520
+ return;
1521
+ }
1522
+ if (row.kind === "alreadySaved" || row.kind === "createUnavailable") {
1310
1523
  return;
1311
1524
  }
1312
1525
  if (row.kind === "browse") {
@@ -1325,7 +1538,7 @@ export class ModelSelectorComponent extends Container {
1325
1538
  return;
1326
1539
  }
1327
1540
  const missing = this.#getMissingProviders(row.profile);
1328
- if (missing.length > 0) {
1541
+ if (missing.length > 0 && !isCustomUserProfile(row.profile)) {
1329
1542
  this.#presetLoginHint = `Run ${missing.map(provider => `/login ${provider}`).join(", ")}`;
1330
1543
  this.#renderPresetLanding();
1331
1544
  return;
@@ -1335,6 +1548,31 @@ export class ModelSelectorComponent extends Container {
1335
1548
  this.#renderPresetLanding();
1336
1549
  }
1337
1550
 
1551
+ #getPresetScopeLabels(profile: ModelProfileDefinition): string[] {
1552
+ return isCustomUserProfile(profile) ? CUSTOM_PRESET_SCOPE_LABELS : PRESET_SCOPE_LABELS;
1553
+ }
1554
+
1555
+ #getPreviewPresetScopeLabels(): string[] {
1556
+ const profile = this.#getProfileByName(this.#previewProfileName);
1557
+ return profile ? this.#getPresetScopeLabels(profile) : PRESET_SCOPE_LABELS;
1558
+ }
1559
+
1560
+ refreshPresetProfiles(profileName?: string): void {
1561
+ if (profileName) {
1562
+ this.#previewProfileName = profileName;
1563
+ this.#presetLoginHint = undefined;
1564
+ if (!this.#relocatePresetCursorForProfile(profileName)) this.#clampPresetCursor();
1565
+ } else {
1566
+ this.#previewProfileName = undefined;
1567
+ this.#presetScopeMenuOpen = false;
1568
+ this.#presetScopeIndex = 0;
1569
+ this.#presetLoginHint = undefined;
1570
+ this.#clampPresetCursor();
1571
+ }
1572
+ this.#renderPresetLanding();
1573
+ this.#tui.requestRender();
1574
+ }
1575
+
1338
1576
  #beginActionMenuOrSelect(item: ModelItem | CanonicalModelItem): void {
1339
1577
  if (this.#temporaryOnly) {
1340
1578
  this.#handleSelect(item, null);
@@ -1454,6 +1692,16 @@ export class ModelSelectorComponent extends Container {
1454
1692
  async __testSelectProfile(profileName: string, setDefault: boolean): Promise<void> {
1455
1693
  await this.#onSelectCallback({ kind: "profile", profileName, setDefault });
1456
1694
  }
1695
+ async __testSelectPresetAction(profileName: string, action: "rename" | "delete"): Promise<void> {
1696
+ await this.#onSelectCallback({
1697
+ kind: action === "rename" ? "renameProfile" : "deleteProfile",
1698
+ profileName,
1699
+ });
1700
+ }
1701
+ __testSelectedPresetRowIdentity(): string | undefined {
1702
+ const row = this.#getSelectedPresetRow();
1703
+ return row ? presetRowIdentity(row) : undefined;
1704
+ }
1457
1705
  }
1458
1706
 
1459
1707
  function requiresExplicitThinkingChoice(model: Model): boolean {
@@ -15,7 +15,7 @@
15
15
  * - Enter on main session -> close overlay (jump back)
16
16
  */
17
17
  import type { ToolResultMessage } from "@gajae-code/ai";
18
- import { Container, Markdown, type MarkdownTheme, matchesKey } from "@gajae-code/tui";
18
+ import { Container, Markdown, type MarkdownTheme, matchesKey, resolveTerminalColumns } from "@gajae-code/tui";
19
19
  import { formatDuration, formatNumber, logger } from "@gajae-code/utils";
20
20
  import type { KeyId } from "../../config/keybindings";
21
21
  import { isSilentAbort } from "../../session/messages";
@@ -39,7 +39,7 @@ const INDENT = " ";
39
39
 
40
40
  /** Compute the max content width for the current terminal, accounting for indent and chrome. */
41
41
  function contentWidth(indent = INDENT): number {
42
- return Math.max(TRUNCATE_LENGTHS.SHORT, (process.stdout.columns || 80) - indent.length - 2);
42
+ return Math.max(TRUNCATE_LENGTHS.SHORT, resolveTerminalColumns() - indent.length - 2);
43
43
  }
44
44
 
45
45
  /** Sanitize a line for TUI display: replace tabs, then truncate to viewport width. */
@@ -390,7 +390,7 @@ export class SessionObserverOverlayComponent extends Container {
390
390
 
391
391
  /** Render markdown text into indented lines using the theme's markdown renderer */
392
392
  #renderMarkdownToLines(text: string, indent: string = INDENT): string[] {
393
- const width = Math.max(40, (process.stdout.columns || 80) - indent.length - 4);
393
+ const width = Math.max(40, resolveTerminalColumns() - indent.length - 4);
394
394
  const md = new Markdown(text, 0, 0, this.#mdTheme);
395
395
  const rendered = md.render(width);
396
396
  return rendered.map(line => `${indent}${line.trimEnd()}`);