@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.
- package/CHANGELOG.md +18 -0
- package/README.md +6 -0
- package/dist/types/config/keybindings.d.ts +1 -1
- package/dist/types/config/model-profile-activation.d.ts +19 -2
- package/dist/types/config/model-profiles.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +2 -2
- package/dist/types/gjc-runtime/launch-tmux.d.ts +12 -3
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +3 -1
- package/dist/types/modes/components/hook-input.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +9 -0
- package/dist/types/modes/components/status-line.d.ts +3 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +3 -1
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +21 -1
- package/dist/types/modes/types.d.ts +3 -1
- package/dist/types/notifications/daemon-paths.d.ts +9 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +8 -2
- package/dist/types/notifications/telegram-daemon.d.ts +1 -10
- package/dist/types/tools/ask.d.ts +24 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -2
- package/package.json +7 -7
- package/src/cli/notify-cli.ts +1 -1
- package/src/cli.ts +1 -0
- package/src/config/keybindings.ts +1 -1
- package/src/config/model-profile-activation.ts +92 -11
- package/src/config/model-profiles.ts +15 -2
- package/src/config/model-registry.ts +81 -5
- package/src/config/settings-schema.ts +3 -2
- package/src/coordinator-mcp/server.ts +4 -2
- package/src/deep-interview/render-middleware.ts +1 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +14 -6
- package/src/defaults/gjc/skills/ralplan/SKILL.md +14 -7
- package/src/gjc-runtime/deep-interview-runtime.ts +1 -1
- package/src/gjc-runtime/launch-tmux.ts +303 -46
- package/src/gjc-runtime/team-runtime.ts +69 -4
- package/src/gjc-runtime/ultragoal-guard.ts +11 -5
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/modes/components/custom-model-preset-wizard.ts +2 -4
- package/src/modes/components/hook-editor.ts +7 -4
- package/src/modes/components/hook-input.ts +4 -0
- package/src/modes/components/model-selector.ts +84 -13
- package/src/modes/components/settings-selector.ts +36 -21
- package/src/modes/components/status-line/segments.ts +10 -2
- package/src/modes/components/status-line.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +2 -0
- package/src/modes/controllers/selector-controller.ts +130 -7
- package/src/modes/interactive-mode.ts +7 -2
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +11 -4
- package/src/modes/types.ts +6 -1
- package/src/notifications/daemon-paths.ts +22 -0
- package/src/notifications/telegram-daemon-cli.ts +13 -4
- package/src/notifications/telegram-daemon.ts +2 -21
- package/src/prompts/system/system-prompt.md +5 -0
- package/src/sdk.ts +10 -1
- package/src/session/agent-session.ts +10 -1
- package/src/tools/ask.ts +11 -1
- package/src/tools/eval.ts +2 -2
- package/src/tools/render-utils.ts +2 -0
- 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 (!
|
|
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
|
|
100
|
-
// newline mappings (Shift+Enter/Ctrl+J
|
|
101
|
-
if (
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
1086
|
-
|
|
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.#
|
|
1398
|
-
|
|
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) %
|
|
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(
|
|
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
|
|
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 {
|
|
8
|
-
|
|
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 {
|
|
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
|
|
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: ${
|
|
711
|
-
: `Model profile: ${
|
|
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:
|
|
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(
|
|
2688
|
-
|
|
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 {
|