@gajae-code/coding-agent 0.3.0 → 0.3.2
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 +32 -0
- package/README.md +1 -1
- package/dist/types/async/job-manager.d.ts +7 -0
- package/dist/types/cli/args.d.ts +3 -1
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/commands/launch.d.ts +6 -0
- package/dist/types/config/keybindings.d.ts +5 -0
- package/dist/types/config/model-profile-activation.d.ts +30 -0
- package/dist/types/config/model-profiles.d.ts +19 -0
- package/dist/types/config/model-registry.d.ts +8 -0
- package/dist/types/config/model-resolver.d.ts +1 -1
- package/dist/types/config/models-config-schema.d.ts +47 -0
- package/dist/types/config/settings-schema.d.ts +14 -4
- package/dist/types/debug/crash-diagnostics.d.ts +45 -0
- package/dist/types/debug/runtime-gauges.d.ts +6 -0
- package/dist/types/deep-interview/render-middleware.d.ts +1 -0
- package/dist/types/eval/py/executor.d.ts +2 -0
- package/dist/types/eval/py/kernel.d.ts +2 -0
- package/dist/types/exec/bash-executor.d.ts +10 -0
- package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
- package/dist/types/gjc-runtime/state-migrations.d.ts +9 -0
- package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +10 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -1
- package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
- package/dist/types/harness-control-plane/control-endpoint.d.ts +3 -2
- package/dist/types/hooks/skill-state.d.ts +21 -0
- package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
- package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
- package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
- package/dist/types/internal-urls/types.d.ts +4 -0
- package/dist/types/lsp/index.d.ts +10 -10
- package/dist/types/main.d.ts +10 -1
- package/dist/types/modes/bridge/auth.d.ts +12 -0
- package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
- package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
- package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
- package/dist/types/modes/bridge/event-stream.d.ts +8 -0
- package/dist/types/modes/components/custom-editor.d.ts +6 -0
- package/dist/types/modes/components/custom-provider-wizard.d.ts +10 -0
- package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
- package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
- package/dist/types/modes/components/model-selector.d.ts +6 -1
- package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
- package/dist/types/modes/components/status-line/types.d.ts +2 -0
- package/dist/types/modes/components/status-line.d.ts +2 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +9 -0
- package/dist/types/modes/index.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/jobs-observer.d.ts +57 -0
- package/dist/types/modes/rpc/host-tools.d.ts +1 -16
- package/dist/types/modes/rpc/host-uris.d.ts +1 -38
- package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
- package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
- package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
- package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
- package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
- package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
- package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
- package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
- package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
- package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk.d.ts +3 -1
- package/dist/types/session/agent-session.d.ts +11 -1
- package/dist/types/skill-state/workflow-state-contract.d.ts +1 -2
- package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/id.d.ts +7 -0
- package/dist/types/task/index.d.ts +5 -0
- package/dist/types/task/receipt.d.ts +85 -0
- package/dist/types/task/spawn-gate.d.ts +38 -0
- package/dist/types/task/types.d.ts +143 -11
- package/dist/types/tools/cron.d.ts +6 -0
- package/dist/types/tools/hindsight-recall.d.ts +0 -2
- package/dist/types/tools/hindsight-reflect.d.ts +0 -2
- package/dist/types/tools/hindsight-retain.d.ts +0 -2
- package/dist/types/tools/index.d.ts +6 -4
- package/dist/types/tools/path-utils.d.ts +1 -0
- package/dist/types/tools/subagent.d.ts +15 -0
- package/package.json +7 -7
- package/scripts/build-binary.ts +7 -0
- package/src/async/job-manager.ts +36 -0
- package/src/cli/args.ts +19 -2
- package/src/commands/deep-interview.ts +1 -0
- package/src/commands/harness.ts +289 -19
- package/src/commands/launch.ts +10 -2
- package/src/commands/state.ts +2 -1
- package/src/commands/team.ts +22 -4
- package/src/config/keybindings.ts +6 -0
- package/src/config/model-profile-activation.ts +157 -0
- package/src/config/model-profiles.ts +155 -0
- package/src/config/model-registry.ts +19 -0
- package/src/config/model-resolver.ts +3 -2
- package/src/config/models-config-schema.ts +36 -0
- package/src/config/settings-schema.ts +16 -3
- package/src/dap/client.ts +17 -3
- package/src/debug/crash-diagnostics.ts +223 -0
- package/src/debug/runtime-gauges.ts +20 -0
- package/src/deep-interview/render-middleware.ts +6 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +39 -3
- package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +61 -0
- package/src/defaults/gjc-defaults.ts +7 -0
- package/src/eval/py/executor.ts +21 -1
- package/src/eval/py/kernel.ts +15 -0
- package/src/exec/bash-executor.ts +41 -0
- package/src/gjc-runtime/cli-write-receipt.ts +31 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +69 -32
- package/src/gjc-runtime/ralplan-runtime.ts +213 -36
- package/src/gjc-runtime/state-migrations.ts +54 -7
- package/src/gjc-runtime/state-runtime.ts +461 -64
- package/src/gjc-runtime/state-schema.ts +192 -0
- package/src/gjc-runtime/state-writer.ts +32 -1
- package/src/gjc-runtime/team-runtime.ts +177 -105
- package/src/gjc-runtime/ultragoal-runtime.ts +231 -38
- package/src/gjc-runtime/workflow-command-ref.ts +239 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +108 -4
- package/src/gjc-runtime/workflow-manifest.ts +3 -1
- package/src/harness-control-plane/control-endpoint.ts +19 -8
- package/src/harness-control-plane/owner.ts +57 -10
- package/src/harness-control-plane/state-machine.ts +2 -1
- package/src/hooks/skill-state.ts +176 -26
- package/src/internal-urls/agent-protocol.ts +68 -21
- package/src/internal-urls/artifact-protocol.ts +12 -17
- package/src/internal-urls/docs-index.generated.ts +8 -10
- package/src/internal-urls/registry-helpers.ts +19 -16
- package/src/internal-urls/types.ts +4 -0
- package/src/lsp/client.ts +18 -2
- package/src/main.ts +88 -6
- package/src/modes/bridge/auth.ts +41 -0
- package/src/modes/bridge/bridge-client-bridge.ts +47 -0
- package/src/modes/bridge/bridge-mode.ts +520 -0
- package/src/modes/bridge/bridge-ui-context.ts +200 -0
- package/src/modes/bridge/event-stream.ts +70 -0
- package/src/modes/components/custom-editor.ts +101 -0
- package/src/modes/components/custom-provider-wizard.ts +318 -0
- package/src/modes/components/hook-selector.ts +61 -18
- package/src/modes/components/jobs-overlay-model.ts +109 -0
- package/src/modes/components/jobs-overlay.ts +172 -0
- package/src/modes/components/model-selector.ts +108 -18
- package/src/modes/components/provider-onboarding-selector.ts +6 -1
- package/src/modes/components/status-line/presets.ts +7 -5
- package/src/modes/components/status-line/segments.ts +25 -0
- package/src/modes/components/status-line/types.ts +2 -0
- package/src/modes/components/status-line.ts +9 -1
- package/src/modes/controllers/extension-ui-controller.ts +39 -3
- package/src/modes/controllers/input-controller.ts +97 -9
- package/src/modes/controllers/selector-controller.ts +86 -1
- package/src/modes/index.ts +1 -0
- package/src/modes/interactive-mode.ts +27 -0
- package/src/modes/jobs-observer.ts +204 -0
- package/src/modes/rpc/host-tools.ts +1 -186
- package/src/modes/rpc/host-uris.ts +1 -235
- package/src/modes/rpc/rpc-client.ts +25 -10
- package/src/modes/rpc/rpc-mode.ts +12 -381
- package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
- package/src/modes/shared/agent-wire/command-validation.ts +131 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
- package/src/modes/shared/agent-wire/handshake.ts +117 -0
- package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
- package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
- package/src/modes/shared/agent-wire/protocol.ts +96 -0
- package/src/modes/shared/agent-wire/responses.ts +17 -0
- package/src/modes/shared/agent-wire/scopes.ts +89 -0
- package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
- package/src/modes/shared/agent-wire/ui-result.ts +48 -0
- package/src/modes/types.ts +2 -0
- package/src/prompts/memories/consolidation.md +1 -1
- package/src/prompts/memories/read-path.md +6 -7
- package/src/prompts/memories/unavailable.md +2 -2
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/irc.md +1 -1
- package/src/prompts/tools/read.md +2 -2
- package/src/prompts/tools/recall.md +1 -0
- package/src/prompts/tools/reflect.md +1 -0
- package/src/prompts/tools/retain.md +1 -0
- package/src/prompts/tools/subagent.md +12 -7
- package/src/prompts/tools/task-summary.md +3 -9
- package/src/prompts/tools/task.md +5 -1
- package/src/sdk.ts +5 -1
- package/src/session/agent-session.ts +214 -38
- package/src/skill-state/deep-interview-mutation-guard.ts +23 -4
- package/src/skill-state/workflow-state-contract.ts +7 -4
- package/src/skill-state/workflow-state-version.ts +3 -0
- package/src/slash-commands/builtin-registry.ts +9 -1
- package/src/task/executor.ts +31 -5
- package/src/task/id.ts +33 -0
- package/src/task/index.ts +259 -67
- package/src/task/output-manager.ts +5 -4
- package/src/task/receipt.ts +297 -0
- package/src/task/render.ts +48 -131
- package/src/task/spawn-gate.ts +132 -0
- package/src/task/types.ts +48 -7
- package/src/tools/ask.ts +73 -33
- package/src/tools/ast-edit.ts +1 -0
- package/src/tools/ast-grep.ts +1 -0
- package/src/tools/bash.ts +1 -1
- package/src/tools/cron.ts +48 -0
- package/src/tools/find.ts +4 -1
- package/src/tools/hindsight-recall.ts +0 -2
- package/src/tools/hindsight-reflect.ts +0 -2
- package/src/tools/hindsight-retain.ts +0 -2
- package/src/tools/index.ts +6 -18
- package/src/tools/path-utils.ts +3 -2
- package/src/tools/read.ts +4 -3
- package/src/tools/search.ts +1 -0
- package/src/tools/skill.ts +6 -1
- package/src/tools/subagent.ts +237 -84
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { Container, Input, matchesKey, Spacer, Text, TruncatedText } from "@gajae-code/tui";
|
|
2
|
+
import type { ProviderCompatibility, ProviderSetupInput } from "../../setup/provider-onboarding";
|
|
3
|
+
import { theme } from "../theme/theme";
|
|
4
|
+
import { matchesAppInterrupt } from "../utils/keybinding-matchers";
|
|
5
|
+
import { DynamicBorder } from "./dynamic-border";
|
|
6
|
+
|
|
7
|
+
export type CustomProviderCredentialSource = "env" | "literal";
|
|
8
|
+
|
|
9
|
+
type WizardStep =
|
|
10
|
+
| "compatibility"
|
|
11
|
+
| "provider-id"
|
|
12
|
+
| "base-url"
|
|
13
|
+
| "credential-source"
|
|
14
|
+
| "credential"
|
|
15
|
+
| "models"
|
|
16
|
+
| "confirm"
|
|
17
|
+
| "force-confirm";
|
|
18
|
+
|
|
19
|
+
interface WizardState {
|
|
20
|
+
compatibility: ProviderCompatibility;
|
|
21
|
+
providerId: string;
|
|
22
|
+
baseUrl: string;
|
|
23
|
+
credentialSource: CustomProviderCredentialSource;
|
|
24
|
+
credential: string;
|
|
25
|
+
models: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type CustomProviderWizardSubmit = ProviderSetupInput;
|
|
29
|
+
|
|
30
|
+
export class CustomProviderWizardComponent extends Container {
|
|
31
|
+
#contentContainer: Container;
|
|
32
|
+
#input: Input | null = null;
|
|
33
|
+
#step: WizardStep = "compatibility";
|
|
34
|
+
#selectedIndex = 0;
|
|
35
|
+
#lastSubmitError: string | null = null;
|
|
36
|
+
#state: WizardState = {
|
|
37
|
+
compatibility: "openai",
|
|
38
|
+
providerId: "",
|
|
39
|
+
baseUrl: "",
|
|
40
|
+
credentialSource: "env",
|
|
41
|
+
credential: "",
|
|
42
|
+
models: "",
|
|
43
|
+
};
|
|
44
|
+
#onSubmit: (input: CustomProviderWizardSubmit) => void;
|
|
45
|
+
#onCancel: () => void;
|
|
46
|
+
#onRender: () => void;
|
|
47
|
+
|
|
48
|
+
constructor(
|
|
49
|
+
onSubmit: (input: CustomProviderWizardSubmit) => void,
|
|
50
|
+
onCancel: () => void,
|
|
51
|
+
onRender: () => void = () => {},
|
|
52
|
+
) {
|
|
53
|
+
super();
|
|
54
|
+
this.#onSubmit = onSubmit;
|
|
55
|
+
this.#onCancel = onCancel;
|
|
56
|
+
this.#onRender = onRender;
|
|
57
|
+
|
|
58
|
+
this.addChild(new DynamicBorder());
|
|
59
|
+
this.addChild(new Spacer(1));
|
|
60
|
+
this.addChild(new TruncatedText(theme.bold("Add custom provider")));
|
|
61
|
+
this.addChild(
|
|
62
|
+
new TruncatedText(theme.fg("muted", " Configure an OpenAI- or Anthropic-compatible API provider."), 0, 0),
|
|
63
|
+
);
|
|
64
|
+
this.addChild(new Spacer(1));
|
|
65
|
+
this.#contentContainer = new Container();
|
|
66
|
+
this.addChild(this.#contentContainer);
|
|
67
|
+
this.addChild(new Spacer(1));
|
|
68
|
+
this.addChild(new DynamicBorder());
|
|
69
|
+
this.#renderStep();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
setSubmitError(error: string): void {
|
|
73
|
+
this.#lastSubmitError = error;
|
|
74
|
+
if (error.includes("already exists")) {
|
|
75
|
+
this.#step = "force-confirm";
|
|
76
|
+
this.#selectedIndex = 1;
|
|
77
|
+
}
|
|
78
|
+
this.#renderStep();
|
|
79
|
+
this.#onRender();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
handleInput(keyData: string): void {
|
|
83
|
+
if (matchesAppInterrupt(keyData)) {
|
|
84
|
+
if (this.#step === "compatibility") {
|
|
85
|
+
this.#onCancel();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
this.#goBack();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (this.#input) {
|
|
93
|
+
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
94
|
+
this.#saveInputAndProceed();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this.#input.handleInput(keyData);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (matchesKey(keyData, "up")) {
|
|
102
|
+
this.#moveSelection(-1);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (matchesKey(keyData, "down")) {
|
|
106
|
+
this.#moveSelection(1);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
110
|
+
this.#selectCurrentOption();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#renderStep(): void {
|
|
115
|
+
this.#contentContainer.clear();
|
|
116
|
+
this.#input = null;
|
|
117
|
+
switch (this.#step) {
|
|
118
|
+
case "compatibility":
|
|
119
|
+
this.#renderCompatibilityStep();
|
|
120
|
+
break;
|
|
121
|
+
case "provider-id":
|
|
122
|
+
this.#renderInputStep(
|
|
123
|
+
"Step 2: Provider id",
|
|
124
|
+
"Enter a provider id:",
|
|
125
|
+
this.#state.providerId,
|
|
126
|
+
"e.g. my-openai-proxy",
|
|
127
|
+
);
|
|
128
|
+
break;
|
|
129
|
+
case "base-url":
|
|
130
|
+
this.#renderInputStep(
|
|
131
|
+
"Step 3: Base URL",
|
|
132
|
+
"Enter the API base URL:",
|
|
133
|
+
this.#state.baseUrl,
|
|
134
|
+
"e.g. https://api.example.com/v1",
|
|
135
|
+
);
|
|
136
|
+
break;
|
|
137
|
+
case "credential-source":
|
|
138
|
+
this.#renderCredentialSourceStep();
|
|
139
|
+
break;
|
|
140
|
+
case "credential":
|
|
141
|
+
this.#renderInputStep(
|
|
142
|
+
"Step 5: Credential",
|
|
143
|
+
this.#state.credentialSource === "env"
|
|
144
|
+
? "Enter the API key environment variable name:"
|
|
145
|
+
: "Paste the API key:",
|
|
146
|
+
this.#state.credential,
|
|
147
|
+
this.#state.credentialSource === "env"
|
|
148
|
+
? "e.g. OPENAI_API_KEY"
|
|
149
|
+
: "The key will be stored in models.yml and redacted in output.",
|
|
150
|
+
);
|
|
151
|
+
break;
|
|
152
|
+
case "models":
|
|
153
|
+
this.#renderInputStep(
|
|
154
|
+
"Step 6: Model id(s)",
|
|
155
|
+
"Enter model ids, comma-separated:",
|
|
156
|
+
this.#state.models,
|
|
157
|
+
"e.g. gpt-5, claude-sonnet-4-5",
|
|
158
|
+
);
|
|
159
|
+
break;
|
|
160
|
+
case "confirm":
|
|
161
|
+
this.#renderConfirmStep(false);
|
|
162
|
+
break;
|
|
163
|
+
case "force-confirm":
|
|
164
|
+
this.#renderConfirmStep(true);
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#renderCompatibilityStep(): void {
|
|
170
|
+
this.#contentContainer.addChild(new Text(theme.fg("accent", "Step 1: Compatibility")));
|
|
171
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
172
|
+
const options: Array<{ value: ProviderCompatibility; label: string }> = [
|
|
173
|
+
{ value: "openai", label: "OpenAI-compatible" },
|
|
174
|
+
{ value: "anthropic", label: "Anthropic-compatible" },
|
|
175
|
+
];
|
|
176
|
+
for (let i = 0; i < options.length; i++) this.#addOption(i, options[i]?.label ?? "");
|
|
177
|
+
this.#addHelp("[↑↓ to navigate, Enter to select, Esc to cancel]");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#renderCredentialSourceStep(): void {
|
|
181
|
+
this.#contentContainer.addChild(new Text(theme.fg("accent", "Step 4: Credential source")));
|
|
182
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
183
|
+
this.#addOption(0, "Environment variable");
|
|
184
|
+
this.#addOption(1, "Paste API key");
|
|
185
|
+
this.#addHelp("[↑↓ to navigate, Enter to select, Esc to go back]");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
#renderInputStep(title: string, prompt: string, value: string, hint: string): void {
|
|
189
|
+
this.#contentContainer.addChild(new Text(theme.fg("accent", title)));
|
|
190
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
191
|
+
this.#contentContainer.addChild(new Text(prompt, 0, 0));
|
|
192
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
193
|
+
this.#input = new Input();
|
|
194
|
+
this.#input.setValue(value);
|
|
195
|
+
this.#contentContainer.addChild(this.#input);
|
|
196
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
197
|
+
this.#addHelp(hint);
|
|
198
|
+
this.#addHelp("[Enter to continue, Esc to go back]");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#renderConfirmStep(force: boolean): void {
|
|
202
|
+
this.#contentContainer.addChild(
|
|
203
|
+
new Text(theme.fg("accent", force ? "Provider exists — replace it?" : "Confirm custom provider")),
|
|
204
|
+
);
|
|
205
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
206
|
+
if (this.#lastSubmitError) {
|
|
207
|
+
this.#contentContainer.addChild(new Text(theme.fg(force ? "warning" : "error", this.#lastSubmitError), 0, 0));
|
|
208
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
209
|
+
}
|
|
210
|
+
this.#contentContainer.addChild(new Text(`Compatibility: ${this.#state.compatibility}`, 0, 0));
|
|
211
|
+
this.#contentContainer.addChild(new Text(`Provider: ${this.#state.providerId}`, 0, 0));
|
|
212
|
+
this.#contentContainer.addChild(new Text(`Base URL: ${this.#state.baseUrl}`, 0, 0));
|
|
213
|
+
this.#contentContainer.addChild(
|
|
214
|
+
new Text(
|
|
215
|
+
`Credential: ${this.#state.credentialSource === "env" ? this.#state.credential : "pasted API key"}`,
|
|
216
|
+
0,
|
|
217
|
+
0,
|
|
218
|
+
),
|
|
219
|
+
);
|
|
220
|
+
this.#contentContainer.addChild(new Text(`Models: ${this.#state.models}`, 0, 0));
|
|
221
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
222
|
+
this.#addOption(0, force ? "Replace existing provider" : "Add provider");
|
|
223
|
+
this.#addOption(1, "Go back");
|
|
224
|
+
this.#addHelp("[↑↓ to navigate, Enter to select, Esc to go back]");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
#addOption(index: number, label: string): void {
|
|
228
|
+
const selected = index === this.#selectedIndex;
|
|
229
|
+
const prefix = selected ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
|
|
230
|
+
this.#contentContainer.addChild(new Text(`${prefix}${selected ? theme.fg("accent", label) : label}`, 0, 0));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#addHelp(text: string): void {
|
|
234
|
+
this.#contentContainer.addChild(new Text(theme.fg("muted", text), 0, 0));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
#saveInputAndProceed(): void {
|
|
238
|
+
const value = this.#input?.getValue().trim() ?? "";
|
|
239
|
+
if (!value) return;
|
|
240
|
+
if (this.#step === "provider-id") {
|
|
241
|
+
this.#state.providerId = value;
|
|
242
|
+
this.#step = "base-url";
|
|
243
|
+
} else if (this.#step === "base-url") {
|
|
244
|
+
this.#state.baseUrl = value;
|
|
245
|
+
this.#step = "credential-source";
|
|
246
|
+
this.#selectedIndex = 0;
|
|
247
|
+
} else if (this.#step === "credential") {
|
|
248
|
+
this.#state.credential = value;
|
|
249
|
+
this.#step = "models";
|
|
250
|
+
} else if (this.#step === "models") {
|
|
251
|
+
this.#state.models = value;
|
|
252
|
+
this.#step = "confirm";
|
|
253
|
+
this.#selectedIndex = 0;
|
|
254
|
+
this.#lastSubmitError = null;
|
|
255
|
+
}
|
|
256
|
+
this.#renderStep();
|
|
257
|
+
this.#onRender();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
#selectCurrentOption(): void {
|
|
261
|
+
if (this.#step === "compatibility") {
|
|
262
|
+
this.#state.compatibility = this.#selectedIndex === 0 ? "openai" : "anthropic";
|
|
263
|
+
this.#step = "provider-id";
|
|
264
|
+
} else if (this.#step === "credential-source") {
|
|
265
|
+
this.#state.credentialSource = this.#selectedIndex === 0 ? "env" : "literal";
|
|
266
|
+
this.#state.credential = "";
|
|
267
|
+
this.#step = "credential";
|
|
268
|
+
} else if (this.#step === "confirm" || this.#step === "force-confirm") {
|
|
269
|
+
if (this.#selectedIndex === 0) {
|
|
270
|
+
this.#onSubmit(this.#buildInput(this.#step === "force-confirm"));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
this.#step = "models";
|
|
274
|
+
}
|
|
275
|
+
this.#renderStep();
|
|
276
|
+
this.#onRender();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
#buildInput(force: boolean): CustomProviderWizardSubmit {
|
|
280
|
+
return {
|
|
281
|
+
compatibility: this.#state.compatibility,
|
|
282
|
+
providerId: this.#state.providerId,
|
|
283
|
+
baseUrl: this.#state.baseUrl,
|
|
284
|
+
apiKeyEnv: this.#state.credentialSource === "env" ? this.#state.credential : undefined,
|
|
285
|
+
apiKey: this.#state.credentialSource === "literal" ? this.#state.credential : undefined,
|
|
286
|
+
models: this.#state.models
|
|
287
|
+
.split(",")
|
|
288
|
+
.map(model => model.trim())
|
|
289
|
+
.filter(Boolean),
|
|
290
|
+
force,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
#moveSelection(delta: number): void {
|
|
295
|
+
const maxIndex =
|
|
296
|
+
this.#step === "confirm" ||
|
|
297
|
+
this.#step === "force-confirm" ||
|
|
298
|
+
this.#step === "compatibility" ||
|
|
299
|
+
this.#step === "credential-source"
|
|
300
|
+
? 1
|
|
301
|
+
: 0;
|
|
302
|
+
this.#selectedIndex = (this.#selectedIndex + delta + maxIndex + 1) % (maxIndex + 1);
|
|
303
|
+
this.#renderStep();
|
|
304
|
+
this.#onRender();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
#goBack(): void {
|
|
308
|
+
if (this.#step === "provider-id") this.#step = "compatibility";
|
|
309
|
+
else if (this.#step === "base-url") this.#step = "provider-id";
|
|
310
|
+
else if (this.#step === "credential-source") this.#step = "base-url";
|
|
311
|
+
else if (this.#step === "credential") this.#step = "credential-source";
|
|
312
|
+
else if (this.#step === "models") this.#step = "credential";
|
|
313
|
+
else if (this.#step === "confirm" || this.#step === "force-confirm") this.#step = "models";
|
|
314
|
+
this.#selectedIndex = 0;
|
|
315
|
+
this.#renderStep();
|
|
316
|
+
this.#onRender();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
@@ -21,6 +21,22 @@ import { matchesAppExternalEditor, matchesSelectCancel } from "../../modes/utils
|
|
|
21
21
|
import { CountdownTimer } from "./countdown-timer";
|
|
22
22
|
import { DynamicBorder } from "./dynamic-border";
|
|
23
23
|
|
|
24
|
+
const SGR_MOUSE_PRESS_PATTERN = /^\x1b\[<(\d+);\d+;\d+M$/;
|
|
25
|
+
const MOUSE_WHEEL_TITLE_SCROLL_ROWS = 3;
|
|
26
|
+
|
|
27
|
+
function getMouseWheelTitleScrollRows(keyData: string): number {
|
|
28
|
+
const match = SGR_MOUSE_PRESS_PATTERN.exec(keyData);
|
|
29
|
+
if (!match) return 0;
|
|
30
|
+
|
|
31
|
+
const button = Number.parseInt(match[1] ?? "", 10);
|
|
32
|
+
if (!Number.isFinite(button) || (button & 64) === 0) return 0;
|
|
33
|
+
|
|
34
|
+
const wheelDirection = button & 3;
|
|
35
|
+
if (wheelDirection === 0) return -MOUSE_WHEEL_TITLE_SCROLL_ROWS;
|
|
36
|
+
if (wheelDirection === 1) return MOUSE_WHEEL_TITLE_SCROLL_ROWS;
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
24
40
|
export interface HookSelectorOptions {
|
|
25
41
|
tui?: TUI;
|
|
26
42
|
timeout?: number;
|
|
@@ -125,11 +141,10 @@ class ScrollableTitle extends Container {
|
|
|
125
141
|
* `maxVisibleRows`; everything that depends on terminal width is recomputed
|
|
126
142
|
* on each render so resize Just Works.
|
|
127
143
|
*
|
|
128
|
-
* `maxVisibleRows` is a
|
|
129
|
-
* options shrink first
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* fully with zero siblings.
|
|
144
|
+
* `maxVisibleRows` is a hard viewport budget for every rendered option-list
|
|
145
|
+
* row. Surrounding options shrink first; if the focused option alone would
|
|
146
|
+
* exceed the remaining budget, it is compacted to contextual rows plus an
|
|
147
|
+
* omitted-rows marker so controls stay reachable for untrusted long labels.
|
|
133
148
|
*/
|
|
134
149
|
class FocusAwareList extends Container {
|
|
135
150
|
#options: string[] = [];
|
|
@@ -168,19 +183,20 @@ class FocusAwareList extends Container {
|
|
|
168
183
|
theme.fg("accent", t),
|
|
169
184
|
);
|
|
170
185
|
const focusedWrappedSegments = wrapTextWithAnsi(focusedLabel, availableLabelWidth);
|
|
171
|
-
const focusedRows = Math.max(1, focusedWrappedSegments.length);
|
|
172
186
|
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
187
|
+
// Reserve one row for the option position marker only when the focused
|
|
188
|
+
// block itself must be compacted. Moderate focused labels keep the legacy
|
|
189
|
+
// wrap-focused behavior and spend the full viewport on label context.
|
|
176
190
|
const totalOptions = this.#options.length;
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
const
|
|
191
|
+
const mustCompactFocused = focusedWrappedSegments.length > this.#maxVisibleRows;
|
|
192
|
+
const positionMarkerSlot = mustCompactFocused && totalOptions > 1 ? 1 : 0;
|
|
193
|
+
const focusedBudget = Math.max(1, this.#maxVisibleRows - positionMarkerSlot);
|
|
194
|
+
const focusedSegments = this.#capFocusedSegments(focusedWrappedSegments, focusedBudget, availableLabelWidth);
|
|
195
|
+
const focusedRows = Math.max(1, focusedSegments.length);
|
|
180
196
|
|
|
181
|
-
// Sibling budget. If the focused block
|
|
182
|
-
//
|
|
183
|
-
const siblingBudget = Math.max(0, this.#maxVisibleRows - focusedRows -
|
|
197
|
+
// Sibling budget. If the focused block consumes the available viewport,
|
|
198
|
+
// render it with zero siblings and the reserved position marker.
|
|
199
|
+
const siblingBudget = Math.max(0, this.#maxVisibleRows - focusedRows - positionMarkerSlot);
|
|
184
200
|
|
|
185
201
|
// Distribute sibling slots around focus, preferring closest options.
|
|
186
202
|
const availableAbove = this.#selectedIndex;
|
|
@@ -203,8 +219,8 @@ class FocusAwareList extends Container {
|
|
|
203
219
|
if (i === this.#selectedIndex) {
|
|
204
220
|
// Emit focused wrapped rows. Cursor only on row 0; continuation
|
|
205
221
|
// rows are whitespace-aligned under the label start.
|
|
206
|
-
for (let r = 0; r <
|
|
207
|
-
const segment =
|
|
222
|
+
for (let r = 0; r < focusedSegments.length; r++) {
|
|
223
|
+
const segment = focusedSegments[r] ?? "";
|
|
208
224
|
rows.push(r === 0 ? styledSelectedPrefix + segment : continuationPrefix + segment);
|
|
209
225
|
}
|
|
210
226
|
} else {
|
|
@@ -217,13 +233,33 @@ class FocusAwareList extends Container {
|
|
|
217
233
|
}
|
|
218
234
|
}
|
|
219
235
|
|
|
220
|
-
if (showMarker) {
|
|
236
|
+
if (showMarker && rows.length < this.#maxVisibleRows) {
|
|
221
237
|
rows.push(theme.fg("dim", ` (${this.#selectedIndex + 1}/${totalOptions})`));
|
|
222
238
|
}
|
|
223
239
|
|
|
224
240
|
return this.#outline ? this.#wrapOutline(rows, width) : rows;
|
|
225
241
|
}
|
|
226
242
|
|
|
243
|
+
#capFocusedSegments(segments: string[], maxRows: number, availableLabelWidth: number): string[] {
|
|
244
|
+
const rows = segments.length > 0 ? segments : [""];
|
|
245
|
+
const budget = Math.max(1, Math.floor(maxRows));
|
|
246
|
+
if (rows.length <= budget) return rows;
|
|
247
|
+
|
|
248
|
+
if (budget === 1) {
|
|
249
|
+
return [truncateToWidth(`… ${rows.length - 1} wrapped rows omitted …`, availableLabelWidth)];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (budget === 2) {
|
|
253
|
+
return [rows[0] ?? "", truncateToWidth(`… ${rows.length - 1} wrapped rows omitted …`, availableLabelWidth)];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const tailRows = Math.max(1, Math.floor((budget - 2) / 2));
|
|
257
|
+
const headRows = Math.max(1, budget - 1 - tailRows);
|
|
258
|
+
const omittedRows = Math.max(1, rows.length - headRows - tailRows);
|
|
259
|
+
const marker = truncateToWidth(`… ${omittedRows} wrapped rows omitted …`, availableLabelWidth);
|
|
260
|
+
return [...rows.slice(0, headRows), marker, ...rows.slice(rows.length - tailRows)];
|
|
261
|
+
}
|
|
262
|
+
|
|
227
263
|
#wrapOutline(rows: string[], width: number): string[] {
|
|
228
264
|
// Mirror the outline border drawn by `OutlinedList.render(width)`. The
|
|
229
265
|
// rows passed in are already constrained to `innerWidth` by
|
|
@@ -381,6 +417,13 @@ export class HookSelectorComponent extends Container {
|
|
|
381
417
|
// Reset countdown on any interaction
|
|
382
418
|
this.#countdown?.reset();
|
|
383
419
|
|
|
420
|
+
if (this.#scrollTitleRows !== undefined) {
|
|
421
|
+
const wheelRows = getMouseWheelTitleScrollRows(keyData);
|
|
422
|
+
if (wheelRows !== 0) {
|
|
423
|
+
this.#scrollableTitle?.scrollBy(wheelRows);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
384
427
|
if (this.#scrollTitleRows !== undefined && matchesKey(keyData, "pageUp")) {
|
|
385
428
|
this.#scrollableTitle?.scrollBy(-this.#scrollTitleRows);
|
|
386
429
|
return;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure model helpers for the jobs overlay.
|
|
3
|
+
*
|
|
4
|
+
* Kept free of UI/Component dependencies so the grouping/ordering and
|
|
5
|
+
* detail-formatting logic is unit-testable. The selector controller wires these
|
|
6
|
+
* SelectItem lists into nested SelectLists (list -> detail -> confirm).
|
|
7
|
+
*/
|
|
8
|
+
import type { SelectItem } from "@gajae-code/tui";
|
|
9
|
+
import type { JobsSnapshot } from "../jobs-observer";
|
|
10
|
+
|
|
11
|
+
export type JobRefKind = "monitor" | "cron";
|
|
12
|
+
|
|
13
|
+
export interface JobRef {
|
|
14
|
+
kind: JobRefKind;
|
|
15
|
+
id: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const PROMPT_PREVIEW_MAX = 60;
|
|
19
|
+
|
|
20
|
+
function preview(text: string, max = PROMPT_PREVIEW_MAX): string {
|
|
21
|
+
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
22
|
+
return oneLine.length > max ? `${oneLine.slice(0, max - 1)}…` : oneLine;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Compact relative time, e.g. "in 5m", "2m ago", "now". */
|
|
26
|
+
export function formatRelative(targetMs: number | undefined, nowMs = Date.now()): string {
|
|
27
|
+
if (targetMs === undefined) return "—";
|
|
28
|
+
const deltaMs = targetMs - nowMs;
|
|
29
|
+
const abs = Math.abs(deltaMs);
|
|
30
|
+
const mins = Math.round(abs / 60_000);
|
|
31
|
+
if (mins < 1) return "now";
|
|
32
|
+
const unit = mins >= 60 ? `${Math.round(mins / 60)}h` : `${mins}m`;
|
|
33
|
+
return deltaMs >= 0 ? `in ${unit}` : `${unit} ago`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Parse a list item value back into a job reference. */
|
|
37
|
+
export function parseJobRef(value: string): JobRef | null {
|
|
38
|
+
const sep = value.indexOf(":");
|
|
39
|
+
if (sep === -1) return null;
|
|
40
|
+
const kind = value.slice(0, sep);
|
|
41
|
+
const id = value.slice(sep + 1);
|
|
42
|
+
if ((kind === "monitor" || kind === "cron") && id.length > 0) {
|
|
43
|
+
return { kind, id };
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build the grouped jobs list: monitors first (newest-first), then crons
|
|
50
|
+
* (newest-first). The snapshot arrays are already sorted newest-first.
|
|
51
|
+
*/
|
|
52
|
+
export function buildJobsListItems(snapshot: JobsSnapshot): SelectItem[] {
|
|
53
|
+
const items: SelectItem[] = [];
|
|
54
|
+
for (const monitor of snapshot.monitors) {
|
|
55
|
+
items.push({
|
|
56
|
+
value: `monitor:${monitor.id}`,
|
|
57
|
+
label: `monitor · ${preview(monitor.label, 40)}`,
|
|
58
|
+
description: monitor.status,
|
|
59
|
+
hint: monitor.status === "failed" ? "failed" : undefined,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
for (const cron of snapshot.crons) {
|
|
63
|
+
items.push({
|
|
64
|
+
value: `cron:${cron.id}`,
|
|
65
|
+
label: `cron · ${cron.humanSchedule}`,
|
|
66
|
+
description: preview(cron.prompt),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return items;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Build the detail-level items for a job: read-only info rows (value "noop"),
|
|
74
|
+
* then the destructive action, then a back row. `output` is the bounded monitor
|
|
75
|
+
* output tail (ignored for cron jobs).
|
|
76
|
+
*/
|
|
77
|
+
export function buildJobDetailItems(snapshot: JobsSnapshot, ref: JobRef, output = ""): SelectItem[] {
|
|
78
|
+
if (ref.kind === "monitor") {
|
|
79
|
+
const monitor = snapshot.monitors.find(m => m.id === ref.id);
|
|
80
|
+
if (!monitor) return [{ value: "back", label: "Back (job no longer present)" }];
|
|
81
|
+
const lastOutput = output.trim().split("\n").filter(Boolean).slice(-1)[0] ?? "(no output captured)";
|
|
82
|
+
return [
|
|
83
|
+
{ value: "noop", label: "Status", description: monitor.status },
|
|
84
|
+
{ value: "noop", label: "Label", description: preview(monitor.label) },
|
|
85
|
+
{ value: "noop", label: "Started", description: formatRelative(monitor.startTime) },
|
|
86
|
+
{ value: "noop", label: "Output", description: preview(lastOutput, 80) },
|
|
87
|
+
{ value: "action:cancel", label: "Cancel this monitor", hint: "stops the running job" },
|
|
88
|
+
{ value: "back", label: "Back" },
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
const cron = snapshot.crons.find(c => c.id === ref.id);
|
|
92
|
+
if (!cron) return [{ value: "back", label: "Back (job no longer present)" }];
|
|
93
|
+
return [
|
|
94
|
+
{ value: "noop", label: "Schedule", description: `${cron.humanSchedule} (${cron.cronExpression})` },
|
|
95
|
+
{ value: "noop", label: "Recurring", description: cron.recurring ? "yes" : "no" },
|
|
96
|
+
{ value: "noop", label: "Next fire", description: formatRelative(cron.nextFireAt) },
|
|
97
|
+
{ value: "noop", label: "Prompt", description: preview(cron.prompt, 80) },
|
|
98
|
+
{ value: "action:delete", label: "Delete this cron", hint: "removes the schedule" },
|
|
99
|
+
{ value: "back", label: "Back" },
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Yes/No confirm items for a destructive action. */
|
|
104
|
+
export function buildConfirmItems(actionLabel: string): SelectItem[] {
|
|
105
|
+
return [
|
|
106
|
+
{ value: "no", label: `No, keep it` },
|
|
107
|
+
{ value: "yes", label: `Yes, ${actionLabel}` },
|
|
108
|
+
];
|
|
109
|
+
}
|