@gajae-code/coding-agent 0.7.6 → 0.7.8
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 +26 -1
- package/README.md +8 -0
- package/dist/types/cli/local-provider-smoke.d.ts +59 -0
- package/dist/types/cli/notify-cli.d.ts +1 -1
- package/dist/types/cli/update-cli.d.ts +24 -1
- package/dist/types/commands/local-provider.d.ts +34 -0
- package/dist/types/config/model-profiles.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +8 -3
- package/dist/types/config/models-config-schema.d.ts +5 -5
- package/dist/types/config/settings-schema.d.ts +123 -9
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/coordinator-mcp/server.d.ts +8 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/gjc-runtime/launch-tmux.d.ts +2 -2
- package/dist/types/gjc-runtime/state-renderer.d.ts +5 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/tmux-common.d.ts +12 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +37 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +80 -0
- package/dist/types/modes/components/custom-model-preset-wizard.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +4 -1
- package/dist/types/modes/components/status-line/types.d.ts +5 -7
- package/dist/types/modes/components/status-line.d.ts +4 -3
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -0
- package/dist/types/modes/tmux-scroll.d.ts +18 -0
- package/dist/types/modes/utils/terminal-bell.d.ts +3 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +13 -2
- package/dist/types/tools/browser/tab-supervisor.d.ts +31 -0
- package/dist/types/tools/computer-gc.d.ts +23 -0
- package/dist/types/tools/cron.d.ts +36 -61
- package/dist/types/tools/index.d.ts +0 -1
- package/dist/types/tools/resource-gc.d.ts +54 -0
- package/dist/types/web/search/index.d.ts +1 -0
- package/dist/types/web/search/providers/utils.d.ts +11 -4
- package/package.json +7 -7
- package/src/cli/args.ts +0 -1
- package/src/cli/fast-help.ts +0 -1
- package/src/cli/local-provider-smoke.ts +632 -0
- package/src/cli/notify-cli.ts +4 -2
- package/src/cli/update-cli.ts +155 -13
- package/src/cli/web-search-cli.ts +5 -0
- package/src/cli.ts +18 -0
- package/src/commands/launch.ts +46 -1
- package/src/commands/local-provider.ts +62 -0
- package/src/commands/notify.ts +1 -1
- package/src/config/model-profiles.ts +3 -4
- package/src/config/model-registry.ts +58 -7
- package/src/config/models-config-schema.ts +10 -1
- package/src/config/settings-schema.ts +127 -10
- package/src/config/settings.ts +11 -0
- package/src/coordinator-mcp/server.ts +276 -34
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -12
- package/src/gjc-runtime/launch-tmux.ts +34 -8
- package/src/gjc-runtime/launch-worktree.ts +16 -2
- package/src/gjc-runtime/state-renderer.ts +13 -0
- package/src/gjc-runtime/team-runtime.ts +21 -6
- package/src/gjc-runtime/tmux-common.ts +16 -0
- package/src/gjc-runtime/tmux-sessions.ts +13 -9
- package/src/gjc-runtime/ultragoal-guard.ts +167 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +221 -1
- package/src/goals/tools/goal-tool.ts +14 -1
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/modes/components/agent-dashboard.ts +2 -1
- package/src/modes/components/custom-model-preset-wizard.ts +30 -189
- package/src/modes/components/model-selector.ts +182 -5
- package/src/modes/components/session-observer-overlay.ts +3 -3
- package/src/modes/components/settings-selector.ts +491 -5
- package/src/modes/components/status-line/segments.ts +7 -18
- package/src/modes/components/status-line/types.ts +6 -2
- package/src/modes/components/status-line.ts +69 -35
- package/src/modes/controllers/event-controller.ts +98 -4
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/input-controller.ts +15 -0
- package/src/modes/controllers/selector-controller.ts +18 -4
- package/src/modes/prompt-action-autocomplete.ts +8 -0
- package/src/modes/tmux-scroll.ts +92 -0
- package/src/modes/utils/terminal-bell.ts +48 -0
- package/src/notifications/telegram-daemon-cli.ts +112 -9
- package/src/notifications/telegram-daemon.ts +4 -0
- package/src/prompts/system/system-prompt.md +2 -2
- package/src/prompts/tools/cron.md +5 -3
- package/src/prompts/tools/read.md +1 -1
- package/src/sdk.ts +5 -0
- package/src/session/agent-session.ts +118 -5
- package/src/slash-commands/builtin-registry.ts +106 -13
- package/src/tools/browser/tab-supervisor.ts +86 -2
- package/src/tools/computer-gc.ts +66 -0
- package/src/tools/computer.ts +2 -0
- package/src/tools/cron.ts +75 -112
- package/src/tools/index.ts +2 -8
- package/src/tools/read.ts +25 -55
- package/src/tools/renderers.ts +0 -2
- package/src/tools/resource-gc.ts +291 -0
- package/src/tools/ultragoal-ask-guard.ts +7 -1
- package/src/web/search/index.ts +1 -0
- package/src/web/search/providers/utils.ts +22 -5
- package/vendor/insane-search/MANIFEST.json +3 -1
- package/vendor/insane-search/engine/__init__.py +14 -0
- package/vendor/insane-search/engine/content_safety.py +151 -0
- package/vendor/insane-search/engine/fetch_chain.py +32 -0
- package/vendor/insane-search/engine/tests/test_u8.py +216 -0
- package/dist/types/tools/inspect-image-renderer.d.ts +0 -26
- package/dist/types/tools/inspect-image.d.ts +0 -31
- package/src/prompts/tools/inspect-image-system.md +0 -20
- package/src/prompts/tools/inspect-image.md +0 -32
- package/src/tools/inspect-image-renderer.ts +0 -103
- package/src/tools/inspect-image.ts +0 -172
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
matchesKey,
|
|
26
26
|
padding,
|
|
27
27
|
replaceTabs,
|
|
28
|
+
resolveTerminalColumns,
|
|
28
29
|
Spacer,
|
|
29
30
|
Text,
|
|
30
31
|
truncateToWidth,
|
|
@@ -870,7 +871,7 @@ export class AgentDashboard extends Container {
|
|
|
870
871
|
}
|
|
871
872
|
|
|
872
873
|
#uiWidth(): number {
|
|
873
|
-
return Math.max(40,
|
|
874
|
+
return Math.max(40, resolveTerminalColumns());
|
|
874
875
|
}
|
|
875
876
|
|
|
876
877
|
/** Rebuild layout and request a TUI render pass (for use after async state changes). */
|
|
@@ -6,15 +6,6 @@ import { DynamicBorder } from "./dynamic-border";
|
|
|
6
6
|
|
|
7
7
|
const PROFILE_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
|
|
8
8
|
|
|
9
|
-
type WizardStep = "name" | "display-name" | "provider" | "model" | "confirm";
|
|
10
|
-
|
|
11
|
-
interface WizardState {
|
|
12
|
-
name: string;
|
|
13
|
-
displayName: string;
|
|
14
|
-
provider: string;
|
|
15
|
-
model: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
9
|
export interface CustomModelPresetWizardSubmit {
|
|
19
10
|
name: string;
|
|
20
11
|
profile: ModelProfileConfig;
|
|
@@ -23,25 +14,21 @@ export interface CustomModelPresetWizardSubmit {
|
|
|
23
14
|
export class CustomModelPresetWizardComponent extends Container {
|
|
24
15
|
#contentContainer: Container;
|
|
25
16
|
#input: Input | null = null;
|
|
26
|
-
#step: WizardStep = "name";
|
|
27
|
-
#selectedIndex = 0;
|
|
28
17
|
#lastError: string | null = null;
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
displayName: "",
|
|
32
|
-
provider: "",
|
|
33
|
-
model: "",
|
|
34
|
-
};
|
|
18
|
+
#name = "";
|
|
19
|
+
#snapshot: ModelProfileConfig;
|
|
35
20
|
#onSubmit: (input: CustomModelPresetWizardSubmit) => void;
|
|
36
21
|
#onCancel: () => void;
|
|
37
22
|
#onRender: () => void;
|
|
38
23
|
|
|
39
24
|
constructor(
|
|
25
|
+
snapshot: ModelProfileConfig,
|
|
40
26
|
onSubmit: (input: CustomModelPresetWizardSubmit) => void,
|
|
41
27
|
onCancel: () => void,
|
|
42
28
|
onRender: () => void = () => {},
|
|
43
29
|
) {
|
|
44
30
|
super();
|
|
31
|
+
this.#snapshot = snapshot;
|
|
45
32
|
this.#onSubmit = onSubmit;
|
|
46
33
|
this.#onCancel = onCancel;
|
|
47
34
|
this.#onRender = onRender;
|
|
@@ -51,7 +38,7 @@ export class CustomModelPresetWizardComponent extends Container {
|
|
|
51
38
|
this.addChild(new TruncatedText(theme.bold("Create custom model preset")));
|
|
52
39
|
this.addChild(
|
|
53
40
|
new TruncatedText(
|
|
54
|
-
theme.fg("muted", " Save
|
|
41
|
+
theme.fg("muted", " Save the current default and explicit role models as a selectable profile."),
|
|
55
42
|
0,
|
|
56
43
|
0,
|
|
57
44
|
),
|
|
@@ -72,222 +59,76 @@ export class CustomModelPresetWizardComponent extends Container {
|
|
|
72
59
|
|
|
73
60
|
handleInput(keyData: string): void {
|
|
74
61
|
if (matchesAppInterrupt(keyData)) {
|
|
75
|
-
|
|
76
|
-
this.#onCancel();
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
this.#goBack();
|
|
62
|
+
this.#onCancel();
|
|
80
63
|
return;
|
|
81
64
|
}
|
|
82
65
|
|
|
83
66
|
if (this.#input) {
|
|
84
67
|
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
85
|
-
this.#
|
|
68
|
+
this.#saveInputAndSubmit();
|
|
86
69
|
return;
|
|
87
70
|
}
|
|
88
71
|
this.#input.handleInput(keyData);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (matchesKey(keyData, "up")) {
|
|
93
|
-
this.#moveSelection(-1);
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
if (matchesKey(keyData, "down")) {
|
|
97
|
-
this.#moveSelection(1);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
101
|
-
this.#selectCurrentOption();
|
|
102
72
|
}
|
|
103
73
|
}
|
|
104
74
|
|
|
105
75
|
#renderStep(): void {
|
|
106
76
|
this.#contentContainer.clear();
|
|
107
77
|
this.#input = null;
|
|
108
|
-
|
|
109
|
-
case "name":
|
|
110
|
-
this.#renderInputStep(
|
|
111
|
-
"Step 1: Preset id",
|
|
112
|
-
"Enter a unique preset id:",
|
|
113
|
-
this.#state.name,
|
|
114
|
-
"e.g. my-fast-coder",
|
|
115
|
-
);
|
|
116
|
-
break;
|
|
117
|
-
case "display-name":
|
|
118
|
-
this.#renderInputStep(
|
|
119
|
-
"Step 2: Display name",
|
|
120
|
-
"Enter a display name:",
|
|
121
|
-
this.#state.displayName,
|
|
122
|
-
"e.g. My Fast Coder",
|
|
123
|
-
);
|
|
124
|
-
break;
|
|
125
|
-
case "provider":
|
|
126
|
-
this.#renderInputStep(
|
|
127
|
-
"Step 3: Provider",
|
|
128
|
-
"Enter the provider id:",
|
|
129
|
-
this.#state.provider,
|
|
130
|
-
"e.g. openai-codex, anthropic, my-oai",
|
|
131
|
-
);
|
|
132
|
-
break;
|
|
133
|
-
case "model":
|
|
134
|
-
this.#renderInputStep(
|
|
135
|
-
"Step 4: Model",
|
|
136
|
-
"Enter the model id or provider/model selector:",
|
|
137
|
-
this.#state.model,
|
|
138
|
-
"e.g. gpt-5.5:medium or my-oai/gpt-example:low",
|
|
139
|
-
);
|
|
140
|
-
break;
|
|
141
|
-
case "confirm":
|
|
142
|
-
this.#renderConfirmStep();
|
|
143
|
-
break;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
#renderInputStep(title: string, prompt: string, value: string, hint: string): void {
|
|
148
|
-
this.#contentContainer.addChild(new Text(theme.fg("accent", title)));
|
|
78
|
+
this.#contentContainer.addChild(new Text(theme.fg("accent", "Preset id")));
|
|
149
79
|
this.#contentContainer.addChild(new Spacer(1));
|
|
150
80
|
if (this.#lastError) {
|
|
151
81
|
this.#contentContainer.addChild(new Text(theme.fg("error", this.#lastError), 0, 0));
|
|
152
82
|
this.#contentContainer.addChild(new Spacer(1));
|
|
153
83
|
}
|
|
154
|
-
this.#contentContainer.addChild(new Text(
|
|
84
|
+
this.#contentContainer.addChild(new Text("Enter a unique preset id:", 0, 0));
|
|
155
85
|
this.#contentContainer.addChild(new Spacer(1));
|
|
156
86
|
this.#input = new Input();
|
|
157
|
-
this.#input.setValue(
|
|
87
|
+
this.#input.setValue(this.#name);
|
|
158
88
|
this.#contentContainer.addChild(this.#input);
|
|
159
89
|
this.#contentContainer.addChild(new Spacer(1));
|
|
160
|
-
this.#
|
|
161
|
-
this.#addHelp("
|
|
90
|
+
this.#addSnapshotPreview();
|
|
91
|
+
this.#addHelp("e.g. my-fast-coder");
|
|
92
|
+
this.#addHelp("[Enter to create, Esc to cancel]");
|
|
162
93
|
}
|
|
163
94
|
|
|
164
|
-
#
|
|
165
|
-
this.#contentContainer.addChild(new Text(theme.fg("
|
|
166
|
-
this.#
|
|
167
|
-
|
|
168
|
-
this.#contentContainer.addChild(new Text(theme.fg("error", this.#lastError), 0, 0));
|
|
169
|
-
this.#contentContainer.addChild(new Spacer(1));
|
|
95
|
+
#addSnapshotPreview(): void {
|
|
96
|
+
this.#contentContainer.addChild(new Text(theme.fg("muted", "Snapshot:"), 0, 0));
|
|
97
|
+
for (const [role, selector] of Object.entries(this.#snapshot.model_mapping)) {
|
|
98
|
+
this.#contentContainer.addChild(new Text(` ${role}: ${selector}`, 0, 0));
|
|
170
99
|
}
|
|
171
|
-
this.#contentContainer.addChild(new Text(`
|
|
172
|
-
this.#contentContainer.addChild(new Text(`Display name: ${this.#state.displayName}`, 0, 0));
|
|
173
|
-
this.#contentContainer.addChild(new Text(`Provider: ${this.#state.provider}`, 0, 0));
|
|
174
|
-
this.#contentContainer.addChild(new Text(`Default model: ${this.#selector()}`, 0, 0));
|
|
100
|
+
this.#contentContainer.addChild(new Text(` providers: ${this.#snapshot.required_providers.join(", ")}`, 0, 0));
|
|
175
101
|
this.#contentContainer.addChild(new Spacer(1));
|
|
176
|
-
this.#addOption(0, "Create preset");
|
|
177
|
-
this.#addOption(1, "Go back");
|
|
178
|
-
this.#addHelp("[↑↓ to navigate, Enter to select, Esc to go back]");
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
#addOption(index: number, label: string): void {
|
|
182
|
-
const selected = index === this.#selectedIndex;
|
|
183
|
-
const prefix = selected ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
|
|
184
|
-
this.#contentContainer.addChild(new Text(`${prefix}${selected ? theme.fg("accent", label) : label}`, 0, 0));
|
|
185
102
|
}
|
|
186
103
|
|
|
187
104
|
#addHelp(text: string): void {
|
|
188
105
|
this.#contentContainer.addChild(new Text(theme.fg("muted", text), 0, 0));
|
|
189
106
|
}
|
|
190
107
|
|
|
191
|
-
#
|
|
108
|
+
#saveInputAndSubmit(): void {
|
|
192
109
|
const value = this.#input?.getValue().trim() ?? "";
|
|
193
110
|
if (!value) {
|
|
194
|
-
this.#lastError =
|
|
111
|
+
this.#lastError = "Preset id is required.";
|
|
195
112
|
this.#renderStep();
|
|
196
113
|
this.#onRender();
|
|
197
114
|
return;
|
|
198
115
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
this.#lastError = validationError;
|
|
116
|
+
if (!PROFILE_NAME_PATTERN.test(value)) {
|
|
117
|
+
this.#lastError = "Preset id must use lowercase letters, numbers, dots, underscores, or hyphens.";
|
|
202
118
|
this.#renderStep();
|
|
203
119
|
this.#onRender();
|
|
204
120
|
return;
|
|
205
121
|
}
|
|
122
|
+
this.#name = value;
|
|
206
123
|
this.#lastError = null;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
this.#step = "display-name";
|
|
210
|
-
} else if (this.#step === "display-name") {
|
|
211
|
-
this.#state.displayName = value;
|
|
212
|
-
this.#step = "provider";
|
|
213
|
-
} else if (this.#step === "provider") {
|
|
214
|
-
this.#state.provider = value;
|
|
215
|
-
this.#step = "model";
|
|
216
|
-
} else if (this.#step === "model") {
|
|
217
|
-
this.#state.model = value;
|
|
218
|
-
this.#step = "confirm";
|
|
219
|
-
this.#selectedIndex = 0;
|
|
220
|
-
}
|
|
221
|
-
this.#renderStep();
|
|
222
|
-
this.#onRender();
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
#emptyFieldMessage(): string {
|
|
226
|
-
switch (this.#step) {
|
|
227
|
-
case "name":
|
|
228
|
-
return "Preset id is required.";
|
|
229
|
-
case "display-name":
|
|
230
|
-
return "Display name is required.";
|
|
231
|
-
case "provider":
|
|
232
|
-
return "Provider is required.";
|
|
233
|
-
case "model":
|
|
234
|
-
return "Model is required.";
|
|
235
|
-
case "confirm":
|
|
236
|
-
return "Value is required.";
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
#validateCurrentInput(value: string): string | undefined {
|
|
241
|
-
if (this.#step === "name" && !PROFILE_NAME_PATTERN.test(value)) {
|
|
242
|
-
return "Preset id must use lowercase letters, numbers, dots, underscores, or hyphens.";
|
|
243
|
-
}
|
|
244
|
-
if (this.#step === "provider" && !PROFILE_NAME_PATTERN.test(value)) {
|
|
245
|
-
return "Provider id must use lowercase letters, numbers, dots, underscores, or hyphens.";
|
|
246
|
-
}
|
|
247
|
-
if (this.#step === "model" && value.includes(",")) {
|
|
248
|
-
return "Model must be one selector, not a comma-separated list.";
|
|
249
|
-
}
|
|
250
|
-
return undefined;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
#selectCurrentOption(): void {
|
|
254
|
-
if (this.#step !== "confirm") return;
|
|
255
|
-
if (this.#selectedIndex === 0) {
|
|
256
|
-
this.#onSubmit(this.#buildInput());
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
this.#goBack();
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
#buildInput(): CustomModelPresetWizardSubmit {
|
|
263
|
-
return {
|
|
264
|
-
name: this.#state.name,
|
|
124
|
+
this.#onSubmit({
|
|
125
|
+
name: value,
|
|
265
126
|
profile: {
|
|
266
|
-
|
|
267
|
-
display_name:
|
|
268
|
-
model_mapping: {
|
|
127
|
+
...this.#snapshot,
|
|
128
|
+
display_name: value,
|
|
129
|
+
model_mapping: { ...this.#snapshot.model_mapping },
|
|
130
|
+
required_providers: [...this.#snapshot.required_providers],
|
|
269
131
|
},
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
#selector(): string {
|
|
274
|
-
return this.#state.model.includes("/") ? this.#state.model : `${this.#state.provider}/${this.#state.model}`;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
#moveSelection(delta: number): void {
|
|
278
|
-
this.#selectedIndex = (this.#selectedIndex + delta + 2) % 2;
|
|
279
|
-
this.#renderStep();
|
|
280
|
-
this.#onRender();
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
#goBack(): void {
|
|
284
|
-
if (this.#step === "display-name") this.#step = "name";
|
|
285
|
-
else if (this.#step === "provider") this.#step = "display-name";
|
|
286
|
-
else if (this.#step === "model") this.#step = "provider";
|
|
287
|
-
else if (this.#step === "confirm") this.#step = "model";
|
|
288
|
-
this.#selectedIndex = 0;
|
|
289
|
-
this.#lastError = null;
|
|
290
|
-
this.#renderStep();
|
|
291
|
-
this.#onRender();
|
|
132
|
+
});
|
|
292
133
|
}
|
|
293
134
|
}
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
resolveModelRoleValue,
|
|
29
29
|
type ScopedModelSelection,
|
|
30
30
|
} from "../../config/model-resolver";
|
|
31
|
+
import type { ModelProfileConfig } from "../../config/models-config-schema";
|
|
31
32
|
import type { Settings } from "../../config/settings";
|
|
32
33
|
import { type ThemeColor, theme } from "../../modes/theme/theme";
|
|
33
34
|
import { formatModelOnboardingInlineHint } from "../../setup/model-onboarding-guidance";
|
|
@@ -105,6 +106,7 @@ export type ModelSelectorSelection =
|
|
|
105
106
|
}
|
|
106
107
|
| {
|
|
107
108
|
kind: "createProfile";
|
|
109
|
+
profile: ModelProfileConfig;
|
|
108
110
|
};
|
|
109
111
|
|
|
110
112
|
interface PendingThinkingChoice {
|
|
@@ -155,11 +157,27 @@ interface PresetCreateRow {
|
|
|
155
157
|
kind: "create";
|
|
156
158
|
}
|
|
157
159
|
|
|
160
|
+
interface PresetCreateUnavailableRow {
|
|
161
|
+
kind: "createUnavailable";
|
|
162
|
+
label: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
interface PresetAlreadySavedRow {
|
|
166
|
+
kind: "alreadySaved";
|
|
167
|
+
profileName: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
158
170
|
interface PresetBrowseRow {
|
|
159
171
|
kind: "browse";
|
|
160
172
|
}
|
|
161
173
|
|
|
162
|
-
type PresetLandingRow =
|
|
174
|
+
type PresetLandingRow =
|
|
175
|
+
| PresetGroupRow
|
|
176
|
+
| PresetProfileRow
|
|
177
|
+
| PresetCreateRow
|
|
178
|
+
| PresetCreateUnavailableRow
|
|
179
|
+
| PresetAlreadySavedRow
|
|
180
|
+
| PresetBrowseRow;
|
|
163
181
|
|
|
164
182
|
// Stable logical identity for a preset landing row, independent of its current
|
|
165
183
|
// list position. Used to relocate the cursor after the expanded group changes so
|
|
@@ -174,6 +192,10 @@ function presetRowIdentity(row: PresetLandingRow): string {
|
|
|
174
192
|
return "browse";
|
|
175
193
|
case "create":
|
|
176
194
|
return "create";
|
|
195
|
+
case "createUnavailable":
|
|
196
|
+
return "createUnavailable";
|
|
197
|
+
case "alreadySaved":
|
|
198
|
+
return `alreadySaved:${row.profileName}`;
|
|
177
199
|
}
|
|
178
200
|
}
|
|
179
201
|
|
|
@@ -193,6 +215,64 @@ function isPrintableCharacter(keyData: string): boolean {
|
|
|
193
215
|
function profileRequiredProviders(profile: ModelProfileDefinition): string[] {
|
|
194
216
|
return [...new Set(profile.requiredProviders)].sort((a, b) => a.localeCompare(b));
|
|
195
217
|
}
|
|
218
|
+
|
|
219
|
+
function isInheritedRoleSelector(value: string | undefined): boolean {
|
|
220
|
+
const normalized = value?.trim();
|
|
221
|
+
return !normalized || normalized === "default" || normalized === "pi/default";
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function getDefaultAliasThinkingLevel(value: string | undefined): ThinkingLevel | undefined {
|
|
225
|
+
const normalized = value?.trim();
|
|
226
|
+
if (!normalized?.startsWith("pi/default:")) return undefined;
|
|
227
|
+
return parseThinkingLevel(normalized.slice("pi/default:".length));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function getSelectorProvider(selector: string): string | undefined {
|
|
231
|
+
const slashIndex = selector.indexOf("/");
|
|
232
|
+
return slashIndex > 0 ? selector.slice(0, slashIndex) : undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function deriveRequiredProviders(modelMapping: ModelProfileConfig["model_mapping"]): string[] {
|
|
236
|
+
const providers = new Set<string>();
|
|
237
|
+
for (const selector of Object.values(modelMapping)) {
|
|
238
|
+
const provider = getSelectorProvider(selector);
|
|
239
|
+
if (provider) providers.add(provider);
|
|
240
|
+
}
|
|
241
|
+
return [...providers].sort((a, b) => a.localeCompare(b));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function sameStringRecord(
|
|
245
|
+
left: Readonly<Record<string, string | undefined>>,
|
|
246
|
+
right: Readonly<Record<string, string | undefined>>,
|
|
247
|
+
): boolean {
|
|
248
|
+
const leftEntries = Object.entries(left)
|
|
249
|
+
.filter((entry): entry is [string, string] => entry[1] !== undefined)
|
|
250
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
251
|
+
const rightEntries = Object.entries(right)
|
|
252
|
+
.filter((entry): entry is [string, string] => entry[1] !== undefined)
|
|
253
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
254
|
+
if (leftEntries.length !== rightEntries.length) return false;
|
|
255
|
+
for (let i = 0; i < leftEntries.length; i++) {
|
|
256
|
+
const leftEntry = leftEntries[i];
|
|
257
|
+
const rightEntry = rightEntries[i];
|
|
258
|
+
if (!leftEntry || !rightEntry || leftEntry[0] !== rightEntry[0] || leftEntry[1] !== rightEntry[1]) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function sameStringArray(left: readonly string[], right: readonly string[]): boolean {
|
|
266
|
+
if (left.length !== right.length) return false;
|
|
267
|
+
for (let i = 0; i < left.length; i++) {
|
|
268
|
+
if (left[i] !== right[i]) return false;
|
|
269
|
+
}
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function hasPersistableProfileSnapshot(snapshot: ModelProfileConfig): boolean {
|
|
274
|
+
return Object.keys(snapshot.model_mapping).length > 0 && snapshot.required_providers.length > 0;
|
|
275
|
+
}
|
|
196
276
|
/**
|
|
197
277
|
* Component that renders a canonical model selector with provider tabs.
|
|
198
278
|
* - Preset landing Left/Right: Collapse/expand selected provider
|
|
@@ -733,6 +813,67 @@ export class ModelSelectorComponent extends Container {
|
|
|
733
813
|
return groupModelProfilesForPresetLanding(this.#modelRegistry.getModelProfiles?.() ?? new Map());
|
|
734
814
|
}
|
|
735
815
|
|
|
816
|
+
#formatCurrentModelSelector(
|
|
817
|
+
thinkingLevel: ThinkingLevel | undefined = this.#currentThinkingLevel,
|
|
818
|
+
): string | undefined {
|
|
819
|
+
if (!this.#currentModel) return undefined;
|
|
820
|
+
return formatModelSelectorValue(`${this.#currentModel.provider}/${this.#currentModel.id}`, thinkingLevel);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
#resolveProfileModelSelector(value: string | undefined): string | undefined {
|
|
824
|
+
const normalized = value?.trim();
|
|
825
|
+
if (isInheritedRoleSelector(normalized)) return undefined;
|
|
826
|
+
|
|
827
|
+
const resolved = resolveModelRoleValue(normalized, this.#modelRegistry.getAll(), {
|
|
828
|
+
settings: this.#settings,
|
|
829
|
+
matchPreferences: { usageOrder: this.#settings.getStorage()?.getModelUsageOrder() },
|
|
830
|
+
modelRegistry: this.#modelRegistry,
|
|
831
|
+
});
|
|
832
|
+
if (resolved.model) {
|
|
833
|
+
return formatModelSelectorValue(`${resolved.model.provider}/${resolved.model.id}`, resolved.thinkingLevel);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const inheritedDefaultThinkingLevel = getDefaultAliasThinkingLevel(normalized);
|
|
837
|
+
if (inheritedDefaultThinkingLevel) return this.#formatCurrentModelSelector(inheritedDefaultThinkingLevel);
|
|
838
|
+
return undefined;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
#buildCustomModelProfileSnapshot(): ModelProfileConfig {
|
|
842
|
+
const modelMapping: ModelProfileConfig["model_mapping"] = {};
|
|
843
|
+
const currentModelSelector = this.#formatCurrentModelSelector();
|
|
844
|
+
if (currentModelSelector) {
|
|
845
|
+
modelMapping.default = currentModelSelector;
|
|
846
|
+
} else {
|
|
847
|
+
const defaultRole = this.#settings.getModelRole("default");
|
|
848
|
+
const defaultSelector = this.#resolveProfileModelSelector(defaultRole);
|
|
849
|
+
if (defaultSelector) modelMapping.default = defaultSelector;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const agentOverrides = this.#settings.get("task.agentModelOverrides");
|
|
853
|
+
for (const role of GJC_MODEL_ASSIGNMENT_TARGET_IDS) {
|
|
854
|
+
if (role === "default") continue;
|
|
855
|
+
const selector = this.#resolveProfileModelSelector(agentOverrides[role]);
|
|
856
|
+
if (selector) modelMapping[role] = selector;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
return {
|
|
860
|
+
required_providers: deriveRequiredProviders(modelMapping),
|
|
861
|
+
model_mapping: modelMapping,
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
#findDuplicateGeneratedProfile(snapshot: ModelProfileConfig): ModelProfileDefinition | undefined {
|
|
866
|
+
for (const profile of this.#modelRegistry.getModelProfiles?.().values() ?? []) {
|
|
867
|
+
if (
|
|
868
|
+
sameStringRecord(profile.modelMapping, snapshot.model_mapping) &&
|
|
869
|
+
sameStringArray(profile.requiredProviders, snapshot.required_providers)
|
|
870
|
+
) {
|
|
871
|
+
return profile;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
return undefined;
|
|
875
|
+
}
|
|
876
|
+
|
|
736
877
|
#getPresetRows(): PresetLandingRow[] {
|
|
737
878
|
const rows: PresetLandingRow[] = [];
|
|
738
879
|
for (const [groupId, profiles] of this.#getPresetGroups()) {
|
|
@@ -741,7 +882,15 @@ export class ModelSelectorComponent extends Container {
|
|
|
741
882
|
for (const profile of profiles) rows.push({ kind: "profile", groupId, profile });
|
|
742
883
|
}
|
|
743
884
|
}
|
|
744
|
-
|
|
885
|
+
const snapshot = this.#buildCustomModelProfileSnapshot();
|
|
886
|
+
if (hasPersistableProfileSnapshot(snapshot)) {
|
|
887
|
+
const duplicateProfile = this.#findDuplicateGeneratedProfile(snapshot);
|
|
888
|
+
rows.push(
|
|
889
|
+
duplicateProfile ? { kind: "alreadySaved", profileName: duplicateProfile.name } : { kind: "create" },
|
|
890
|
+
);
|
|
891
|
+
} else {
|
|
892
|
+
rows.push({ kind: "createUnavailable", label: "Select a model before creating a custom preset" });
|
|
893
|
+
}
|
|
745
894
|
rows.push({ kind: "browse" });
|
|
746
895
|
return rows;
|
|
747
896
|
}
|
|
@@ -815,7 +964,14 @@ export class ModelSelectorComponent extends Container {
|
|
|
815
964
|
|
|
816
965
|
#expandSelectedPresetProvider(): void {
|
|
817
966
|
const selected = this.#getSelectedPresetRow();
|
|
818
|
-
if (
|
|
967
|
+
if (
|
|
968
|
+
!selected ||
|
|
969
|
+
selected.kind === "browse" ||
|
|
970
|
+
selected.kind === "create" ||
|
|
971
|
+
selected.kind === "createUnavailable" ||
|
|
972
|
+
selected.kind === "alreadySaved"
|
|
973
|
+
)
|
|
974
|
+
return;
|
|
819
975
|
if (this.#expandedPresetProviderId === selected.groupId) return;
|
|
820
976
|
const targetIdentity = presetRowIdentity(selected);
|
|
821
977
|
this.#expandedPresetProviderId = selected.groupId;
|
|
@@ -824,7 +980,14 @@ export class ModelSelectorComponent extends Container {
|
|
|
824
980
|
|
|
825
981
|
#collapseSelectedPresetProvider(): void {
|
|
826
982
|
const selected = this.#getSelectedPresetRow();
|
|
827
|
-
if (
|
|
983
|
+
if (
|
|
984
|
+
!selected ||
|
|
985
|
+
selected.kind === "browse" ||
|
|
986
|
+
selected.kind === "create" ||
|
|
987
|
+
selected.kind === "createUnavailable" ||
|
|
988
|
+
selected.kind === "alreadySaved"
|
|
989
|
+
)
|
|
990
|
+
return;
|
|
828
991
|
if (this.#expandedPresetProviderId !== selected.groupId) return;
|
|
829
992
|
const targetIdentity = selected.kind === "profile" ? `group:${selected.groupId}` : presetRowIdentity(selected);
|
|
830
993
|
this.#expandedPresetProviderId = undefined;
|
|
@@ -860,6 +1023,17 @@ export class ModelSelectorComponent extends Container {
|
|
|
860
1023
|
this.#listContainer.addChild(new Text(`${prefix}${selected ? theme.fg("accent", label) : label}`, 0, 0));
|
|
861
1024
|
continue;
|
|
862
1025
|
}
|
|
1026
|
+
if (row.kind === "createUnavailable") {
|
|
1027
|
+
const renderedLabel = selected ? theme.fg("accent", row.label) : theme.fg("dim", row.label);
|
|
1028
|
+
this.#listContainer.addChild(new Text(`${prefix}${renderedLabel}`, 0, 0));
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
if (row.kind === "alreadySaved") {
|
|
1032
|
+
const label = `Already saved as ${row.profileName}`;
|
|
1033
|
+
const renderedLabel = selected ? theme.fg("accent", label) : theme.fg("dim", label);
|
|
1034
|
+
this.#listContainer.addChild(new Text(`${prefix}${renderedLabel}`, 0, 0));
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
863
1037
|
if (row.kind === "browse") {
|
|
864
1038
|
const label = "Browse all models";
|
|
865
1039
|
this.#listContainer.addChild(new Text(`${prefix}${selected ? theme.fg("accent", label) : label}`, 0, 0));
|
|
@@ -1306,7 +1480,10 @@ export class ModelSelectorComponent extends Container {
|
|
|
1306
1480
|
const row = this.#getSelectedPresetRow();
|
|
1307
1481
|
if (!row) return;
|
|
1308
1482
|
if (row.kind === "create") {
|
|
1309
|
-
this.#onSelectCallback({ kind: "createProfile" });
|
|
1483
|
+
this.#onSelectCallback({ kind: "createProfile", profile: this.#buildCustomModelProfileSnapshot() });
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
if (row.kind === "alreadySaved" || row.kind === "createUnavailable") {
|
|
1310
1487
|
return;
|
|
1311
1488
|
}
|
|
1312
1489
|
if (row.kind === "browse") {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* - Enter on main session -> close overlay (jump back)
|
|
16
16
|
*/
|
|
17
17
|
import type { ToolResultMessage } from "@gajae-code/ai";
|
|
18
|
-
import { Container, Markdown, type MarkdownTheme, matchesKey } from "@gajae-code/tui";
|
|
18
|
+
import { Container, Markdown, type MarkdownTheme, matchesKey, resolveTerminalColumns } from "@gajae-code/tui";
|
|
19
19
|
import { formatDuration, formatNumber, logger } from "@gajae-code/utils";
|
|
20
20
|
import type { KeyId } from "../../config/keybindings";
|
|
21
21
|
import { isSilentAbort } from "../../session/messages";
|
|
@@ -39,7 +39,7 @@ const INDENT = " ";
|
|
|
39
39
|
|
|
40
40
|
/** Compute the max content width for the current terminal, accounting for indent and chrome. */
|
|
41
41
|
function contentWidth(indent = INDENT): number {
|
|
42
|
-
return Math.max(TRUNCATE_LENGTHS.SHORT, (
|
|
42
|
+
return Math.max(TRUNCATE_LENGTHS.SHORT, resolveTerminalColumns() - indent.length - 2);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/** Sanitize a line for TUI display: replace tabs, then truncate to viewport width. */
|
|
@@ -390,7 +390,7 @@ export class SessionObserverOverlayComponent extends Container {
|
|
|
390
390
|
|
|
391
391
|
/** Render markdown text into indented lines using the theme's markdown renderer */
|
|
392
392
|
#renderMarkdownToLines(text: string, indent: string = INDENT): string[] {
|
|
393
|
-
const width = Math.max(40, (
|
|
393
|
+
const width = Math.max(40, resolveTerminalColumns() - indent.length - 4);
|
|
394
394
|
const md = new Markdown(text, 0, 0, this.#mdTheme);
|
|
395
395
|
const rendered = md.render(width);
|
|
396
396
|
return rendered.map(line => `${indent}${line.trimEnd()}`);
|