@gajae-code/coding-agent 0.5.4 → 0.6.1
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 +23 -0
- package/dist/types/cli/web-search-cli.d.ts +12 -0
- package/dist/types/commands/rlm.d.ts +10 -0
- package/dist/types/commands/web-search.d.ts +54 -0
- package/dist/types/config/keybindings.d.ts +10 -0
- package/dist/types/config/model-profiles.d.ts +2 -1
- package/dist/types/config/model-registry.d.ts +3 -0
- package/dist/types/config/models-config-schema.d.ts +3 -0
- package/dist/types/config/settings-schema.d.ts +61 -3
- package/dist/types/edit/notebook.d.ts +3 -0
- package/dist/types/eval/py/executor.d.ts +3 -0
- package/dist/types/eval/py/kernel.d.ts +3 -1
- package/dist/types/eval/py/runtime.d.ts +9 -1
- package/dist/types/exec/bash-executor.d.ts +4 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -0
- package/dist/types/extensibility/custom-tools/wrapper.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +2 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +1 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +6 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +14 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +6 -0
- package/dist/types/gjc-runtime/tmux-gc.d.ts +3 -3
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +4 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +18 -0
- package/dist/types/goals/state.d.ts +1 -1
- package/dist/types/goals/tools/goal-tool.d.ts +2 -0
- package/dist/types/main.d.ts +11 -0
- package/dist/types/modes/components/custom-editor.d.ts +4 -2
- package/dist/types/modes/components/custom-model-preset-wizard.d.ts +12 -0
- package/dist/types/modes/components/model-selector.d.ts +5 -2
- package/dist/types/modes/components/status-line.d.ts +4 -1
- package/dist/types/modes/controllers/input-controller.d.ts +3 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/print-mode.d.ts +6 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +21 -0
- package/dist/types/modes/rpc/rpc-socket-security.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +13 -0
- package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +2 -0
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +1 -0
- package/dist/types/rlm/artifacts.d.ts +9 -0
- package/dist/types/rlm/complete-research-tool.d.ts +35 -0
- package/dist/types/rlm/data-context.d.ts +6 -0
- package/dist/types/rlm/index.d.ts +35 -0
- package/dist/types/rlm/notebook.d.ts +12 -0
- package/dist/types/rlm/preset.d.ts +23 -0
- package/dist/types/rlm/python-tool.d.ts +16 -0
- package/dist/types/rlm/report.d.ts +14 -0
- package/dist/types/rlm/types.d.ts +37 -0
- package/dist/types/sdk.d.ts +7 -0
- package/dist/types/session/agent-session.d.ts +21 -0
- package/dist/types/tools/bash-allowed-prefixes.d.ts +6 -1
- package/dist/types/tools/browser/attach.d.ts +19 -3
- package/dist/types/tools/browser/registry.d.ts +15 -0
- package/dist/types/tools/browser/render.d.ts +3 -0
- package/dist/types/tools/browser.d.ts +18 -1
- package/dist/types/tools/computer/render.d.ts +17 -0
- package/dist/types/tools/computer.d.ts +465 -0
- package/dist/types/tools/index.d.ts +24 -1
- package/dist/types/tools/job.d.ts +13 -0
- package/dist/types/tools/tool-timeouts.d.ts +5 -0
- package/dist/types/web/search/index.d.ts +32 -2
- package/dist/types/web/search/providers/base.d.ts +22 -0
- package/dist/types/web/search/providers/xai.d.ts +64 -0
- package/dist/types/web/search/types.d.ts +11 -3
- package/package.json +7 -7
- package/src/cli/web-search-cli.ts +123 -8
- package/src/cli.ts +2 -0
- package/src/commands/rlm.ts +19 -0
- package/src/commands/web-search.ts +66 -0
- package/src/config/keybindings.ts +11 -0
- package/src/config/model-profiles.ts +11 -3
- package/src/config/model-registry.ts +55 -1
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +67 -1
- package/src/edit/notebook.ts +6 -2
- package/src/eval/py/executor.ts +8 -1
- package/src/eval/py/kernel.ts +9 -4
- package/src/eval/py/runtime.ts +153 -32
- package/src/exec/bash-executor.ts +10 -4
- package/src/extensibility/custom-tools/types.ts +2 -0
- package/src/extensibility/custom-tools/wrapper.ts +2 -0
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/extensibility/extensions/wrapper.ts +1 -0
- package/src/gjc-runtime/launch-tmux.ts +129 -1
- package/src/gjc-runtime/session-state-sidecar.ts +61 -1
- package/src/gjc-runtime/tmux-common.ts +26 -2
- package/src/gjc-runtime/tmux-gc.ts +40 -27
- package/src/gjc-runtime/tmux-sessions.ts +13 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +340 -18
- package/src/goals/runtime.ts +4 -3
- package/src/goals/state.ts +1 -1
- package/src/goals/tools/goal-tool.ts +16 -3
- package/src/internal-urls/docs-index.generated.ts +13 -9
- package/src/main.ts +28 -3
- package/src/modes/components/custom-editor.ts +13 -4
- package/src/modes/components/custom-model-preset-wizard.ts +293 -0
- package/src/modes/components/hook-selector.ts +1 -1
- package/src/modes/components/model-selector.ts +72 -29
- package/src/modes/components/skill-message.ts +62 -8
- package/src/modes/components/status-line.ts +13 -1
- package/src/modes/controllers/input-controller.ts +60 -11
- package/src/modes/controllers/selector-controller.ts +39 -0
- package/src/modes/interactive-mode.ts +1 -1
- package/src/modes/print-mode.ts +14 -4
- package/src/modes/rpc/rpc-client.ts +250 -80
- package/src/modes/rpc/rpc-mode.ts +6 -12
- package/src/modes/rpc/rpc-socket-security.ts +103 -0
- package/src/modes/rpc/rpc-types.ts +10 -0
- package/src/modes/shared/agent-wire/command-dispatch.ts +7 -0
- package/src/modes/shared/agent-wire/command-validation.ts +1 -0
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +9 -0
- package/src/modes/utils/hotkeys-markdown.ts +4 -2
- package/src/modes/utils/ui-helpers.ts +2 -2
- package/src/prompts/goals/goal-continuation.md +1 -0
- package/src/prompts/goals/goal-mode-active.md +1 -0
- package/src/prompts/system/rlm-report-command.md +1 -0
- package/src/prompts/system/rlm-research.md +23 -0
- package/src/prompts/tools/bash.md +23 -2
- package/src/prompts/tools/browser.md +7 -3
- package/src/prompts/tools/computer.md +74 -0
- package/src/prompts/tools/goal.md +3 -0
- package/src/prompts/tools/job.md +9 -1
- package/src/prompts/tools/web-search.md +7 -0
- package/src/rlm/artifacts.ts +60 -0
- package/src/rlm/complete-research-tool.ts +163 -0
- package/src/rlm/data-context.ts +26 -0
- package/src/rlm/index.ts +339 -0
- package/src/rlm/notebook.ts +108 -0
- package/src/rlm/preset.ts +76 -0
- package/src/rlm/python-tool.ts +68 -0
- package/src/rlm/report.ts +70 -0
- package/src/rlm/types.ts +40 -0
- package/src/sdk.ts +12 -0
- package/src/session/agent-session.ts +48 -3
- package/src/slash-commands/builtin-registry.ts +17 -0
- package/src/tools/bash-allowed-prefixes.ts +84 -1
- package/src/tools/bash.ts +80 -13
- package/src/tools/browser/attach.ts +103 -3
- package/src/tools/browser/registry.ts +176 -2
- package/src/tools/browser/render.ts +9 -1
- package/src/tools/browser.ts +33 -0
- package/src/tools/computer/render.ts +78 -0
- package/src/tools/computer.ts +640 -0
- package/src/tools/index.ts +41 -1
- package/src/tools/job.ts +88 -5
- package/src/tools/json-tree.ts +42 -29
- package/src/tools/renderers.ts +2 -0
- package/src/tools/tool-timeouts.ts +1 -0
- package/src/web/search/index.ts +27 -2
- package/src/web/search/provider.ts +16 -1
- package/src/web/search/providers/base.ts +22 -0
- package/src/web/search/providers/xai.ts +511 -0
- package/src/web/search/render.ts +7 -0
- package/src/web/search/types.ts +11 -1
package/src/main.ts
CHANGED
|
@@ -705,11 +705,23 @@ async function buildSessionOptions(
|
|
|
705
705
|
return { options };
|
|
706
706
|
}
|
|
707
707
|
|
|
708
|
+
/**
|
|
709
|
+
* Research-mode (RLM) preset hook. Lets `gjc rlm` augment the session options
|
|
710
|
+
* (system prompt, restricted toolset, custom python tool) and assert the tool
|
|
711
|
+
* boundary once the session's tool registry is fully assembled.
|
|
712
|
+
*/
|
|
713
|
+
export interface RlmPreset {
|
|
714
|
+
applyOptions: (options: CreateAgentSessionOptions, settings: Settings) => void;
|
|
715
|
+
onSessionCreated?: (session: AgentSession) => void | Promise<void>;
|
|
716
|
+
}
|
|
717
|
+
|
|
708
718
|
interface RunRootCommandDependencies {
|
|
709
719
|
createAgentSession?: typeof createAgentSession;
|
|
710
720
|
discoverAuthStorage?: typeof discoverAuthStorage;
|
|
711
721
|
runAcpMode?: (createSession: AcpSessionFactory) => Promise<void>;
|
|
712
722
|
settings?: Settings;
|
|
723
|
+
rlmPreset?: RlmPreset;
|
|
724
|
+
suppressProcessExit?: boolean;
|
|
713
725
|
}
|
|
714
726
|
|
|
715
727
|
export async function runRootCommand(
|
|
@@ -897,6 +909,9 @@ export async function runRootCommand(
|
|
|
897
909
|
sessionOptions.hasUI = isInteractive || mode === "rpc-ui";
|
|
898
910
|
sessionOptions.settings = settingsInstance;
|
|
899
911
|
|
|
912
|
+
// Research-mode (RLM) preset: augment session options before session creation.
|
|
913
|
+
deps.rlmPreset?.applyOptions(sessionOptions, settingsInstance);
|
|
914
|
+
|
|
900
915
|
// Handle CLI --api-key as runtime override (not persisted)
|
|
901
916
|
if (parsedArgs.apiKey) {
|
|
902
917
|
if (!sessionOptions.model && !sessionOptions.modelPattern) {
|
|
@@ -939,6 +954,11 @@ export async function runRootCommand(
|
|
|
939
954
|
authStorage.setRuntimeApiKey(session.model.provider, parsedArgs.apiKey);
|
|
940
955
|
}
|
|
941
956
|
|
|
957
|
+
// Research-mode (RLM) preset: hard tool-boundary assertion after the registry is assembled.
|
|
958
|
+
if (deps.rlmPreset?.onSessionCreated) {
|
|
959
|
+
await deps.rlmPreset.onSessionCreated(session);
|
|
960
|
+
}
|
|
961
|
+
|
|
942
962
|
await applyStartupModelProfilesOrExit({
|
|
943
963
|
session,
|
|
944
964
|
settings: settingsInstance,
|
|
@@ -1038,13 +1058,18 @@ export async function runRootCommand(
|
|
|
1038
1058
|
messages: parsedArgs.messages,
|
|
1039
1059
|
initialMessage,
|
|
1040
1060
|
initialImages,
|
|
1061
|
+
suppressProcessExit: deps.suppressProcessExit,
|
|
1041
1062
|
});
|
|
1042
1063
|
if ($env.PI_TIMING) {
|
|
1043
1064
|
logger.printTimings();
|
|
1044
1065
|
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1066
|
+
if (!deps.suppressProcessExit) {
|
|
1067
|
+
await session.dispose();
|
|
1068
|
+
stopThemeWatcher();
|
|
1069
|
+
await postmortem.quit(0);
|
|
1070
|
+
} else {
|
|
1071
|
+
stopThemeWatcher();
|
|
1072
|
+
}
|
|
1048
1073
|
}
|
|
1049
1074
|
}
|
|
1050
1075
|
}
|
|
@@ -18,6 +18,7 @@ type ConfigurableEditorAction = Extract<
|
|
|
18
18
|
| "app.editor.external"
|
|
19
19
|
| "app.history.search"
|
|
20
20
|
| "app.message.dequeue"
|
|
21
|
+
| "app.message.queue"
|
|
21
22
|
| "app.clipboard.pasteImage"
|
|
22
23
|
| "app.clipboard.copyPrompt"
|
|
23
24
|
>;
|
|
@@ -36,6 +37,7 @@ const DEFAULT_ACTION_KEYS: Record<ConfigurableEditorAction, KeyId[]> = {
|
|
|
36
37
|
"app.thinking.toggle": ["ctrl+t"],
|
|
37
38
|
"app.editor.external": ["ctrl+g"],
|
|
38
39
|
"app.history.search": ["ctrl+r"],
|
|
40
|
+
"app.message.queue": ["alt+enter"],
|
|
39
41
|
"app.message.dequeue": ["alt+up"],
|
|
40
42
|
"app.clipboard.pasteImage": ["ctrl+v"],
|
|
41
43
|
"app.clipboard.copyPrompt": ["alt+shift+c"],
|
|
@@ -82,11 +84,13 @@ export class CustomEditor extends Editor {
|
|
|
82
84
|
onPastePendingInputCleared?: (reason: PastePendingClearReason, droppedInputCount: number) => void;
|
|
83
85
|
/** Called when the configured dequeue shortcut is pressed. */
|
|
84
86
|
onDequeue?: () => void;
|
|
87
|
+
/** Called when the configured queue shortcut is pressed. */
|
|
88
|
+
onQueue?: () => void;
|
|
85
89
|
/** Called when Caps Lock is pressed. */
|
|
86
90
|
onCapsLock?: () => void;
|
|
87
91
|
|
|
88
92
|
/** Custom key handlers from extensions and non-built-in app actions. */
|
|
89
|
-
#customKeyHandlers = new Map<KeyId, () =>
|
|
93
|
+
#customKeyHandlers = new Map<KeyId, () => boolean | undefined>();
|
|
90
94
|
#actionKeys = new Map<ConfigurableEditorAction, KeyId[]>(
|
|
91
95
|
Object.entries(DEFAULT_ACTION_KEYS).map(([action, keys]) => [action as ConfigurableEditorAction, [...keys]]),
|
|
92
96
|
);
|
|
@@ -112,7 +116,7 @@ export class CustomEditor extends Editor {
|
|
|
112
116
|
/**
|
|
113
117
|
* Register a custom key handler. Extensions use this for shortcuts.
|
|
114
118
|
*/
|
|
115
|
-
setCustomKeyHandler(key: KeyId, handler: () =>
|
|
119
|
+
setCustomKeyHandler(key: KeyId, handler: () => boolean | undefined): void {
|
|
116
120
|
this.#customKeyHandlers.set(key, handler);
|
|
117
121
|
}
|
|
118
122
|
|
|
@@ -328,6 +332,12 @@ export class CustomEditor extends Editor {
|
|
|
328
332
|
return;
|
|
329
333
|
}
|
|
330
334
|
|
|
335
|
+
// Intercept configured queue shortcut (send message after current turn)
|
|
336
|
+
if (this.#matchesAction(data, "app.message.queue") && this.onQueue) {
|
|
337
|
+
this.onQueue();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
331
341
|
// Intercept configured copy-prompt shortcut
|
|
332
342
|
if (this.#matchesAction(data, "app.clipboard.copyPrompt") && this.onCopyPrompt) {
|
|
333
343
|
this.onCopyPrompt();
|
|
@@ -343,8 +353,7 @@ export class CustomEditor extends Editor {
|
|
|
343
353
|
// Check custom key handlers (extensions)
|
|
344
354
|
for (const [keyId, handler] of this.#customKeyHandlers) {
|
|
345
355
|
if (matchesKey(data, keyId)) {
|
|
346
|
-
handler();
|
|
347
|
-
return;
|
|
356
|
+
if (handler() !== false) return;
|
|
348
357
|
}
|
|
349
358
|
}
|
|
350
359
|
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { Container, Input, matchesKey, Spacer, Text, TruncatedText } from "@gajae-code/tui";
|
|
2
|
+
import type { ModelProfileConfig } from "../../config/models-config-schema";
|
|
3
|
+
import { theme } from "../theme/theme";
|
|
4
|
+
import { matchesAppInterrupt } from "../utils/keybinding-matchers";
|
|
5
|
+
import { DynamicBorder } from "./dynamic-border";
|
|
6
|
+
|
|
7
|
+
const PROFILE_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
|
|
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
|
+
export interface CustomModelPresetWizardSubmit {
|
|
19
|
+
name: string;
|
|
20
|
+
profile: ModelProfileConfig;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class CustomModelPresetWizardComponent extends Container {
|
|
24
|
+
#contentContainer: Container;
|
|
25
|
+
#input: Input | null = null;
|
|
26
|
+
#step: WizardStep = "name";
|
|
27
|
+
#selectedIndex = 0;
|
|
28
|
+
#lastError: string | null = null;
|
|
29
|
+
#state: WizardState = {
|
|
30
|
+
name: "",
|
|
31
|
+
displayName: "",
|
|
32
|
+
provider: "",
|
|
33
|
+
model: "",
|
|
34
|
+
};
|
|
35
|
+
#onSubmit: (input: CustomModelPresetWizardSubmit) => void;
|
|
36
|
+
#onCancel: () => void;
|
|
37
|
+
#onRender: () => void;
|
|
38
|
+
|
|
39
|
+
constructor(
|
|
40
|
+
onSubmit: (input: CustomModelPresetWizardSubmit) => void,
|
|
41
|
+
onCancel: () => void,
|
|
42
|
+
onRender: () => void = () => {},
|
|
43
|
+
) {
|
|
44
|
+
super();
|
|
45
|
+
this.#onSubmit = onSubmit;
|
|
46
|
+
this.#onCancel = onCancel;
|
|
47
|
+
this.#onRender = onRender;
|
|
48
|
+
|
|
49
|
+
this.addChild(new DynamicBorder());
|
|
50
|
+
this.addChild(new Spacer(1));
|
|
51
|
+
this.addChild(new TruncatedText(theme.bold("Create custom model preset")));
|
|
52
|
+
this.addChild(
|
|
53
|
+
new TruncatedText(
|
|
54
|
+
theme.fg("muted", " Save provider/model as a selectable profile. Secrets are not requested."),
|
|
55
|
+
0,
|
|
56
|
+
0,
|
|
57
|
+
),
|
|
58
|
+
);
|
|
59
|
+
this.addChild(new Spacer(1));
|
|
60
|
+
this.#contentContainer = new Container();
|
|
61
|
+
this.addChild(this.#contentContainer);
|
|
62
|
+
this.addChild(new Spacer(1));
|
|
63
|
+
this.addChild(new DynamicBorder());
|
|
64
|
+
this.#renderStep();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
setSubmitError(error: string): void {
|
|
68
|
+
this.#lastError = error;
|
|
69
|
+
this.#renderStep();
|
|
70
|
+
this.#onRender();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
handleInput(keyData: string): void {
|
|
74
|
+
if (matchesAppInterrupt(keyData)) {
|
|
75
|
+
if (this.#step === "name") {
|
|
76
|
+
this.#onCancel();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
this.#goBack();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (this.#input) {
|
|
84
|
+
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
85
|
+
this.#saveInputAndProceed();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
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
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#renderStep(): void {
|
|
106
|
+
this.#contentContainer.clear();
|
|
107
|
+
this.#input = null;
|
|
108
|
+
switch (this.#step) {
|
|
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)));
|
|
149
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
150
|
+
if (this.#lastError) {
|
|
151
|
+
this.#contentContainer.addChild(new Text(theme.fg("error", this.#lastError), 0, 0));
|
|
152
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
153
|
+
}
|
|
154
|
+
this.#contentContainer.addChild(new Text(prompt, 0, 0));
|
|
155
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
156
|
+
this.#input = new Input();
|
|
157
|
+
this.#input.setValue(value);
|
|
158
|
+
this.#contentContainer.addChild(this.#input);
|
|
159
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
160
|
+
this.#addHelp(hint);
|
|
161
|
+
this.#addHelp("[Enter to continue, Esc to go back]");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
#renderConfirmStep(): void {
|
|
165
|
+
this.#contentContainer.addChild(new Text(theme.fg("accent", "Confirm custom preset")));
|
|
166
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
167
|
+
if (this.#lastError) {
|
|
168
|
+
this.#contentContainer.addChild(new Text(theme.fg("error", this.#lastError), 0, 0));
|
|
169
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
170
|
+
}
|
|
171
|
+
this.#contentContainer.addChild(new Text(`Preset id: ${this.#state.name}`, 0, 0));
|
|
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));
|
|
175
|
+
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
|
+
}
|
|
186
|
+
|
|
187
|
+
#addHelp(text: string): void {
|
|
188
|
+
this.#contentContainer.addChild(new Text(theme.fg("muted", text), 0, 0));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
#saveInputAndProceed(): void {
|
|
192
|
+
const value = this.#input?.getValue().trim() ?? "";
|
|
193
|
+
if (!value) {
|
|
194
|
+
this.#lastError = this.#emptyFieldMessage();
|
|
195
|
+
this.#renderStep();
|
|
196
|
+
this.#onRender();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const validationError = this.#validateCurrentInput(value);
|
|
200
|
+
if (validationError) {
|
|
201
|
+
this.#lastError = validationError;
|
|
202
|
+
this.#renderStep();
|
|
203
|
+
this.#onRender();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
this.#lastError = null;
|
|
207
|
+
if (this.#step === "name") {
|
|
208
|
+
this.#state.name = value;
|
|
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,
|
|
265
|
+
profile: {
|
|
266
|
+
required_providers: [this.#state.provider],
|
|
267
|
+
display_name: this.#state.displayName,
|
|
268
|
+
model_mapping: { default: this.#selector() },
|
|
269
|
+
},
|
|
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();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
@@ -518,7 +518,7 @@ export class HookSelectorComponent extends Container {
|
|
|
518
518
|
return;
|
|
519
519
|
}
|
|
520
520
|
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return")) {
|
|
521
|
-
this.#customInput?.onSubmit(editor.
|
|
521
|
+
this.#customInput?.onSubmit(editor.getExpandedText());
|
|
522
522
|
return;
|
|
523
523
|
}
|
|
524
524
|
editor.handleInput(keyData);
|
|
@@ -102,6 +102,9 @@ export type ModelSelectorSelection =
|
|
|
102
102
|
kind: "profile";
|
|
103
103
|
profileName: string;
|
|
104
104
|
setDefault: boolean;
|
|
105
|
+
}
|
|
106
|
+
| {
|
|
107
|
+
kind: "createProfile";
|
|
105
108
|
};
|
|
106
109
|
|
|
107
110
|
interface PendingThinkingChoice {
|
|
@@ -148,11 +151,15 @@ interface PresetProfileRow {
|
|
|
148
151
|
profile: ModelProfileDefinition;
|
|
149
152
|
}
|
|
150
153
|
|
|
154
|
+
interface PresetCreateRow {
|
|
155
|
+
kind: "create";
|
|
156
|
+
}
|
|
157
|
+
|
|
151
158
|
interface PresetBrowseRow {
|
|
152
159
|
kind: "browse";
|
|
153
160
|
}
|
|
154
161
|
|
|
155
|
-
type PresetLandingRow = PresetGroupRow | PresetProfileRow | PresetBrowseRow;
|
|
162
|
+
type PresetLandingRow = PresetGroupRow | PresetProfileRow | PresetCreateRow | PresetBrowseRow;
|
|
156
163
|
|
|
157
164
|
// Stable logical identity for a preset landing row, independent of its current
|
|
158
165
|
// list position. Used to relocate the cursor after the expanded group changes so
|
|
@@ -165,6 +172,8 @@ function presetRowIdentity(row: PresetLandingRow): string {
|
|
|
165
172
|
return `profile:${row.groupId}:${row.profile.name}`;
|
|
166
173
|
case "browse":
|
|
167
174
|
return "browse";
|
|
175
|
+
case "create":
|
|
176
|
+
return "create";
|
|
168
177
|
}
|
|
169
178
|
}
|
|
170
179
|
|
|
@@ -186,8 +195,9 @@ function profileRequiredProviders(profile: ModelProfileDefinition): string[] {
|
|
|
186
195
|
}
|
|
187
196
|
/**
|
|
188
197
|
* Component that renders a canonical model selector with provider tabs.
|
|
189
|
-
* -
|
|
190
|
-
* - Arrow
|
|
198
|
+
* - Preset landing Left/Right: Collapse/expand selected provider
|
|
199
|
+
* - Model browser Tab/Arrow Left/Right: Switch between provider tabs
|
|
200
|
+
* - Arrow Up/Down: Navigate rows
|
|
191
201
|
* - Enter: Open assignment actions for default plus GJC role-agent models
|
|
192
202
|
* - Escape: Close selector
|
|
193
203
|
*/
|
|
@@ -314,7 +324,6 @@ export class ModelSelectorComponent extends Container {
|
|
|
314
324
|
this.#viewMode = "models";
|
|
315
325
|
}
|
|
316
326
|
if (this.#viewMode === "presets") {
|
|
317
|
-
this.#updatePresetExpansion();
|
|
318
327
|
void this.#refreshProviderAuth();
|
|
319
328
|
this.#renderPresetLanding();
|
|
320
329
|
} else {
|
|
@@ -701,6 +710,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
701
710
|
for (const profile of profiles) rows.push({ kind: "profile", groupId, profile });
|
|
702
711
|
}
|
|
703
712
|
}
|
|
713
|
+
rows.push({ kind: "create" });
|
|
704
714
|
rows.push({ kind: "browse" });
|
|
705
715
|
return rows;
|
|
706
716
|
}
|
|
@@ -760,26 +770,36 @@ export class ModelSelectorComponent extends Container {
|
|
|
760
770
|
this.#tui.requestRender();
|
|
761
771
|
}
|
|
762
772
|
|
|
763
|
-
#
|
|
764
|
-
const selected = this.#getSelectedPresetRow();
|
|
765
|
-
if (selected?.kind === "group") this.#expandedPresetProviderId = selected.groupId;
|
|
766
|
-
if (selected?.kind === "profile") this.#expandedPresetProviderId = selected.groupId;
|
|
773
|
+
#clampPresetCursor(): void {
|
|
767
774
|
const rows = this.#getPresetRows();
|
|
768
|
-
// Expanding/collapsing a group shifts row positions. Relocate the cursor by
|
|
769
|
-
// the selected row's logical identity so crossing a provider group boundary
|
|
770
|
-
// keeps it on the same logical row instead of overshooting into the
|
|
771
|
-
// destination group's profiles (or off the end of the list).
|
|
772
|
-
if (selected) {
|
|
773
|
-
const targetIdentity = presetRowIdentity(selected);
|
|
774
|
-
const relocated = rows.findIndex(row => presetRowIdentity(row) === targetIdentity);
|
|
775
|
-
if (relocated >= 0) {
|
|
776
|
-
this.#presetCursor = relocated;
|
|
777
|
-
return;
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
775
|
this.#presetCursor = Math.min(this.#presetCursor, Math.max(0, rows.length - 1));
|
|
781
776
|
}
|
|
782
777
|
|
|
778
|
+
#relocatePresetCursor(targetIdentity: string): boolean {
|
|
779
|
+
const relocated = this.#getPresetRows().findIndex(row => presetRowIdentity(row) === targetIdentity);
|
|
780
|
+
if (relocated < 0) return false;
|
|
781
|
+
this.#presetCursor = relocated;
|
|
782
|
+
return true;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
#expandSelectedPresetProvider(): void {
|
|
786
|
+
const selected = this.#getSelectedPresetRow();
|
|
787
|
+
if (!selected || selected.kind === "browse" || selected.kind === "create") return;
|
|
788
|
+
if (this.#expandedPresetProviderId === selected.groupId) return;
|
|
789
|
+
const targetIdentity = presetRowIdentity(selected);
|
|
790
|
+
this.#expandedPresetProviderId = selected.groupId;
|
|
791
|
+
if (!this.#relocatePresetCursor(targetIdentity)) this.#clampPresetCursor();
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
#collapseSelectedPresetProvider(): void {
|
|
795
|
+
const selected = this.#getSelectedPresetRow();
|
|
796
|
+
if (!selected || selected.kind === "browse" || selected.kind === "create") return;
|
|
797
|
+
if (this.#expandedPresetProviderId !== selected.groupId) return;
|
|
798
|
+
const targetIdentity = selected.kind === "profile" ? `group:${selected.groupId}` : presetRowIdentity(selected);
|
|
799
|
+
this.#expandedPresetProviderId = undefined;
|
|
800
|
+
if (!this.#relocatePresetCursor(targetIdentity)) this.#clampPresetCursor();
|
|
801
|
+
}
|
|
802
|
+
|
|
783
803
|
#switchToModelMode(seed?: string): void {
|
|
784
804
|
this.#viewMode = "models";
|
|
785
805
|
this.#expandedPresetProviderId = undefined;
|
|
@@ -804,6 +824,11 @@ export class ModelSelectorComponent extends Container {
|
|
|
804
824
|
const row = rows[i];
|
|
805
825
|
const selected = i === this.#presetCursor;
|
|
806
826
|
const prefix = selected ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
|
|
827
|
+
if (row.kind === "create") {
|
|
828
|
+
const label = "Create custom preset";
|
|
829
|
+
this.#listContainer.addChild(new Text(`${prefix}${selected ? theme.fg("accent", label) : label}`, 0, 0));
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
807
832
|
if (row.kind === "browse") {
|
|
808
833
|
const label = "Browse all models";
|
|
809
834
|
this.#listContainer.addChild(new Text(`${prefix}${selected ? theme.fg("accent", label) : label}`, 0, 0));
|
|
@@ -817,7 +842,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
817
842
|
this.#listContainer.addChild(new Text(`${prefix}${renderedLabel}`, 0, 0));
|
|
818
843
|
continue;
|
|
819
844
|
}
|
|
820
|
-
const presentation = getModelProfilePresentation(row.profile
|
|
845
|
+
const presentation = getModelProfilePresentation(row.profile);
|
|
821
846
|
const authenticated = this.#isPresetAuthenticated(row.profile);
|
|
822
847
|
const mark = this.#providerAuthPending ? "…" : authenticated ? "✓" : "✗";
|
|
823
848
|
const label = ` ${mark} ${presentation.displayName}`;
|
|
@@ -835,11 +860,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
835
860
|
#renderPresetPreview(profile: ModelProfileDefinition): void {
|
|
836
861
|
this.#listContainer.addChild(new Spacer(1));
|
|
837
862
|
this.#listContainer.addChild(
|
|
838
|
-
new Text(
|
|
839
|
-
theme.fg("muted", ` Preset preview: ${getModelProfilePresentation(profile.name).displayName}`),
|
|
840
|
-
0,
|
|
841
|
-
0,
|
|
842
|
-
),
|
|
863
|
+
new Text(theme.fg("muted", ` Preset preview: ${getModelProfilePresentation(profile).displayName}`), 0, 0),
|
|
843
864
|
);
|
|
844
865
|
for (const role of PROFILE_ROLE_PREVIEW_ORDER) {
|
|
845
866
|
const selector = profile.modelMapping[role];
|
|
@@ -1160,7 +1181,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
1160
1181
|
this.#presetCursor = this.#presetCursor === 0 ? rows.length - 1 : this.#presetCursor - 1;
|
|
1161
1182
|
this.#previewProfileName = undefined;
|
|
1162
1183
|
this.#presetLoginHint = undefined;
|
|
1163
|
-
this.#
|
|
1184
|
+
this.#clampPresetCursor();
|
|
1164
1185
|
}
|
|
1165
1186
|
this.#renderPresetLanding();
|
|
1166
1187
|
return;
|
|
@@ -1174,11 +1195,29 @@ export class ModelSelectorComponent extends Container {
|
|
|
1174
1195
|
this.#presetCursor = (this.#presetCursor + 1) % rows.length;
|
|
1175
1196
|
this.#previewProfileName = undefined;
|
|
1176
1197
|
this.#presetLoginHint = undefined;
|
|
1177
|
-
this.#
|
|
1198
|
+
this.#clampPresetCursor();
|
|
1178
1199
|
}
|
|
1179
1200
|
this.#renderPresetLanding();
|
|
1180
1201
|
return;
|
|
1181
1202
|
}
|
|
1203
|
+
if (matchesKey(keyData, "right")) {
|
|
1204
|
+
if (!this.#presetScopeMenuOpen) {
|
|
1205
|
+
this.#expandSelectedPresetProvider();
|
|
1206
|
+
this.#previewProfileName = undefined;
|
|
1207
|
+
this.#presetLoginHint = undefined;
|
|
1208
|
+
this.#renderPresetLanding();
|
|
1209
|
+
}
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
if (matchesKey(keyData, "left")) {
|
|
1213
|
+
if (!this.#presetScopeMenuOpen) {
|
|
1214
|
+
this.#collapseSelectedPresetProvider();
|
|
1215
|
+
this.#previewProfileName = undefined;
|
|
1216
|
+
this.#presetLoginHint = undefined;
|
|
1217
|
+
this.#renderPresetLanding();
|
|
1218
|
+
}
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1182
1221
|
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
1183
1222
|
this.#handlePresetEnter();
|
|
1184
1223
|
return;
|
|
@@ -1196,7 +1235,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
1196
1235
|
}
|
|
1197
1236
|
if (this.#expandedPresetProviderId) {
|
|
1198
1237
|
this.#expandedPresetProviderId = undefined;
|
|
1199
|
-
this.#
|
|
1238
|
+
this.#clampPresetCursor();
|
|
1200
1239
|
this.#renderPresetLanding();
|
|
1201
1240
|
return;
|
|
1202
1241
|
}
|
|
@@ -1221,6 +1260,10 @@ export class ModelSelectorComponent extends Container {
|
|
|
1221
1260
|
}
|
|
1222
1261
|
const row = this.#getSelectedPresetRow();
|
|
1223
1262
|
if (!row) return;
|
|
1263
|
+
if (row.kind === "create") {
|
|
1264
|
+
this.#onSelectCallback({ kind: "createProfile" });
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1224
1267
|
if (row.kind === "browse") {
|
|
1225
1268
|
this.#switchToModelMode();
|
|
1226
1269
|
return;
|