@gajae-code/coding-agent 0.7.8 → 0.7.10
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 +40 -0
- package/README.md +8 -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 +12 -3
- package/dist/types/extensibility/extensions/types.d.ts +11 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +19 -3
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -4
- package/dist/types/modes/components/hook-input.d.ts +1 -0
- package/dist/types/modes/components/hook-selector.d.ts +5 -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 +41 -7
- package/dist/types/modes/types.d.ts +3 -1
- package/dist/types/notifications/daemon-paths.d.ts +9 -0
- package/dist/types/notifications/html-format.d.ts +2 -0
- package/dist/types/notifications/recent-activity.d.ts +5 -1
- package/dist/types/notifications/telegram-daemon-cli.d.ts +8 -2
- package/dist/types/notifications/telegram-daemon-control.d.ts +8 -0
- package/dist/types/notifications/telegram-daemon.d.ts +1 -10
- package/dist/types/notifications/telegram-reference.d.ts +4 -0
- package/dist/types/notifications/threaded-render.d.ts +1 -0
- package/dist/types/tools/ask.d.ts +26 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -2
- package/dist/types/utils/cmux-workspace.d.ts +23 -0
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/provider.d.ts +5 -0
- package/dist/types/web/search/render.d.ts +1 -0
- package/package.json +7 -7
- package/src/cli/notify-cli.ts +1 -1
- package/src/cli/web-search-cli.ts +3 -2
- 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 +14 -3
- 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 +40 -8
- package/src/defaults/gjc/skills/ralplan/SKILL.md +14 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +37 -112
- package/src/extensibility/extensions/types.ts +11 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +1 -1
- package/src/gjc-runtime/launch-tmux.ts +366 -48
- package/src/gjc-runtime/team-runtime.ts +69 -4
- package/src/gjc-runtime/ultragoal-guard.ts +150 -24
- package/src/gjc-runtime/ultragoal-runtime.ts +16 -124
- package/src/gjc-runtime/workflow-manifest.generated.json +0 -8
- package/src/gjc-runtime/workflow-manifest.ts +0 -1
- package/src/hooks/skill-state.ts +33 -73
- package/src/internal-urls/docs-index.generated.ts +8 -7
- 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/hook-selector.ts +21 -3
- package/src/modes/components/model-selector.ts +84 -13
- package/src/modes/components/settings-selector.ts +83 -24
- package/src/modes/components/status-line/presets.ts +11 -0
- 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 +20 -2
- 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 +64 -13
- package/src/modes/types.ts +6 -1
- package/src/notifications/daemon-paths.ts +22 -0
- package/src/notifications/html-format.ts +75 -1
- package/src/notifications/index.ts +12 -1
- package/src/notifications/recent-activity.ts +32 -8
- package/src/notifications/telegram-daemon-cli.ts +13 -4
- package/src/notifications/telegram-daemon-control.ts +9 -0
- package/src/notifications/telegram-daemon.ts +69 -45
- package/src/notifications/telegram-reference.ts +36 -14
- package/src/notifications/threaded-render.ts +14 -6
- package/src/prompts/system/system-prompt.md +10 -1
- package/src/sdk.ts +13 -6
- package/src/session/agent-session.ts +10 -1
- package/src/tools/ask.ts +147 -17
- package/src/tools/eval.ts +2 -2
- package/src/tools/render-utils.ts +2 -0
- package/src/tools/ultragoal-ask-guard.ts +39 -5
- package/src/utils/cmux-workspace.ts +106 -0
- package/src/utils/title-generator.ts +2 -0
- package/src/web/search/index.ts +22 -10
- package/src/web/search/provider.ts +15 -0
- package/src/web/search/render.ts +3 -0
|
@@ -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));
|
|
@@ -58,6 +58,11 @@ export interface HookSelectorOptions {
|
|
|
58
58
|
optionLabel: string;
|
|
59
59
|
onSubmit: (text: string) => void;
|
|
60
60
|
};
|
|
61
|
+
clarificationInput?: {
|
|
62
|
+
optionLabel: string;
|
|
63
|
+
onSubmit: (text: string) => void;
|
|
64
|
+
allowEmpty?: boolean;
|
|
65
|
+
};
|
|
61
66
|
/**
|
|
62
67
|
* Autocomplete provider for the inline custom-input editor. When present,
|
|
63
68
|
* the "Other (type your own)" editor gains the same `@` file-link and `/`
|
|
@@ -338,6 +343,8 @@ export class HookSelectorComponent extends Container {
|
|
|
338
343
|
#outline: boolean;
|
|
339
344
|
#scrollTitleRows: number | undefined;
|
|
340
345
|
#customInput: { optionLabel: string; onSubmit: (text: string) => void } | undefined;
|
|
346
|
+
#clarificationInput: { optionLabel: string; onSubmit: (text: string) => void; allowEmpty?: boolean } | undefined;
|
|
347
|
+
#activeInput: { onSubmit: (text: string) => void; allowEmpty?: boolean } | undefined;
|
|
341
348
|
#inputArea: Container;
|
|
342
349
|
#inlineEditor: Editor | undefined;
|
|
343
350
|
#helpTextComponent: Text;
|
|
@@ -365,6 +372,7 @@ export class HookSelectorComponent extends Container {
|
|
|
365
372
|
this.#wrapFocused = opts?.wrapFocused === true;
|
|
366
373
|
this.#outline = opts?.outline === true;
|
|
367
374
|
this.#customInput = opts?.customInput;
|
|
375
|
+
this.#clarificationInput = opts?.clarificationInput;
|
|
368
376
|
this.#tui = opts?.tui;
|
|
369
377
|
this.#autocompleteProvider = opts?.autocompleteProvider;
|
|
370
378
|
|
|
@@ -500,7 +508,11 @@ export class HookSelectorComponent extends Container {
|
|
|
500
508
|
const selected = this.#options[this.#selectedIndex];
|
|
501
509
|
if (!selected) return;
|
|
502
510
|
if (this.#customInput && selected === this.#customInput.optionLabel) {
|
|
503
|
-
this.#enterInputMode();
|
|
511
|
+
this.#enterInputMode(this.#customInput);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (this.#clarificationInput && selected === this.#clarificationInput.optionLabel) {
|
|
515
|
+
this.#enterInputMode(this.#clarificationInput);
|
|
504
516
|
return;
|
|
505
517
|
}
|
|
506
518
|
this.#onSelectCallback(selected);
|
|
@@ -535,14 +547,19 @@ export class HookSelectorComponent extends Container {
|
|
|
535
547
|
return;
|
|
536
548
|
}
|
|
537
549
|
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return")) {
|
|
538
|
-
|
|
550
|
+
const text = editor.getExpandedText();
|
|
551
|
+
if (this.#activeInput?.allowEmpty === false && text.trim() === "") {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
this.#activeInput?.onSubmit(text);
|
|
539
555
|
return;
|
|
540
556
|
}
|
|
541
557
|
editor.handleInput(keyData);
|
|
542
558
|
}
|
|
543
559
|
|
|
544
|
-
#enterInputMode(): void {
|
|
560
|
+
#enterInputMode(input: { onSubmit: (text: string) => void; allowEmpty?: boolean }): void {
|
|
545
561
|
if (this.#inlineEditor) return;
|
|
562
|
+
this.#activeInput = input;
|
|
546
563
|
// Stop the auto-select countdown for good: the user is actively typing,
|
|
547
564
|
// matching the old behavior where the separate editor had no timeout.
|
|
548
565
|
if (this.#countdown) {
|
|
@@ -577,6 +594,7 @@ export class HookSelectorComponent extends Container {
|
|
|
577
594
|
#exitInputMode(): void {
|
|
578
595
|
if (!this.#inlineEditor) return;
|
|
579
596
|
this.#inlineEditor = undefined;
|
|
597
|
+
this.#activeInput = undefined;
|
|
580
598
|
this.#inputArea.clear();
|
|
581
599
|
this.#helpTextComponent.setText(theme.fg("dim", this.#baseHelpText));
|
|
582
600
|
this.invalidate();
|
|
@@ -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 {
|
|
@@ -164,6 +164,7 @@ class SelectSubmenu extends Container {
|
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
const STATUS_LINE_CUSTOM_EDITOR_ID = "statusLine.customEditor";
|
|
167
|
+
const STATUS_LINE_USAGE_MODE_ID = "statusLine.usageMode";
|
|
167
168
|
const PUBLIC_STATUS_SEGMENTS = ALL_SEGMENT_IDS.filter(id => id !== "pi");
|
|
168
169
|
|
|
169
170
|
type StatusLineDraft = Required<
|
|
@@ -179,6 +180,12 @@ const TIME_FORMAT_OPTIONS: SelectItem[] = [
|
|
|
179
180
|
{ value: "24h", label: "24h" },
|
|
180
181
|
{ value: "12h", label: "12h" },
|
|
181
182
|
];
|
|
183
|
+
const USAGE_MODE_OPTIONS: SelectItem[] = [
|
|
184
|
+
{ value: "used", label: "Used" },
|
|
185
|
+
{ value: "remaining", label: "Remaining" },
|
|
186
|
+
];
|
|
187
|
+
const USAGE_MODE_VALUES = ["used", "remaining"] as const;
|
|
188
|
+
type UsageMode = (typeof USAGE_MODE_VALUES)[number];
|
|
182
189
|
|
|
183
190
|
function cloneSegmentOptions(options: StatusLineSegmentOptions | undefined): StatusLineSegmentOptions {
|
|
184
191
|
return mergeSegmentOptions(undefined, options);
|
|
@@ -193,6 +200,7 @@ function mergeSegmentOptions(
|
|
|
193
200
|
path: base?.path || overrides?.path ? { ...(base?.path ?? {}), ...(overrides?.path ?? {}) } : undefined,
|
|
194
201
|
git: base?.git || overrides?.git ? { ...(base?.git ?? {}), ...(overrides?.git ?? {}) } : undefined,
|
|
195
202
|
time: base?.time || overrides?.time ? { ...(base?.time ?? {}), ...(overrides?.time ?? {}) } : undefined,
|
|
203
|
+
usage: base?.usage || overrides?.usage ? { ...(base?.usage ?? {}), ...(overrides?.usage ?? {}) } : undefined,
|
|
196
204
|
};
|
|
197
205
|
}
|
|
198
206
|
|
|
@@ -218,6 +226,25 @@ function effectiveCustomSegments(
|
|
|
218
226
|
};
|
|
219
227
|
}
|
|
220
228
|
|
|
229
|
+
function getSavedUsageMode(): UsageMode {
|
|
230
|
+
const segmentOptions = settings.get("statusLine.segmentOptions") as StatusLineSegmentOptions;
|
|
231
|
+
return segmentOptions.usage?.mode === "remaining" ? "remaining" : "used";
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function setSavedUsageMode(mode: string): StatusLineSegmentOptions {
|
|
235
|
+
const normalizedMode: UsageMode = mode === "remaining" ? "remaining" : "used";
|
|
236
|
+
const segmentOptions = settings.get("statusLine.segmentOptions") as StatusLineSegmentOptions;
|
|
237
|
+
const nextOptions: StatusLineSegmentOptions = {
|
|
238
|
+
...segmentOptions,
|
|
239
|
+
usage: {
|
|
240
|
+
...(segmentOptions.usage ?? {}),
|
|
241
|
+
mode: normalizedMode,
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
settings.set("statusLine.segmentOptions", nextOptions as Record<string, unknown>);
|
|
245
|
+
return nextOptions;
|
|
246
|
+
}
|
|
247
|
+
|
|
221
248
|
function statusSegmentLabel(id: StatusLineSegmentId): string {
|
|
222
249
|
return id.replace(/_/g, " ");
|
|
223
250
|
}
|
|
@@ -235,8 +262,6 @@ function segmentPlacement(
|
|
|
235
262
|
class StatusLineCustomEditor extends Container {
|
|
236
263
|
#list!: SettingsList;
|
|
237
264
|
#draft: StatusLineDraft;
|
|
238
|
-
#currentWidthPreviewText!: Text;
|
|
239
|
-
#narrowWidthPreviewText!: Text;
|
|
240
265
|
#previewHighlightSegment: StatusLineSegmentId | undefined;
|
|
241
266
|
|
|
242
267
|
constructor(
|
|
@@ -268,13 +293,6 @@ class StatusLineCustomEditor extends Container {
|
|
|
268
293
|
this.clear();
|
|
269
294
|
this.addChild(new Text(theme.bold(theme.fg("accent", "Status Line Custom Editor")), 0, 0));
|
|
270
295
|
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
296
|
this.#list = new SettingsList(
|
|
279
297
|
this.#items(),
|
|
280
298
|
14,
|
|
@@ -282,27 +300,17 @@ class StatusLineCustomEditor extends Container {
|
|
|
282
300
|
(id, value) => this.#handleChange(id, value),
|
|
283
301
|
() => this.#cancel(),
|
|
284
302
|
item => this.#setSelectedItem(item),
|
|
303
|
+
2,
|
|
285
304
|
);
|
|
286
305
|
this.addChild(this.#list);
|
|
287
306
|
}
|
|
288
307
|
|
|
289
308
|
#refresh(): void {
|
|
290
|
-
this.#refreshPreview();
|
|
291
309
|
this.#list.setItems(this.#items());
|
|
292
310
|
}
|
|
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
311
|
#setSelectedItem(item: SettingItem | undefined): void {
|
|
303
312
|
this.#previewHighlightSegment = this.#highlightSegmentForItem(item);
|
|
304
313
|
this.#preview();
|
|
305
|
-
this.#refreshPreview();
|
|
306
314
|
}
|
|
307
315
|
|
|
308
316
|
#highlightSegmentForItem(item: SettingItem | undefined): StatusLineSegmentId | undefined {
|
|
@@ -388,6 +396,22 @@ class StatusLineCustomEditor extends Container {
|
|
|
388
396
|
},
|
|
389
397
|
);
|
|
390
398
|
}
|
|
399
|
+
if (id === "usage") {
|
|
400
|
+
items.push({
|
|
401
|
+
id: "option.usage.mode",
|
|
402
|
+
label: "Usage: mode",
|
|
403
|
+
currentValue: this.#draft.segmentOptions.usage?.mode ?? "used",
|
|
404
|
+
submenu: (currentValue, done) =>
|
|
405
|
+
new SelectSubmenu(
|
|
406
|
+
"Usage mode",
|
|
407
|
+
"Show used quota or remaining quota in the usage segment.",
|
|
408
|
+
USAGE_MODE_OPTIONS,
|
|
409
|
+
currentValue,
|
|
410
|
+
done,
|
|
411
|
+
() => done(),
|
|
412
|
+
),
|
|
413
|
+
});
|
|
414
|
+
}
|
|
391
415
|
}
|
|
392
416
|
|
|
393
417
|
items.push(
|
|
@@ -552,6 +576,12 @@ class StatusLineCustomEditor extends Container {
|
|
|
552
576
|
case "time.showSeconds":
|
|
553
577
|
this.#draft.segmentOptions.time = { ...(this.#draft.segmentOptions.time ?? {}), showSeconds: bool };
|
|
554
578
|
break;
|
|
579
|
+
case "usage.mode":
|
|
580
|
+
this.#draft.segmentOptions.usage = {
|
|
581
|
+
...(this.#draft.segmentOptions.usage ?? {}),
|
|
582
|
+
mode: value === "remaining" ? "remaining" : "used",
|
|
583
|
+
};
|
|
584
|
+
break;
|
|
555
585
|
}
|
|
556
586
|
}
|
|
557
587
|
|
|
@@ -836,9 +866,7 @@ export class SettingsSelectorComponent extends Container {
|
|
|
836
866
|
};
|
|
837
867
|
} else if (def.path === "statusLine.preset") {
|
|
838
868
|
onPreview = value => {
|
|
839
|
-
const presetDef = getPreset(
|
|
840
|
-
value as "default" | "minimal" | "compact" | "full" | "nerd" | "ascii" | "custom",
|
|
841
|
-
);
|
|
869
|
+
const presetDef = getPreset(value as StatusLinePreset);
|
|
842
870
|
this.callbacks.onStatusLinePreview?.({
|
|
843
871
|
preset: value as StatusLinePreset,
|
|
844
872
|
leftSegments: presetDef.leftSegments,
|
|
@@ -976,6 +1004,16 @@ export class SettingsSelectorComponent extends Container {
|
|
|
976
1004
|
10,
|
|
977
1005
|
getSettingsListTheme(),
|
|
978
1006
|
(id, newValue) => {
|
|
1007
|
+
if (id === STATUS_LINE_USAGE_MODE_ID) {
|
|
1008
|
+
const segmentOptions = setSavedUsageMode(newValue);
|
|
1009
|
+
this.callbacks.onChange("statusLine.segmentOptions", segmentOptions);
|
|
1010
|
+
if (tabId === "appearance") {
|
|
1011
|
+
this.#triggerStatusLinePreview();
|
|
1012
|
+
}
|
|
1013
|
+
this.#refreshCurrentTabItems(defs);
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
979
1017
|
const def = defs.find(d => d.path === id);
|
|
980
1018
|
if (!def) return;
|
|
981
1019
|
|
|
@@ -1019,13 +1057,20 @@ export class SettingsSelectorComponent extends Container {
|
|
|
1019
1057
|
#buildItemsForTab(defs: SettingDef[], tabId: SettingTab): SettingItem[] {
|
|
1020
1058
|
const items = this.#buildItemsForDefs(defs);
|
|
1021
1059
|
if (tabId === "appearance") {
|
|
1060
|
+
const customEditorCallbacks: SettingsCallbacks = {
|
|
1061
|
+
...this.callbacks,
|
|
1062
|
+
onStatusLinePreview: previewSettings => {
|
|
1063
|
+
this.callbacks.onStatusLinePreview?.(previewSettings);
|
|
1064
|
+
this.#updateStatusPreview();
|
|
1065
|
+
},
|
|
1066
|
+
};
|
|
1022
1067
|
const customEditorItem: SettingItem = {
|
|
1023
1068
|
id: STATUS_LINE_CUSTOM_EDITOR_ID,
|
|
1024
1069
|
label: "Status Line Custom Editor",
|
|
1025
1070
|
description:
|
|
1026
1071
|
"Edit custom status line segments, placement, separator, and typed segment options with live previews.",
|
|
1027
1072
|
currentValue: "open",
|
|
1028
|
-
submenu: (_currentValue, done) => new StatusLineCustomEditor(
|
|
1073
|
+
submenu: (_currentValue, done) => new StatusLineCustomEditor(customEditorCallbacks, done),
|
|
1029
1074
|
};
|
|
1030
1075
|
const presetIndex = items.findIndex(item => item.id === "statusLine.preset");
|
|
1031
1076
|
if (presetIndex >= 0) {
|
|
@@ -1033,6 +1078,20 @@ export class SettingsSelectorComponent extends Container {
|
|
|
1033
1078
|
} else {
|
|
1034
1079
|
items.push(customEditorItem);
|
|
1035
1080
|
}
|
|
1081
|
+
{
|
|
1082
|
+
const usageModeItem: SettingItem = {
|
|
1083
|
+
id: STATUS_LINE_USAGE_MODE_ID,
|
|
1084
|
+
label: "Status Line Usage Mode",
|
|
1085
|
+
description: "Show provider quota in the status line as used or remaining.",
|
|
1086
|
+
currentValue: getSavedUsageMode(),
|
|
1087
|
+
values: [...USAGE_MODE_VALUES],
|
|
1088
|
+
};
|
|
1089
|
+
if (presetIndex >= 0) {
|
|
1090
|
+
items.splice(presetIndex + 2, 0, usageModeItem);
|
|
1091
|
+
} else {
|
|
1092
|
+
items.push(usageModeItem);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1036
1095
|
}
|
|
1037
1096
|
return items;
|
|
1038
1097
|
}
|
|
@@ -12,6 +12,17 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
12
12
|
},
|
|
13
13
|
},
|
|
14
14
|
|
|
15
|
+
"default-usage": {
|
|
16
|
+
leftSegments: ["model", "mode", "git", "pr", "path"],
|
|
17
|
+
rightSegments: ["session_name", "jobs", "token_rate", "usage", "context_pct", "cost"],
|
|
18
|
+
separator: "slash",
|
|
19
|
+
segmentOptions: {
|
|
20
|
+
model: { showThinkingLevel: true },
|
|
21
|
+
path: { abbreviate: true, maxLength: 32, stripWorkPrefix: true },
|
|
22
|
+
git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: true },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
|
|
15
26
|
minimal: {
|
|
16
27
|
leftSegments: ["path", "git"],
|
|
17
28
|
rightSegments: ["session_name", "jobs", "mode", "context_pct"],
|
|
@@ -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 {
|
|
@@ -617,7 +617,8 @@ export class ExtensionUiController {
|
|
|
617
617
|
const listChromeRows = dialogOptions?.outline === true ? HOOK_SELECTOR_OUTLINE_ROWS : 0;
|
|
618
618
|
// Reserve rows for the inline custom-input editor so opening it doesn't
|
|
619
619
|
// push the scrollable title past the viewport into terminal scrollback.
|
|
620
|
-
const inlineInputRows =
|
|
620
|
+
const inlineInputRows =
|
|
621
|
+
dialogOptions?.customInput || dialogOptions?.clarificationInput ? HOOK_SELECTOR_INLINE_INPUT_ROWS : 0;
|
|
621
622
|
const availableTitleRows =
|
|
622
623
|
this.ctx.ui.terminal.rows - scrollOptionRows - listChromeRows - inlineInputRows - HOOK_SELECTOR_CHROME_ROWS;
|
|
623
624
|
const scrollTitleRows =
|
|
@@ -659,7 +660,10 @@ export class ExtensionUiController {
|
|
|
659
660
|
tui: this.ctx.ui,
|
|
660
661
|
// Share the main prompt editor's autocomplete provider so the
|
|
661
662
|
// inline "Other (type your own)" editor supports `@` file links.
|
|
662
|
-
autocompleteProvider:
|
|
663
|
+
autocompleteProvider:
|
|
664
|
+
dialogOptions?.customInput || dialogOptions?.clarificationInput
|
|
665
|
+
? this.ctx.editor.getAutocompleteProvider()
|
|
666
|
+
: undefined,
|
|
663
667
|
outline: dialogOptions?.outline,
|
|
664
668
|
wrapFocused: dialogOptions?.wrapFocused,
|
|
665
669
|
scrollTitleRows,
|
|
@@ -675,6 +679,18 @@ export class ExtensionUiController {
|
|
|
675
679
|
},
|
|
676
680
|
}
|
|
677
681
|
: undefined,
|
|
682
|
+
clarificationInput: dialogOptions?.clarificationInput
|
|
683
|
+
? {
|
|
684
|
+
optionLabel: dialogOptions.clarificationInput.optionLabel,
|
|
685
|
+
allowEmpty: dialogOptions.clarificationInput.allowEmpty,
|
|
686
|
+
onSubmit: text => {
|
|
687
|
+
const optionLabel = dialogOptions.clarificationInput?.optionLabel;
|
|
688
|
+
this.hideHookSelector();
|
|
689
|
+
dialogOptions.clarificationInput?.onSubmit(text);
|
|
690
|
+
finish(optionLabel);
|
|
691
|
+
},
|
|
692
|
+
}
|
|
693
|
+
: undefined,
|
|
678
694
|
},
|
|
679
695
|
);
|
|
680
696
|
// Detach (not dispose) the reusable editor before mounting the transient hook UI, so the
|
|
@@ -715,6 +731,7 @@ export class ExtensionUiController {
|
|
|
715
731
|
title: string,
|
|
716
732
|
placeholder?: string,
|
|
717
733
|
dialogOptions?: ExtensionUIDialogOptions,
|
|
734
|
+
inputOptions?: { readonly initialValue?: string },
|
|
718
735
|
): Promise<string | undefined> {
|
|
719
736
|
const { promise, finish, attachAbort } = this.#createHookDialogState(
|
|
720
737
|
() => this.hideHookInput(),
|
|
@@ -732,6 +749,7 @@ export class ExtensionUiController {
|
|
|
732
749
|
finish(undefined);
|
|
733
750
|
},
|
|
734
751
|
{
|
|
752
|
+
initialValue: inputOptions?.initialValue,
|
|
735
753
|
timeout: dialogOptions?.timeout,
|
|
736
754
|
onTimeout: dialogOptions?.onTimeout,
|
|
737
755
|
tui: this.ctx.ui,
|