@gajae-code/coding-agent 0.7.8 → 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 (60) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +6 -0
  3. package/dist/types/config/keybindings.d.ts +1 -1
  4. package/dist/types/config/model-profile-activation.d.ts +19 -2
  5. package/dist/types/config/model-profiles.d.ts +1 -0
  6. package/dist/types/config/model-registry.d.ts +4 -0
  7. package/dist/types/config/settings-schema.d.ts +2 -2
  8. package/dist/types/gjc-runtime/launch-tmux.d.ts +12 -3
  9. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +3 -1
  10. package/dist/types/modes/components/hook-input.d.ts +1 -0
  11. package/dist/types/modes/components/model-selector.d.ts +9 -0
  12. package/dist/types/modes/components/status-line.d.ts +3 -0
  13. package/dist/types/modes/controllers/extension-ui-controller.d.ts +3 -1
  14. package/dist/types/modes/interactive-mode.d.ts +3 -1
  15. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +21 -1
  16. package/dist/types/modes/types.d.ts +3 -1
  17. package/dist/types/notifications/daemon-paths.d.ts +9 -0
  18. package/dist/types/notifications/telegram-daemon-cli.d.ts +8 -2
  19. package/dist/types/notifications/telegram-daemon.d.ts +1 -10
  20. package/dist/types/tools/ask.d.ts +24 -0
  21. package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -2
  22. package/package.json +7 -7
  23. package/src/cli/notify-cli.ts +1 -1
  24. package/src/cli.ts +1 -0
  25. package/src/config/keybindings.ts +1 -1
  26. package/src/config/model-profile-activation.ts +92 -11
  27. package/src/config/model-profiles.ts +15 -2
  28. package/src/config/model-registry.ts +81 -5
  29. package/src/config/settings-schema.ts +3 -2
  30. package/src/coordinator-mcp/server.ts +4 -2
  31. package/src/deep-interview/render-middleware.ts +1 -0
  32. package/src/defaults/gjc/skills/deep-interview/SKILL.md +14 -6
  33. package/src/defaults/gjc/skills/ralplan/SKILL.md +14 -7
  34. package/src/gjc-runtime/deep-interview-runtime.ts +1 -1
  35. package/src/gjc-runtime/launch-tmux.ts +303 -46
  36. package/src/gjc-runtime/team-runtime.ts +69 -4
  37. package/src/gjc-runtime/ultragoal-guard.ts +11 -5
  38. package/src/internal-urls/docs-index.generated.ts +6 -6
  39. package/src/modes/components/custom-model-preset-wizard.ts +2 -4
  40. package/src/modes/components/hook-editor.ts +7 -4
  41. package/src/modes/components/hook-input.ts +4 -0
  42. package/src/modes/components/model-selector.ts +84 -13
  43. package/src/modes/components/settings-selector.ts +36 -21
  44. package/src/modes/components/status-line/segments.ts +10 -2
  45. package/src/modes/components/status-line.ts +1 -0
  46. package/src/modes/controllers/extension-ui-controller.ts +2 -0
  47. package/src/modes/controllers/selector-controller.ts +130 -7
  48. package/src/modes/interactive-mode.ts +7 -2
  49. package/src/modes/shared/agent-wire/deep-interview-gate.ts +11 -4
  50. package/src/modes/types.ts +6 -1
  51. package/src/notifications/daemon-paths.ts +22 -0
  52. package/src/notifications/telegram-daemon-cli.ts +13 -4
  53. package/src/notifications/telegram-daemon.ts +2 -21
  54. package/src/prompts/system/system-prompt.md +5 -0
  55. package/src/sdk.ts +10 -1
  56. package/src/session/agent-session.ts +10 -1
  57. package/src/tools/ask.ts +11 -1
  58. package/src/tools/eval.ts +2 -2
  59. package/src/tools/render-utils.ts +2 -0
  60. package/src/tools/ultragoal-ask-guard.ts +39 -5
@@ -1,11 +1,9 @@
1
1
  import { Container, Input, matchesKey, Spacer, Text, TruncatedText } from "@gajae-code/tui";
2
+ import { MODEL_PROFILE_NAME_PATTERN } from "../../config/model-registry";
2
3
  import type { ModelProfileConfig } from "../../config/models-config-schema";
3
4
  import { theme } from "../theme/theme";
4
5
  import { matchesAppInterrupt } from "../utils/keybinding-matchers";
5
6
  import { DynamicBorder } from "./dynamic-border";
6
-
7
- const PROFILE_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
8
-
9
7
  export interface CustomModelPresetWizardSubmit {
10
8
  name: string;
11
9
  profile: ModelProfileConfig;
@@ -113,7 +111,7 @@ export class CustomModelPresetWizardComponent extends Container {
113
111
  this.#onRender();
114
112
  return;
115
113
  }
116
- if (!PROFILE_NAME_PATTERN.test(value)) {
114
+ if (!MODEL_PROFILE_NAME_PATTERN.test(value)) {
117
115
  this.#lastError = "Preset id must use lowercase letters, numbers, dots, underscores, or hyphens.";
118
116
  this.#renderStep();
119
117
  this.#onRender();
@@ -96,14 +96,17 @@ export class HookEditorComponent extends Container {
96
96
  return;
97
97
  }
98
98
 
99
- // Submit on plain Enter encodings. On Windows, raw LF is reserved for terminal
100
- // newline mappings (Shift+Enter/Ctrl+J/Ctrl+Enter); plain Enter reports CR.
101
- if (!isWindowsRawLfNewlineInput(keyData) && (matchesKey(keyData, "enter") || matchesKey(keyData, "return"))) {
99
+ // Submit on Enter and Ctrl+Enter encodings. On Windows, raw LF is reserved for
100
+ // terminal newline mappings (Shift+Enter/Ctrl+J); plain Enter reports CR.
101
+ if (
102
+ !isWindowsRawLfNewlineInput(keyData) &&
103
+ (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || matchesKey(keyData, "ctrl+enter"))
104
+ ) {
102
105
  this.#onSubmitCallback(this.#editor.getText());
103
106
  return;
104
107
  }
105
108
 
106
- // Let Editor handle modified newline-producing variants (Shift+Enter, Ctrl+Enter, Alt+Enter, etc.)
109
+ // Let Editor handle newline-producing variants (Shift+Enter, Ctrl+J, Alt+Enter, etc.)
107
110
  this.#editor.handleInput(keyData);
108
111
  }
109
112
 
@@ -8,6 +8,7 @@ import { CountdownTimer } from "./countdown-timer";
8
8
  import { DynamicBorder } from "./dynamic-border";
9
9
 
10
10
  export interface HookInputOptions {
11
+ initialValue?: string;
11
12
  tui?: TUI;
12
13
  timeout?: number;
13
14
  onTimeout?: () => void;
@@ -54,6 +55,9 @@ export class HookInputComponent extends Container {
54
55
  }
55
56
 
56
57
  this.#input = new Input();
58
+ if (opts?.initialValue) {
59
+ this.#input.setValue(opts.initialValue);
60
+ }
57
61
  this.addChild(this.#input);
58
62
  this.addChild(new Spacer(1));
59
63
  this.addChild(new Text(theme.fg("dim", "enter submit esc cancel"), 1, 0));
@@ -107,6 +107,14 @@ export type ModelSelectorSelection =
107
107
  | {
108
108
  kind: "createProfile";
109
109
  profile: ModelProfileConfig;
110
+ }
111
+ | {
112
+ kind: "renameProfile";
113
+ profileName: string;
114
+ }
115
+ | {
116
+ kind: "deleteProfile";
117
+ profileName: string;
110
118
  };
111
119
 
112
120
  interface PendingThinkingChoice {
@@ -164,7 +172,7 @@ interface PresetCreateUnavailableRow {
164
172
 
165
173
  interface PresetAlreadySavedRow {
166
174
  kind: "alreadySaved";
167
- profileName: string;
175
+ profile: ModelProfileDefinition;
168
176
  }
169
177
 
170
178
  interface PresetBrowseRow {
@@ -195,7 +203,7 @@ function presetRowIdentity(row: PresetLandingRow): string {
195
203
  case "createUnavailable":
196
204
  return "createUnavailable";
197
205
  case "alreadySaved":
198
- return `alreadySaved:${row.profileName}`;
206
+ return `alreadySaved:${row.profile.name}`;
199
207
  }
200
208
  }
201
209
 
@@ -207,6 +215,7 @@ const PROFILE_ROLE_PREVIEW_ORDER: GjcModelAssignmentTargetId[] = [
207
215
  "architect",
208
216
  ];
209
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"];
210
219
 
211
220
  function isPrintableCharacter(keyData: string): boolean {
212
221
  return keyData.length === 1 && keyData >= " " && keyData !== "\x7f";
@@ -273,6 +282,10 @@ function sameStringArray(left: readonly string[], right: readonly string[]): boo
273
282
  function hasPersistableProfileSnapshot(snapshot: ModelProfileConfig): boolean {
274
283
  return Object.keys(snapshot.model_mapping).length > 0 && snapshot.required_providers.length > 0;
275
284
  }
285
+
286
+ function isCustomUserProfile(profile: ModelProfileDefinition): boolean {
287
+ return profile.source === "user";
288
+ }
276
289
  /**
277
290
  * Component that renders a canonical model selector with provider tabs.
278
291
  * - Preset landing Left/Right: Collapse/expand selected provider
@@ -885,9 +898,7 @@ export class ModelSelectorComponent extends Container {
885
898
  const snapshot = this.#buildCustomModelProfileSnapshot();
886
899
  if (hasPersistableProfileSnapshot(snapshot)) {
887
900
  const duplicateProfile = this.#findDuplicateGeneratedProfile(snapshot);
888
- rows.push(
889
- duplicateProfile ? { kind: "alreadySaved", profileName: duplicateProfile.name } : { kind: "create" },
890
- );
901
+ rows.push(duplicateProfile ? { kind: "alreadySaved", profile: duplicateProfile } : { kind: "create" });
891
902
  } else {
892
903
  rows.push({ kind: "createUnavailable", label: "Select a model before creating a custom preset" });
893
904
  }
@@ -961,7 +972,14 @@ export class ModelSelectorComponent extends Container {
961
972
  this.#presetCursor = relocated;
962
973
  return true;
963
974
  }
964
-
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
+ }
965
983
  #expandSelectedPresetProvider(): void {
966
984
  const selected = this.#getSelectedPresetRow();
967
985
  if (
@@ -1029,7 +1047,8 @@ export class ModelSelectorComponent extends Container {
1029
1047
  continue;
1030
1048
  }
1031
1049
  if (row.kind === "alreadySaved") {
1032
- const label = `Already saved as ${row.profileName}`;
1050
+ const presentation = getModelProfilePresentation(row.profile);
1051
+ const label = `Already saved as ${presentation.displayName}`;
1033
1052
  const renderedLabel = selected ? theme.fg("accent", label) : theme.fg("dim", label);
1034
1053
  this.#listContainer.addChild(new Text(`${prefix}${renderedLabel}`, 0, 0));
1035
1054
  continue;
@@ -1082,8 +1101,9 @@ export class ModelSelectorComponent extends Container {
1082
1101
  }
1083
1102
  this.#listContainer.addChild(new Spacer(1));
1084
1103
  if (this.#presetScopeMenuOpen) {
1085
- for (let i = 0; i < PRESET_SCOPE_LABELS.length; i++) {
1086
- 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] ?? "";
1087
1107
  const prefix = i === this.#presetScopeIndex ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
1088
1108
  this.#listContainer.addChild(
1089
1109
  new Text(`${prefix}${i === this.#presetScopeIndex ? theme.fg("accent", label) : label}`, 0, 0),
@@ -1394,8 +1414,8 @@ export class ModelSelectorComponent extends Container {
1394
1414
  const rows = this.#getPresetRows();
1395
1415
  if (rows.length === 0) return;
1396
1416
  if (this.#presetScopeMenuOpen) {
1397
- this.#presetScopeIndex =
1398
- 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;
1399
1419
  } else {
1400
1420
  this.#presetCursor = this.#presetCursor === 0 ? rows.length - 1 : this.#presetCursor - 1;
1401
1421
  this.#previewProfileName = undefined;
@@ -1409,7 +1429,7 @@ export class ModelSelectorComponent extends Container {
1409
1429
  const rows = this.#getPresetRows();
1410
1430
  if (rows.length === 0) return;
1411
1431
  if (this.#presetScopeMenuOpen) {
1412
- this.#presetScopeIndex = (this.#presetScopeIndex + 1) % PRESET_SCOPE_LABELS.length;
1432
+ this.#presetScopeIndex = (this.#presetScopeIndex + 1) % this.#getPreviewPresetScopeLabels().length;
1413
1433
  } else {
1414
1434
  this.#presetCursor = (this.#presetCursor + 1) % rows.length;
1415
1435
  this.#previewProfileName = undefined;
@@ -1464,6 +1484,22 @@ export class ModelSelectorComponent extends Container {
1464
1484
 
1465
1485
  #handlePresetEnter(): void {
1466
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
+ }
1467
1503
  this.#onSelectCallback({
1468
1504
  kind: "profile",
1469
1505
  profileName: this.#previewProfileName,
@@ -1502,7 +1538,7 @@ export class ModelSelectorComponent extends Container {
1502
1538
  return;
1503
1539
  }
1504
1540
  const missing = this.#getMissingProviders(row.profile);
1505
- if (missing.length > 0) {
1541
+ if (missing.length > 0 && !isCustomUserProfile(row.profile)) {
1506
1542
  this.#presetLoginHint = `Run ${missing.map(provider => `/login ${provider}`).join(", ")}`;
1507
1543
  this.#renderPresetLanding();
1508
1544
  return;
@@ -1512,6 +1548,31 @@ export class ModelSelectorComponent extends Container {
1512
1548
  this.#renderPresetLanding();
1513
1549
  }
1514
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
+
1515
1576
  #beginActionMenuOrSelect(item: ModelItem | CanonicalModelItem): void {
1516
1577
  if (this.#temporaryOnly) {
1517
1578
  this.#handleSelect(item, null);
@@ -1631,6 +1692,16 @@ export class ModelSelectorComponent extends Container {
1631
1692
  async __testSelectProfile(profileName: string, setDefault: boolean): Promise<void> {
1632
1693
  await this.#onSelectCallback({ kind: "profile", profileName, setDefault });
1633
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
+ }
1634
1705
  }
1635
1706
 
1636
1707
  function requiresExplicitThinkingChoice(model: Model): boolean {
@@ -179,6 +179,10 @@ const TIME_FORMAT_OPTIONS: SelectItem[] = [
179
179
  { value: "24h", label: "24h" },
180
180
  { value: "12h", label: "12h" },
181
181
  ];
182
+ const USAGE_MODE_OPTIONS: SelectItem[] = [
183
+ { value: "used", label: "Used" },
184
+ { value: "remaining", label: "Remaining" },
185
+ ];
182
186
 
183
187
  function cloneSegmentOptions(options: StatusLineSegmentOptions | undefined): StatusLineSegmentOptions {
184
188
  return mergeSegmentOptions(undefined, options);
@@ -193,6 +197,7 @@ function mergeSegmentOptions(
193
197
  path: base?.path || overrides?.path ? { ...(base?.path ?? {}), ...(overrides?.path ?? {}) } : undefined,
194
198
  git: base?.git || overrides?.git ? { ...(base?.git ?? {}), ...(overrides?.git ?? {}) } : undefined,
195
199
  time: base?.time || overrides?.time ? { ...(base?.time ?? {}), ...(overrides?.time ?? {}) } : undefined,
200
+ usage: base?.usage || overrides?.usage ? { ...(base?.usage ?? {}), ...(overrides?.usage ?? {}) } : undefined,
196
201
  };
197
202
  }
198
203
 
@@ -235,8 +240,6 @@ function segmentPlacement(
235
240
  class StatusLineCustomEditor extends Container {
236
241
  #list!: SettingsList;
237
242
  #draft: StatusLineDraft;
238
- #currentWidthPreviewText!: Text;
239
- #narrowWidthPreviewText!: Text;
240
243
  #previewHighlightSegment: StatusLineSegmentId | undefined;
241
244
 
242
245
  constructor(
@@ -268,13 +271,6 @@ class StatusLineCustomEditor extends Container {
268
271
  this.clear();
269
272
  this.addChild(new Text(theme.bold(theme.fg("accent", "Status Line Custom Editor")), 0, 0));
270
273
  this.addChild(new Spacer(1));
271
- this.addChild(new Text(theme.fg("muted", "Current width preview:"), 0, 0));
272
- this.#currentWidthPreviewText = new Text(this.#statusLinePreview(), 0, 0);
273
- this.addChild(this.#currentWidthPreviewText);
274
- this.addChild(new Text(theme.fg("muted", "Narrow width preview:"), 0, 0));
275
- this.#narrowWidthPreviewText = new Text(this.#statusLinePreview(40), 0, 0);
276
- this.addChild(this.#narrowWidthPreviewText);
277
- this.addChild(new Spacer(1));
278
274
  this.#list = new SettingsList(
279
275
  this.#items(),
280
276
  14,
@@ -282,27 +278,17 @@ class StatusLineCustomEditor extends Container {
282
278
  (id, value) => this.#handleChange(id, value),
283
279
  () => this.#cancel(),
284
280
  item => this.#setSelectedItem(item),
281
+ 2,
285
282
  );
286
283
  this.addChild(this.#list);
287
284
  }
288
285
 
289
286
  #refresh(): void {
290
- this.#refreshPreview();
291
287
  this.#list.setItems(this.#items());
292
288
  }
293
-
294
- #refreshPreview(): void {
295
- this.#currentWidthPreviewText.setText(this.#statusLinePreview());
296
- this.#narrowWidthPreviewText.setText(this.#statusLinePreview(40));
297
- }
298
-
299
- #statusLinePreview(width?: number): string {
300
- return this.callbacks.getStatusLinePreview?.(width) ?? theme.fg("dim", "(preview not available)");
301
- }
302
289
  #setSelectedItem(item: SettingItem | undefined): void {
303
290
  this.#previewHighlightSegment = this.#highlightSegmentForItem(item);
304
291
  this.#preview();
305
- this.#refreshPreview();
306
292
  }
307
293
 
308
294
  #highlightSegmentForItem(item: SettingItem | undefined): StatusLineSegmentId | undefined {
@@ -388,6 +374,22 @@ class StatusLineCustomEditor extends Container {
388
374
  },
389
375
  );
390
376
  }
377
+ if (id === "usage") {
378
+ items.push({
379
+ id: "option.usage.mode",
380
+ label: "Usage: mode",
381
+ currentValue: this.#draft.segmentOptions.usage?.mode ?? "used",
382
+ submenu: (currentValue, done) =>
383
+ new SelectSubmenu(
384
+ "Usage mode",
385
+ "Show used quota or remaining quota in the usage segment.",
386
+ USAGE_MODE_OPTIONS,
387
+ currentValue,
388
+ done,
389
+ () => done(),
390
+ ),
391
+ });
392
+ }
391
393
  }
392
394
 
393
395
  items.push(
@@ -552,6 +554,12 @@ class StatusLineCustomEditor extends Container {
552
554
  case "time.showSeconds":
553
555
  this.#draft.segmentOptions.time = { ...(this.#draft.segmentOptions.time ?? {}), showSeconds: bool };
554
556
  break;
557
+ case "usage.mode":
558
+ this.#draft.segmentOptions.usage = {
559
+ ...(this.#draft.segmentOptions.usage ?? {}),
560
+ mode: value === "remaining" ? "remaining" : "used",
561
+ };
562
+ break;
555
563
  }
556
564
  }
557
565
 
@@ -1019,13 +1027,20 @@ export class SettingsSelectorComponent extends Container {
1019
1027
  #buildItemsForTab(defs: SettingDef[], tabId: SettingTab): SettingItem[] {
1020
1028
  const items = this.#buildItemsForDefs(defs);
1021
1029
  if (tabId === "appearance") {
1030
+ const customEditorCallbacks: SettingsCallbacks = {
1031
+ ...this.callbacks,
1032
+ onStatusLinePreview: previewSettings => {
1033
+ this.callbacks.onStatusLinePreview?.(previewSettings);
1034
+ this.#updateStatusPreview();
1035
+ },
1036
+ };
1022
1037
  const customEditorItem: SettingItem = {
1023
1038
  id: STATUS_LINE_CUSTOM_EDITOR_ID,
1024
1039
  label: "Status Line Custom Editor",
1025
1040
  description:
1026
1041
  "Edit custom status line segments, placement, separator, and typed segment options with live previews.",
1027
1042
  currentValue: "open",
1028
- submenu: (_currentValue, done) => new StatusLineCustomEditor(this.callbacks, done),
1043
+ submenu: (_currentValue, done) => new StatusLineCustomEditor(customEditorCallbacks, done),
1029
1044
  };
1030
1045
  const presetIndex = items.findIndex(item => item.id === "statusLine.preset");
1031
1046
  if (presetIndex >= 0) {
@@ -480,7 +480,12 @@ const sessionNameSegment: StatusLineSegment = {
480
480
  },
481
481
  };
482
482
 
483
- function pickUsageColor(percent: number): "muted" | "warning" | "error" {
483
+ function pickUsageColor(percent: number, mode: "used" | "remaining"): "muted" | "warning" | "error" {
484
+ if (mode === "remaining") {
485
+ if (percent <= 20) return "error";
486
+ if (percent <= 50) return "warning";
487
+ return "muted";
488
+ }
484
489
  if (percent >= 80) return "error";
485
490
  if (percent >= 50) return "warning";
486
491
  return "muted";
@@ -508,8 +513,11 @@ const usageSegment: StatusLineSegment = {
508
513
  if (!u || u.windows.length === 0) {
509
514
  return { content: "", visible: false };
510
515
  }
516
+ const mode = ctx.options.usage?.mode === "remaining" ? "remaining" : "used";
511
517
  const parts = u.windows.map(window => {
512
- const pctText = theme.fg(pickUsageColor(window.percent), `${Math.round(window.percent)}%`);
518
+ const displayPercent =
519
+ mode === "remaining" ? Math.max(0, Math.min(100, 100 - window.percent)) : window.percent;
520
+ const pctText = theme.fg(pickUsageColor(displayPercent, mode), `${Math.round(displayPercent)}%`);
513
521
  const reset =
514
522
  window.resetValue !== undefined && window.resetUnit !== undefined
515
523
  ? theme.fg("muted", ` (${formatUsageReset(window.resetValue, window.resetUnit)})`)
@@ -32,6 +32,7 @@ export interface StatusLineSegmentOptions {
32
32
  path?: { abbreviate?: boolean; maxLength?: number; stripWorkPrefix?: boolean };
33
33
  git?: { showBranch?: boolean; showStaged?: boolean; showUnstaged?: boolean; showUntracked?: boolean };
34
34
  time?: { format?: "12h" | "24h"; showSeconds?: boolean };
35
+ usage?: { mode?: "used" | "remaining" };
35
36
  }
36
37
 
37
38
  export interface StatusLineSettings {
@@ -715,6 +715,7 @@ export class ExtensionUiController {
715
715
  title: string,
716
716
  placeholder?: string,
717
717
  dialogOptions?: ExtensionUIDialogOptions,
718
+ inputOptions?: { readonly initialValue?: string },
718
719
  ): Promise<string | undefined> {
719
720
  const { promise, finish, attachAbort } = this.#createHookDialogState(
720
721
  () => this.hideHookInput(),
@@ -732,6 +733,7 @@ export class ExtensionUiController {
732
733
  finish(undefined);
733
734
  },
734
735
  {
736
+ initialValue: inputOptions?.initialValue,
735
737
  timeout: dialogOptions?.timeout,
736
738
  onTimeout: dialogOptions?.onTimeout,
737
739
  tui: this.ctx.ui,
@@ -4,8 +4,14 @@ import type { OAuthProvider } from "@gajae-code/ai/utils/oauth/types";
4
4
  import type { Component, OverlayHandle } from "@gajae-code/tui";
5
5
  import { Input, Loader, Spacer, Text } from "@gajae-code/tui";
6
6
  import { getAgentDbPath, getProjectDir } from "@gajae-code/utils";
7
- import { activateModelProfile, materializeActiveModelProfileAssignment } from "../../config/model-profile-activation";
8
- import { recommendModelProfileForProvider } from "../../config/model-profiles";
7
+ import {
8
+ activateModelProfile,
9
+ type MaterializeModelProfileForDeletionResult,
10
+ materializeActiveModelProfileAssignment,
11
+ materializeModelProfileForDeletion,
12
+ restoreMaterializedModelProfileForDeletion,
13
+ } from "../../config/model-profile-activation";
14
+ import { formatModelProfileDisplayLabel, recommendModelProfileForProvider } from "../../config/model-profiles";
9
15
  import { GJC_MODEL_ASSIGNMENT_TARGETS } from "../../config/model-registry";
10
16
  import { formatModelSelectorValue } from "../../config/model-resolver";
11
17
  import type { ModelProfileConfig } from "../../config/models-config-schema";
@@ -39,7 +45,11 @@ import {
39
45
  CREDENTIAL_AUTO_IMPORT_ROTATION_WARNING,
40
46
  runExternalCredentialAutoImport,
41
47
  } from "../../setup/credential-auto-import";
42
- import { filterAutoImportOAuthCredentials, formatDiscoverySummary } from "../../setup/credential-import";
48
+ import {
49
+ filterAutoImportOAuthCredentials,
50
+ formatDiscoverySummary,
51
+ type ImportableCredential,
52
+ } from "../../setup/credential-import";
43
53
  import {
44
54
  MODEL_ONBOARDING_API_PROVIDER_COMMAND,
45
55
  MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND,
@@ -248,7 +258,7 @@ export class SelectorController {
248
258
  const profile = await this.ctx.session.modelRegistry.saveCustomModelProfile(input.name, input.profile);
249
259
  await this.ctx.session.modelRegistry.refresh("offline");
250
260
  await this.ctx.notifyConfigChanged?.();
251
- this.ctx.showStatus(`Custom model preset created: ${profile.displayName ?? profile.name}`);
261
+ this.ctx.showStatus(`Custom model preset created: ${formatModelProfileDisplayLabel(profile)}`);
252
262
  done();
253
263
  this.ctx.ui.requestRender();
254
264
  } catch (err) {
@@ -271,6 +281,106 @@ export class SelectorController {
271
281
  });
272
282
  }
273
283
 
284
+ async #renameCustomModelPreset(profileName: string, modelSelector: ModelSelectorComponent): Promise<void> {
285
+ const profile = this.ctx.session.modelRegistry.getModelProfile(profileName);
286
+ const currentName = profile ? formatModelProfileDisplayLabel(profile) : profileName;
287
+ const input = await this.ctx.showHookInput(`Rename custom model preset: ${currentName}`, undefined, undefined, {
288
+ initialValue: currentName,
289
+ });
290
+ if (input === undefined) {
291
+ this.ctx.showStatus("Preset rename cancelled.");
292
+ this.ctx.ui.requestRender();
293
+ return;
294
+ }
295
+ try {
296
+ const renamed = await this.ctx.session.modelRegistry.renameCustomModelProfile(profileName, input);
297
+ await this.ctx.session.modelRegistry.refresh("offline");
298
+ await this.ctx.notifyConfigChanged?.();
299
+ modelSelector.refreshPresetProfiles(renamed.name);
300
+ this.ctx.showStatus(`Custom model preset renamed: ${formatModelProfileDisplayLabel(renamed)}`);
301
+ this.ctx.ui.requestRender();
302
+ } catch (err) {
303
+ this.ctx.showError(`Preset rename failed: ${err instanceof Error ? err.message : String(err)}`);
304
+ }
305
+ }
306
+
307
+ async #deleteCustomModelPreset(profileName: string, modelSelector: ModelSelectorComponent): Promise<void> {
308
+ const profile = this.ctx.session.modelRegistry.getModelProfile(profileName);
309
+ const profileLabel = profile ? formatModelProfileDisplayLabel(profile) : profileName;
310
+ const confirmed = await this.ctx.showHookConfirm(
311
+ `Delete custom model preset: ${profileLabel}`,
312
+ "This removes the preset entry after preserving current role model settings when this preset is active/default.",
313
+ );
314
+ if (!confirmed) {
315
+ this.ctx.showStatus("Preset delete cancelled.");
316
+ this.ctx.ui.requestRender();
317
+ return;
318
+ }
319
+
320
+ const activeProfile = this.ctx.session.getActiveModelProfile?.();
321
+ const defaultProfile = this.ctx.settings.get("modelProfile.default");
322
+ let snapshot: MaterializeModelProfileForDeletionResult | undefined;
323
+ let deletedProfile: ModelProfileConfig | undefined;
324
+ const refreshSelectorState = (refreshedProfileName?: string): void => {
325
+ modelSelector.refreshRoleAssignments({
326
+ currentModel: this.ctx.session.model,
327
+ currentThinkingLevel: this.ctx.session.thinkingLevel,
328
+ activeModelProfile:
329
+ this.ctx.session.getActiveModelProfile?.() ?? this.ctx.settings.get("modelProfile.default"),
330
+ });
331
+ modelSelector.refreshPresetProfiles(refreshedProfileName);
332
+ };
333
+ try {
334
+ if (activeProfile === profileName || defaultProfile === profileName) {
335
+ snapshot = await materializeModelProfileForDeletion({
336
+ session: this.ctx.session,
337
+ modelRegistry: this.ctx.session.modelRegistry,
338
+ settings: this.ctx.settings,
339
+ profileName,
340
+ });
341
+ }
342
+ deletedProfile = await this.ctx.session.modelRegistry.deleteCustomModelProfile(profileName);
343
+ await this.ctx.session.modelRegistry.refresh("offline");
344
+ await this.ctx.notifyConfigChanged?.();
345
+ refreshSelectorState();
346
+ this.ctx.showStatus(`Custom model preset deleted: ${profileLabel}`);
347
+ this.ctx.ui.requestRender();
348
+ } catch (err) {
349
+ let presetRestoreError: unknown;
350
+ if (deletedProfile) {
351
+ try {
352
+ await this.ctx.session.modelRegistry.saveCustomModelProfile(profileName, deletedProfile);
353
+ await this.ctx.session.modelRegistry.refresh("offline");
354
+ } catch (restoreErr) {
355
+ presetRestoreError = restoreErr;
356
+ }
357
+ }
358
+ if (snapshot) {
359
+ try {
360
+ await restoreMaterializedModelProfileForDeletion({
361
+ settings: this.ctx.settings,
362
+ session: this.ctx.session,
363
+ snapshot,
364
+ });
365
+ } catch (restoreErr) {
366
+ refreshSelectorState(deletedProfile ? profileName : undefined);
367
+ this.ctx.showError(
368
+ `Preset delete failed and settings rollback failed: ${restoreErr instanceof Error ? restoreErr.message : String(restoreErr)}`,
369
+ );
370
+ return;
371
+ }
372
+ }
373
+ if (deletedProfile) refreshSelectorState(profileName);
374
+ if (presetRestoreError) {
375
+ this.ctx.showError(
376
+ `Preset delete failed and preset restore failed: ${presetRestoreError instanceof Error ? presetRestoreError.message : String(presetRestoreError)}`,
377
+ );
378
+ return;
379
+ }
380
+ this.ctx.showError(`Preset delete failed: ${err instanceof Error ? err.message : String(err)}`);
381
+ }
382
+ }
383
+
274
384
  showCustomProviderWizard(): void {
275
385
  this.showSelector(done => {
276
386
  let wizard: CustomProviderWizardComponent;
@@ -693,7 +803,20 @@ export class SelectorController {
693
803
  this.showCustomModelPresetWizard(selection.profile);
694
804
  return;
695
805
  }
806
+ if (selection.kind === "renameProfile") {
807
+ await this.#renameCustomModelPreset(selection.profileName, modelSelector);
808
+ return;
809
+ }
810
+ if (selection.kind === "deleteProfile") {
811
+ await this.#deleteCustomModelPreset(selection.profileName, modelSelector);
812
+ return;
813
+ }
696
814
  if (selection.kind === "profile") {
815
+ const profileLabel = formatModelProfileDisplayLabel(
816
+ this.ctx.session.modelRegistry.getModelProfile(selection.profileName) ?? {
817
+ name: selection.profileName,
818
+ },
819
+ );
697
820
  await activateModelProfile(
698
821
  {
699
822
  session: this.ctx.session,
@@ -707,8 +830,8 @@ export class SelectorController {
707
830
  this.ctx.updateEditorBorderColor();
708
831
  this.ctx.showStatus(
709
832
  selection.setDefault
710
- ? `Default model profile: ${selection.profileName}`
711
- : `Model profile: ${selection.profileName}`,
833
+ ? `Default model profile: ${profileLabel}`
834
+ : `Model profile: ${profileLabel}`,
712
835
  );
713
836
  done();
714
837
  this.ctx.ui.requestRender();
@@ -1338,7 +1461,7 @@ export class SelectorController {
1338
1461
  }
1339
1462
  }
1340
1463
 
1341
- let externalCredentialCandidates: ReturnType<typeof filterAutoImportOAuthCredentials> = [];
1464
+ let externalCredentialCandidates: ImportableCredential[] = [];
1342
1465
  if (
1343
1466
  mode === "login" &&
1344
1467
  providerId === undefined &&
@@ -2684,8 +2684,13 @@ export class InteractiveMode implements InteractiveModeContext {
2684
2684
  this.#extensionUiController.hideHookSelector();
2685
2685
  }
2686
2686
 
2687
- showHookInput(title: string, placeholder?: string): Promise<string | undefined> {
2688
- return this.#extensionUiController.showHookInput(title, placeholder);
2687
+ showHookInput(
2688
+ title: string,
2689
+ placeholder?: string,
2690
+ dialogOptions?: ExtensionUIDialogOptions,
2691
+ inputOptions?: { readonly initialValue?: string },
2692
+ ): Promise<string | undefined> {
2693
+ return this.#extensionUiController.showHookInput(title, placeholder, dialogOptions, inputOptions);
2689
2694
  }
2690
2695
 
2691
2696
  hideHookInput(): void {