@crouton-kit/crouter 0.3.70 → 0.3.79
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/README.md +1 -1
- package/dist/build-root.d.ts +12 -4
- package/dist/build-root.js +25 -6
- package/dist/builtin-memory/00-runtime-base.md +3 -10
- package/dist/builtin-memory/04-base-worker.md +18 -0
- package/dist/builtin-memory/04-orchestration-kernel.md +1 -1
- package/dist/builtin-memory/crouter-development/plugins.md +82 -5
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +13 -34
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +1115 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +234 -71
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/statusline.ts +2 -9
- package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +2 -516
- package/dist/builtin-pi-packages/pi-crtr-extensions/package-lock.json +2 -2
- package/dist/builtin-pi-packages/pi-mode-switch/README.md +11 -39
- package/dist/builtin-pi-packages/pi-mode-switch/extensions/index.ts +20 -15
- package/dist/builtin-pi-packages/pi-mode-switch/package.json +1 -1
- package/dist/builtin-views/canvas/tui.mjs +8 -9
- package/dist/builtin-views/chat/tui.mjs +12 -12
- package/dist/builtin-views/git-pr/tui.mjs +7 -8
- package/dist/builtin-views/inbox/tui.mjs +27 -41
- package/dist/builtin-views/linkedin/tui.mjs +23 -37
- package/dist/builtin-views/prompt-review/tui.mjs +11 -11
- package/dist/builtin-views/settings/tui.mjs +11 -11
- package/dist/builtin-views/workspace-sidebar/tui.mjs +8 -9
- package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +11 -6
- package/dist/clients/attach/__tests__/attach-keybindings.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/attach-keybindings.test.js +113 -0
- package/dist/clients/attach/__tests__/crtr-output-render.test.js +1 -1
- package/dist/clients/attach/__tests__/mermaid-render.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/mermaid-render.test.js +28 -0
- package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.js +110 -0
- package/dist/clients/attach/attach-cmd.d.ts +9 -1
- package/dist/clients/attach/attach-cmd.js +841 -792
- package/dist/clients/attach/auth-pickers.d.ts +0 -12
- package/dist/clients/attach/auth-pickers.js +64 -15
- package/dist/clients/attach/canvas-panels.js +2 -3
- package/dist/clients/attach/chat-view.d.ts +7 -8
- package/dist/clients/attach/chat-view.js +119 -79
- package/dist/clients/attach/edit-diff-render.d.ts +6 -0
- package/dist/clients/attach/edit-diff-render.js +263 -0
- package/dist/clients/attach/graph-overlay.d.ts +12 -2
- package/dist/clients/attach/graph-overlay.js +83 -33
- package/dist/clients/attach/input-controller.d.ts +28 -2
- package/dist/clients/attach/input-controller.js +38 -5
- package/dist/clients/attach/mermaid-render.js +5 -2
- package/dist/clients/attach/pickers.d.ts +8 -7
- package/dist/clients/attach/pickers.js +11 -17
- package/dist/clients/attach/slash-commands.d.ts +9 -0
- package/dist/clients/attach/slash-commands.js +127 -7
- package/dist/clients/attach/titled-editor.d.ts +12 -1
- package/dist/clients/attach/titled-editor.js +103 -8
- package/dist/commands/canvas-browse.js +2 -2
- package/dist/commands/human/queue.js +3 -4
- package/dist/commands/memory/lint.js +39 -5
- package/dist/commands/memory/write.js +1 -0
- package/dist/commands/node.js +9 -2
- package/dist/commands/pkg/plugin-inspect.js +22 -1
- package/dist/commands/pkg/plugin-manage.js +31 -9
- package/dist/commands/surface-tmux-spread.js +1 -3
- package/dist/commands/sys/__tests__/config-keybindings.test.d.ts +1 -0
- package/dist/commands/sys/__tests__/config-keybindings.test.js +55 -0
- package/dist/commands/sys/__tests__/config-model-ladders.test.d.ts +1 -0
- package/dist/commands/sys/__tests__/config-model-ladders.test.js +121 -0
- package/dist/commands/sys/__tests__/setup-core.test.js +158 -1
- package/dist/commands/sys/config.js +18 -21
- package/dist/commands/sys/doctor.js +42 -4
- package/dist/commands/sys/setup-core.d.ts +49 -1
- package/dist/commands/sys/setup-core.js +161 -4
- package/dist/commands/sys/setup.d.ts +88 -0
- package/dist/commands/sys/setup.js +943 -169
- package/dist/commands/view-pick.d.ts +4 -0
- package/dist/commands/view-pick.js +17 -7
- package/dist/core/__tests__/base-worker-prompt.test.d.ts +1 -0
- package/dist/core/__tests__/base-worker-prompt.test.js +24 -0
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +232 -1
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +34 -9
- package/dist/core/__tests__/command-plugins-surfaces.test.d.ts +1 -0
- package/dist/core/__tests__/command-plugins-surfaces.test.js +298 -0
- package/dist/core/__tests__/command-plugins.test.d.ts +1 -0
- package/dist/core/__tests__/command-plugins.test.js +444 -0
- package/dist/core/__tests__/fault-classifier.test.js +15 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +6 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +9 -1
- package/dist/core/__tests__/full/broker-dialogs.test.js +7 -2
- package/dist/core/__tests__/host-teardown-process-group.test.js +15 -4
- package/dist/core/__tests__/preview-registry-sync.test.js +30 -1
- package/dist/core/__tests__/scope-crouter-home-fence.test.d.ts +1 -0
- package/dist/core/__tests__/scope-crouter-home-fence.test.js +55 -0
- package/dist/core/__tests__/stream-watchdog.test.d.ts +1 -0
- package/dist/core/__tests__/stream-watchdog.test.js +70 -0
- package/dist/core/__tests__/tmux-surface.test.js +72 -0
- package/dist/core/canvas/browse/__tests__/model.test.js +23 -5
- package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
- package/dist/core/canvas/browse/app.d.ts +6 -0
- package/dist/core/canvas/browse/app.js +230 -41
- package/dist/core/canvas/browse/model.d.ts +10 -6
- package/dist/core/canvas/browse/model.js +20 -12
- package/dist/core/canvas/browse/pins.d.ts +4 -0
- package/dist/core/canvas/browse/pins.js +29 -0
- package/dist/core/canvas/browse/render.d.ts +13 -1
- package/dist/core/canvas/browse/render.js +72 -15
- package/dist/core/canvas/nav-model.js +5 -11
- package/dist/core/canvas/paths.d.ts +5 -0
- package/dist/core/canvas/paths.js +7 -0
- package/dist/core/command-plugins/adapter.d.ts +15 -0
- package/dist/core/command-plugins/adapter.js +145 -0
- package/dist/core/command-plugins/compose.d.ts +5 -0
- package/dist/core/command-plugins/compose.js +56 -0
- package/dist/core/command-plugins/discovery.d.ts +104 -0
- package/dist/core/command-plugins/discovery.js +565 -0
- package/dist/core/config.d.ts +48 -0
- package/dist/core/config.js +228 -110
- package/dist/core/fault-classifier.js +1 -1
- package/dist/core/fs-utils.d.ts +1 -0
- package/dist/core/fs-utils.js +15 -1
- package/dist/core/keybindings/__tests__/bespoke-consumers.test.d.ts +1 -0
- package/dist/core/keybindings/__tests__/bespoke-consumers.test.js +40 -0
- package/dist/core/keybindings/__tests__/resolve.test.d.ts +1 -0
- package/dist/core/keybindings/__tests__/resolve.test.js +224 -0
- package/dist/core/keybindings/catalog.d.ts +14 -0
- package/dist/core/keybindings/catalog.js +257 -0
- package/dist/core/keybindings/index.d.ts +5 -0
- package/dist/core/keybindings/index.js +4 -0
- package/dist/core/keybindings/match.d.ts +29 -0
- package/dist/core/keybindings/match.js +99 -0
- package/dist/core/keybindings/persistence.d.ts +14 -0
- package/dist/core/keybindings/persistence.js +31 -0
- package/dist/core/keybindings/resolve.d.ts +14 -0
- package/dist/core/keybindings/resolve.js +305 -0
- package/dist/core/keybindings/types.d.ts +37 -0
- package/dist/core/keybindings/types.js +1 -0
- package/dist/core/memory-resolver.d.ts +1 -1
- package/dist/core/memory-resolver.js +10 -3
- package/dist/core/predicate.d.ts +5 -3
- package/dist/core/predicate.js +5 -3
- package/dist/core/profiles/select.d.ts +6 -0
- package/dist/core/profiles/select.js +86 -52
- package/dist/core/provider-management.d.ts +12 -0
- package/dist/core/provider-management.js +24 -0
- package/dist/core/runtime/broker-protocol.d.ts +33 -5
- package/dist/core/runtime/broker.js +164 -14
- package/dist/core/runtime/launch.d.ts +39 -6
- package/dist/core/runtime/launch.js +78 -20
- package/dist/core/runtime/naming.js +3 -3
- package/dist/core/runtime/pi-cli.d.ts +6 -0
- package/dist/core/runtime/pi-cli.js +16 -2
- package/dist/core/runtime/pi-vendored.d.ts +8 -0
- package/dist/core/runtime/pi-vendored.js +14 -0
- package/dist/core/runtime/placement.d.ts +2 -2
- package/dist/core/runtime/placement.js +4 -1
- package/dist/core/runtime/promote.js +4 -0
- package/dist/core/runtime/recap.d.ts +3 -3
- package/dist/core/runtime/recap.js +75 -47
- package/dist/core/runtime/recycle.js +6 -1
- package/dist/core/runtime/reset.js +5 -0
- package/dist/core/runtime/session-list-cache.d.ts +33 -0
- package/dist/core/runtime/session-list-cache.js +338 -0
- package/dist/core/runtime/spawn.js +52 -45
- package/dist/core/runtime/stream-watchdog.d.ts +26 -0
- package/dist/core/runtime/stream-watchdog.js +75 -0
- package/dist/core/runtime/tmux-chrome.d.ts +1 -1
- package/dist/core/runtime/tmux-chrome.js +2 -2
- package/dist/core/runtime/tmux.d.ts +18 -15
- package/dist/core/runtime/tmux.js +217 -110
- package/dist/core/scope.js +27 -4
- package/dist/core/subscription-state.d.ts +90 -0
- package/dist/core/subscription-state.js +762 -0
- package/dist/core/tui/__tests__/host-keybindings.test.d.ts +1 -0
- package/dist/core/tui/__tests__/host-keybindings.test.js +112 -0
- package/dist/core/tui/host.d.ts +30 -1
- package/dist/core/tui/host.js +148 -34
- package/dist/core/view/contract.d.ts +17 -3
- package/dist/daemon/crtrd.js +253 -12
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +41 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.js +121 -0
- package/dist/pi-extensions/canvas-inbox-watcher.d.ts +1 -1
- package/dist/pi-extensions/canvas-inbox-watcher.js +203 -25
- package/dist/pi-extensions/canvas-recap.d.ts +4 -0
- package/dist/pi-extensions/canvas-recap.js +73 -46
- package/dist/pi-extensions/canvas-stophook.d.ts +11 -0
- package/dist/pi-extensions/canvas-stophook.js +33 -4
- package/dist/types.d.ts +9 -26
- package/dist/types.js +1 -28
- package/dist/web-client/assets/{index-DiFuLcp6.js → index--md2ylfi.js} +1 -1
- package/dist/web-client/index.html +1 -1
- package/dist/web-client/sw.js +1 -1
- package/package.json +8 -4
- package/dist/pi-extensions/canvas-nav.d.ts +0 -43
- package/dist/pi-extensions/canvas-nav.js +0 -640
- package/dist/pi-extensions/widget-order-bus.d.ts +0 -6
- package/dist/pi-extensions/widget-order-bus.js +0 -34
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { defineLeaf } from '../../core/command.js';
|
|
2
2
|
import { getMarkdownTheme, getSelectListTheme, getSettingsListTheme } from '@earendil-works/pi-coding-agent';
|
|
3
3
|
import { TUI, ProcessTerminal, Input, matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
|
|
4
|
+
import { AuthStorage, LoginDialogComponent, ModelRegistry } from '@earendil-works/pi-coding-agent';
|
|
5
|
+
import { loginAnthropic, loginOpenAICodex } from '@earendil-works/pi-ai/oauth';
|
|
6
|
+
import { execFile } from 'node:child_process';
|
|
7
|
+
import { join } from 'node:path';
|
|
4
8
|
import { applyTheme, defaultAgentDir } from '../../clients/attach/config-load.js';
|
|
5
|
-
import {
|
|
9
|
+
import { ANTHROPIC_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID, promoteSubscription, readSubscriptionPool, commitManagedLogin, renameSubscription, } from '../../core/subscription-state.js';
|
|
10
|
+
import { removeProviderRow } from '../../core/provider-management.js';
|
|
11
|
+
import { readRawScopeConfig } from '../../core/config.js';
|
|
12
|
+
import { BINDING_CATALOG, formatBinding, formatGesture, matchesPiTuiInput, persistUserKeybindings, resolveKeybindings, KeybindingConcurrentEditError, KeybindingValidationError, } from '../../core/keybindings/index.js';
|
|
13
|
+
import { installTmuxBindings } from '../../core/runtime/tmux-chrome.js';
|
|
6
14
|
const ANSI_GREEN = (s) => `\x1b[32m${s}\x1b[39m`;
|
|
7
15
|
const ANSI_YELLOW = (s) => `\x1b[33m${s}\x1b[39m`;
|
|
8
16
|
const theme = {
|
|
@@ -33,7 +41,7 @@ const theme = {
|
|
|
33
41
|
return getMarkdownTheme().bold(text);
|
|
34
42
|
},
|
|
35
43
|
};
|
|
36
|
-
import { buildSetupInitialState, executeSetupSubmission, formatSetupExecutionSummary, renderSetupStaticInstructions, resolveExaKeyPath, } from './setup-core.js';
|
|
44
|
+
import { buildSetupInitialState, executeSetupSubmission, formatSetupExecutionSummary, renderSetupStaticInstructions, resolveExaKeyPath, resolveComposioKeyPath, detectExaKey, detectComposioKey, SetupKeybindingsDraft, } from './setup-core.js';
|
|
37
45
|
function wrapText(text, width) {
|
|
38
46
|
const clean = text.trim();
|
|
39
47
|
if (clean === '')
|
|
@@ -67,6 +75,15 @@ function currentWidth() {
|
|
|
67
75
|
function toolLabel(tool) {
|
|
68
76
|
return tool === 'nvim' ? 'nvim' : 'tmux';
|
|
69
77
|
}
|
|
78
|
+
/** Inline badge for an optional API-key tab header: green ✓ when a key is
|
|
79
|
+
* already resolvable (naming its source so an env-provided key isn't mistaken
|
|
80
|
+
* for the writable file), dim — when absent. */
|
|
81
|
+
function keyStatusBadge(presence, envVar) {
|
|
82
|
+
if (!presence.present)
|
|
83
|
+
return theme.fg('dim', ' · not set');
|
|
84
|
+
const where = presence.source === 'env' ? `${envVar} env` : 'saved';
|
|
85
|
+
return theme.fg('success', ` ✓ configured (${where})`);
|
|
86
|
+
}
|
|
70
87
|
function renderRows(lines, width) {
|
|
71
88
|
return lines.map((line) => truncateToWidth(line, width));
|
|
72
89
|
}
|
|
@@ -97,33 +114,386 @@ function framed(lines, width) {
|
|
|
97
114
|
});
|
|
98
115
|
return [top, ...body, bottom];
|
|
99
116
|
}
|
|
100
|
-
class
|
|
117
|
+
class ProviderPanel {
|
|
118
|
+
tui;
|
|
119
|
+
agentDir;
|
|
120
|
+
renameInput = new Input();
|
|
121
|
+
rows = [];
|
|
122
|
+
selected = 0;
|
|
123
|
+
mode = 'list';
|
|
124
|
+
notice = '';
|
|
125
|
+
catalog = [];
|
|
126
|
+
catalogIndex = 0;
|
|
127
|
+
loginProvider;
|
|
128
|
+
loginLabel = '';
|
|
129
|
+
dialog;
|
|
130
|
+
loginFlow;
|
|
131
|
+
// Wired by the host wizard to bust its render-line cache. The panel proxies pi's
|
|
132
|
+
// LoginDialogComponent (it is not a real pi TUI child), so the host only repaints on
|
|
133
|
+
// synchronous input. startLogin drives OAuth through async promise chains whose state
|
|
134
|
+
// transitions land between keystrokes; notifyChange() lets each one force a fresh frame.
|
|
135
|
+
onChange;
|
|
136
|
+
constructor(tui, agentDir) {
|
|
137
|
+
this.tui = tui;
|
|
138
|
+
this.agentDir = agentDir;
|
|
139
|
+
this.renameInput.onSubmit = () => this.rename();
|
|
140
|
+
this.renameInput.onEscape = () => { this.mode = 'list'; };
|
|
141
|
+
this.reload();
|
|
142
|
+
}
|
|
143
|
+
// Repaint the host after an async login transition: bust the wizard's cached lines
|
|
144
|
+
// (onChange) so render() recomputes the NEW state, then ask pi's loop to draw a frame.
|
|
145
|
+
notifyChange() {
|
|
146
|
+
this.onChange?.();
|
|
147
|
+
this.tui.requestRender();
|
|
148
|
+
}
|
|
149
|
+
providerCatalog(auth, registry) {
|
|
150
|
+
const oauth = auth.getOAuthProviders();
|
|
151
|
+
const oauthIds = new Set(oauth.map((provider) => provider.id));
|
|
152
|
+
const seen = new Set();
|
|
153
|
+
return [
|
|
154
|
+
...oauth.map((provider) => ({ providerId: provider.id, name: provider.name, managed: provider.id === ANTHROPIC_PROVIDER_ID || provider.id === OPENAI_CODEX_PROVIDER_ID, authType: 'oauth' })),
|
|
155
|
+
...registry.getAll().map((model) => ({ providerId: model.provider, name: registry.getProviderDisplayName(model.provider), managed: model.provider === ANTHROPIC_PROVIDER_ID || model.provider === OPENAI_CODEX_PROVIDER_ID, authType: oauthIds.has(model.provider) ? 'oauth' : 'api_key' })),
|
|
156
|
+
].filter((provider) => !seen.has(provider.providerId) && Boolean(seen.add(provider.providerId)));
|
|
157
|
+
}
|
|
158
|
+
reload() {
|
|
159
|
+
const auth = AuthStorage.create(join(this.agentDir, 'auth.json'));
|
|
160
|
+
const registry = ModelRegistry.create(auth, join(this.agentDir, 'models.json'));
|
|
161
|
+
const rows = [];
|
|
162
|
+
for (const provider of this.providerCatalog(auth, registry)) {
|
|
163
|
+
const status = auth.getAuthStatus(provider.providerId);
|
|
164
|
+
if (provider.managed) {
|
|
165
|
+
const pool = readSubscriptionPool(provider.providerId);
|
|
166
|
+
if (pool.length === 0 && !status.source)
|
|
167
|
+
continue;
|
|
168
|
+
rows.push({ ...provider, configured: pool.length > 0 || status.configured, source: status.source, sourceLabel: status.label });
|
|
169
|
+
pool.forEach((account, index) => rows.push({ ...provider, label: account.label === provider.providerId ? 'Default account' : account.label, preferred: index === 0, cooldownUntil: account.rateLimitedUntil, accountId: account.accountId }));
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (!status.source)
|
|
173
|
+
continue;
|
|
174
|
+
const stored = auth.get(provider.providerId);
|
|
175
|
+
rows.push({ ...provider, configured: status.configured, source: status.source, sourceLabel: status.label, accountId: typeof stored?.accountId === 'string' ? stored.accountId : undefined });
|
|
176
|
+
}
|
|
177
|
+
this.rows = rows;
|
|
178
|
+
this.selected = Math.min(this.selected, Math.max(0, rows.length));
|
|
179
|
+
}
|
|
180
|
+
selectedRow() { return this.rows[this.selected]; }
|
|
181
|
+
openBrowser(url) {
|
|
182
|
+
const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
183
|
+
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
184
|
+
execFile(command, args, { timeout: 5_000 }, () => { });
|
|
185
|
+
}
|
|
186
|
+
openCatalog() {
|
|
187
|
+
const auth = AuthStorage.create(join(this.agentDir, 'auth.json'));
|
|
188
|
+
this.catalog = this.providerCatalog(auth, ModelRegistry.create(auth, join(this.agentDir, 'models.json')));
|
|
189
|
+
this.catalogIndex = 0;
|
|
190
|
+
this.mode = 'catalog';
|
|
191
|
+
}
|
|
192
|
+
startLogin(provider, label = provider.providerId) {
|
|
193
|
+
this.loginProvider = provider;
|
|
194
|
+
this.loginLabel = label;
|
|
195
|
+
const auth = AuthStorage.create(join(this.agentDir, 'auth.json'));
|
|
196
|
+
let flow;
|
|
197
|
+
const complete = (message) => {
|
|
198
|
+
if (!flow.active || this.loginFlow !== flow)
|
|
199
|
+
return;
|
|
200
|
+
flow.active = false;
|
|
201
|
+
this.loginFlow = undefined;
|
|
202
|
+
this.dialog = undefined;
|
|
203
|
+
this.mode = 'list';
|
|
204
|
+
this.notice = message;
|
|
205
|
+
this.reload();
|
|
206
|
+
this.notifyChange();
|
|
207
|
+
};
|
|
208
|
+
const dialog = new LoginDialogComponent(this.tui, provider.providerId, (success, message) => {
|
|
209
|
+
if (!success)
|
|
210
|
+
complete(message ? `Login failed: ${message}` : 'Login cancelled');
|
|
211
|
+
}, provider.name);
|
|
212
|
+
flow = { dialog, active: true };
|
|
213
|
+
this.loginFlow = flow;
|
|
214
|
+
this.dialog = dialog;
|
|
215
|
+
this.mode = 'login';
|
|
216
|
+
const isActive = () => flow.active && this.loginFlow === flow && !dialog.signal.aborted;
|
|
217
|
+
const fail = (error) => {
|
|
218
|
+
complete(error.name === 'AbortError' ? 'Login cancelled' : `Login error: ${error instanceof Error ? error.message : String(error)}`);
|
|
219
|
+
};
|
|
220
|
+
if (provider.authType === 'api_key') {
|
|
221
|
+
void dialog.showPrompt(`Enter API key for ${provider.name}:`, 'sk-...').then((key) => {
|
|
222
|
+
if (!isActive())
|
|
223
|
+
return;
|
|
224
|
+
if (!key.trim()) {
|
|
225
|
+
complete('No key entered — login cancelled');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
auth.set(provider.providerId, { type: 'api_key', key: key.trim() });
|
|
229
|
+
complete(`API key saved for ${provider.name}`);
|
|
230
|
+
}).catch(fail);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (!provider.managed) {
|
|
234
|
+
void auth.login(provider.providerId, {
|
|
235
|
+
onAuth: (info) => { if (isActive()) {
|
|
236
|
+
dialog.showAuth(info.url, info.instructions);
|
|
237
|
+
this.openBrowser(info.url);
|
|
238
|
+
this.notifyChange();
|
|
239
|
+
} },
|
|
240
|
+
onDeviceCode: (info) => { if (isActive()) {
|
|
241
|
+
dialog.showDeviceCode(info);
|
|
242
|
+
this.openBrowser(info.verificationUri);
|
|
243
|
+
this.notifyChange();
|
|
244
|
+
} },
|
|
245
|
+
onPrompt: (prompt) => { const answer = dialog.showPrompt(prompt.message, prompt.placeholder); this.notifyChange(); return answer; },
|
|
246
|
+
onProgress: (message) => { if (isActive()) {
|
|
247
|
+
dialog.showProgress(message);
|
|
248
|
+
this.notifyChange();
|
|
249
|
+
} },
|
|
250
|
+
onManualCodeInput: () => { const answer = dialog.showManualInput('Paste the code or URL:'); this.notifyChange(); return answer; },
|
|
251
|
+
onSelect: async (prompt) => { const answer = dialog.showPrompt(`${prompt.message}\n${prompt.options.map((option, index) => `${index + 1}. ${option.label}`).join('\n')}`, '1'); this.notifyChange(); return prompt.options[(Number.parseInt(await answer, 10) || 1) - 1]?.id; },
|
|
252
|
+
signal: dialog.signal,
|
|
253
|
+
}).then(() => { if (isActive())
|
|
254
|
+
complete(`Logged in to ${provider.name}`); }).catch(fail);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const login = provider.providerId === ANTHROPIC_PROVIDER_ID ? loginAnthropic : loginOpenAICodex;
|
|
258
|
+
// Snapshot the default slot's rate-limit timestamp BEFORE OAuth begins: a strictly newer
|
|
259
|
+
// concurrent 429 recorded during the login must survive a same-account re-login.
|
|
260
|
+
const defaultLastRateLimitedAt = readSubscriptionPool(provider.providerId).find((entry) => entry.label === provider.providerId)?.lastRateLimitedAt ?? 0;
|
|
261
|
+
void login({
|
|
262
|
+
onAuth: ({ url, instructions }) => { if (isActive()) {
|
|
263
|
+
dialog.showAuth(url, instructions);
|
|
264
|
+
this.openBrowser(url);
|
|
265
|
+
this.notifyChange();
|
|
266
|
+
} },
|
|
267
|
+
onPrompt: async (prompt) => { const answer = dialog.showPrompt(prompt.message, prompt.placeholder); this.notifyChange(); return answer; },
|
|
268
|
+
onProgress: (message) => { if (isActive()) {
|
|
269
|
+
dialog.showProgress(message);
|
|
270
|
+
this.notifyChange();
|
|
271
|
+
} },
|
|
272
|
+
onManualCodeInput: () => { const answer = dialog.showManualInput('Paste the code or URL:'); this.notifyChange(); return answer; },
|
|
273
|
+
}).then((credential) => {
|
|
274
|
+
if (!isActive())
|
|
275
|
+
return;
|
|
276
|
+
const accountId = typeof credential.accountId === 'string' && credential.accountId.trim() ? credential.accountId.trim() : undefined;
|
|
277
|
+
commitManagedLogin(provider.providerId, this.loginLabel, { refresh: credential.refresh, access: credential.access, expires: credential.expires, accountId, defaultLastRateLimitedAt });
|
|
278
|
+
if (isActive())
|
|
279
|
+
complete(`Logged in to ${provider.name}`);
|
|
280
|
+
}).catch(fail);
|
|
281
|
+
}
|
|
282
|
+
isNestedFlowActive() { return this.mode === 'login'; }
|
|
283
|
+
dispose() {
|
|
284
|
+
const flow = this.loginFlow;
|
|
285
|
+
if (!flow)
|
|
286
|
+
return;
|
|
287
|
+
flow.active = false;
|
|
288
|
+
this.loginFlow = undefined;
|
|
289
|
+
this.dialog = undefined;
|
|
290
|
+
flow.dialog.handleInput('\u001b');
|
|
291
|
+
}
|
|
292
|
+
rename() {
|
|
293
|
+
const row = this.selectedRow();
|
|
294
|
+
if (!row?.managed || !row.label || row.label === 'Default account')
|
|
295
|
+
return;
|
|
296
|
+
try {
|
|
297
|
+
renameSubscription(row.providerId, row.label, this.renameInput.getValue());
|
|
298
|
+
this.notice = `Renamed ${row.name} account`;
|
|
299
|
+
this.mode = 'list';
|
|
300
|
+
this.reload();
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
this.notice = error instanceof Error ? error.message : String(error);
|
|
304
|
+
this.mode = 'list';
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
handleInput(data) {
|
|
308
|
+
if (this.mode === 'login') {
|
|
309
|
+
this.dialog?.handleInput(data);
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
if (this.mode === 'rename' || this.mode === 'label') {
|
|
313
|
+
this.renameInput.handleInput(data);
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
if (this.mode === 'catalog') {
|
|
317
|
+
if (matchesKey(data, 'escape')) {
|
|
318
|
+
this.mode = 'list';
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
if (matchesKey(data, 'j') || matchesKey(data, 'down')) {
|
|
322
|
+
this.catalogIndex = Math.min(this.catalog.length - 1, this.catalogIndex + 1);
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
if (matchesKey(data, 'k') || matchesKey(data, 'up')) {
|
|
326
|
+
this.catalogIndex = Math.max(0, this.catalogIndex - 1);
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
if (matchesKey(data, 'enter')) {
|
|
330
|
+
const provider = this.catalog[this.catalogIndex];
|
|
331
|
+
if (provider.managed) {
|
|
332
|
+
this.loginProvider = provider;
|
|
333
|
+
this.renameInput.setValue('');
|
|
334
|
+
this.renameInput.onSubmit = () => { const label = this.renameInput.getValue().trim(); if (label)
|
|
335
|
+
this.startLogin(provider, label); };
|
|
336
|
+
this.mode = 'label';
|
|
337
|
+
}
|
|
338
|
+
else
|
|
339
|
+
this.startLogin(provider);
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
if (this.mode === 'remove') {
|
|
345
|
+
if (matchesKey(data, 'y') || matchesKey(data, 'enter')) {
|
|
346
|
+
const row = this.selectedRow();
|
|
347
|
+
if (row && (row.managed || row.configured)) {
|
|
348
|
+
try {
|
|
349
|
+
this.notice = removeProviderRow(this.agentDir, row);
|
|
350
|
+
this.reload();
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
this.notice = error instanceof Error ? error.message : String(error);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
this.mode = 'list';
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
if (matchesKey(data, 'j') || matchesKey(data, 'down')) {
|
|
361
|
+
this.selected = Math.min(this.rows.length, this.selected + 1);
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
if (matchesKey(data, 'k') || matchesKey(data, 'up')) {
|
|
365
|
+
this.selected = Math.max(0, this.selected - 1);
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
const row = this.selectedRow();
|
|
369
|
+
if (matchesKey(data, 'p') && row?.managed && row.label) {
|
|
370
|
+
try {
|
|
371
|
+
promoteSubscription(row.providerId, row.label === 'Default account' ? row.providerId : row.label);
|
|
372
|
+
this.notice = `Preferred ${row.name} account`;
|
|
373
|
+
this.reload();
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
this.notice = error instanceof Error ? error.message : String(error);
|
|
377
|
+
}
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
if (matchesKey(data, 'r') && row?.managed && row.label && row.label !== 'Default account') {
|
|
381
|
+
this.renameInput.setValue(row.label);
|
|
382
|
+
this.renameInput.onSubmit = () => this.rename();
|
|
383
|
+
this.mode = 'rename';
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
if (matchesKey(data, 'x') && (row?.managed || row?.configured)) {
|
|
387
|
+
this.mode = 'remove';
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
if (matchesKey(data, 'a')) {
|
|
391
|
+
this.openCatalog();
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
const viewOnly = row?.source === 'environment' || row?.source === 'runtime';
|
|
395
|
+
if (matchesKey(data, 'l') && row && !viewOnly) {
|
|
396
|
+
if (row.managed && row.label)
|
|
397
|
+
this.startLogin(row, row.label === 'Default account' ? row.providerId : row.label);
|
|
398
|
+
else
|
|
399
|
+
this.startLogin(row);
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
if (matchesKey(data, 'enter')) {
|
|
403
|
+
if (row && !viewOnly)
|
|
404
|
+
this.startLogin(row, row.managed && row.label ? (row.label === 'Default account' ? row.providerId : row.label) : row.providerId);
|
|
405
|
+
else if (!row)
|
|
406
|
+
this.openCatalog();
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
render(width) {
|
|
412
|
+
const body = [theme.fg('accent', theme.bold('Providers')), theme.fg('dim', `${this.rows.filter((row) => row.label === undefined).length} configured`)];
|
|
413
|
+
if (this.mode === 'login')
|
|
414
|
+
return framed(this.dialog?.render(Math.max(1, width - 4)) ?? [], width);
|
|
415
|
+
if (this.mode === 'rename' || this.mode === 'label')
|
|
416
|
+
body.push('', theme.fg('accent', this.mode === 'rename' ? 'Rename account' : `Name the ${this.loginProvider?.name ?? 'provider'} account`), ...this.renameInput.render(Math.max(1, width - 8)));
|
|
417
|
+
else if (this.mode === 'catalog') {
|
|
418
|
+
body.push('', theme.fg('dim', 'Select a provider to add · esc back'));
|
|
419
|
+
this.catalog.forEach((provider, index) => body.push(`${index === this.catalogIndex ? theme.fg('accent', '▶') : ' '} ${provider.name}`));
|
|
420
|
+
}
|
|
421
|
+
else if (this.mode === 'remove') {
|
|
422
|
+
const target = this.selectedRow();
|
|
423
|
+
const label = target?.label ?? (target?.managed ? `${target.name} · all accounts` : target?.name ?? 'account');
|
|
424
|
+
body.push('', theme.fg('warning', `Remove ${label}? y/enter confirm · any other key cancel`));
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
if (this.rows.length === 0)
|
|
428
|
+
body.push('', theme.fg('warning', 'No provider configured yet.'));
|
|
429
|
+
for (let index = 0; index < this.rows.length; index++) {
|
|
430
|
+
const row = this.rows[index];
|
|
431
|
+
const selected = index === this.selected ? theme.fg('accent', '▶') : ' ';
|
|
432
|
+
if (!row.label) {
|
|
433
|
+
const state = row.managed
|
|
434
|
+
? `${this.rows.filter((item) => item.providerId === row.providerId && item.label).length} accounts`
|
|
435
|
+
: row.source === 'environment' || row.source === 'runtime'
|
|
436
|
+
? `${row.source}${row.sourceLabel ? ` · ${row.sourceLabel}` : ''} (view only)`
|
|
437
|
+
: 'configured';
|
|
438
|
+
body.push(`${selected} ${theme.fg('accent', row.name)}${theme.fg('dim', ` ${state}`)}`);
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
const health = row.cooldownUntil && row.cooldownUntil > Date.now() ? `cooldown ${Math.ceil((row.cooldownUntil - Date.now()) / 60000)}m` : 'ready';
|
|
442
|
+
const identity = row.accountId ? ` · ${row.accountId.length > 12 ? `${row.accountId.slice(0, 8)}…${row.accountId.slice(-4)}` : row.accountId}` : '';
|
|
443
|
+
body.push(`${selected} ${row.preferred ? theme.fg('success', '●') : ' '} ${row.label}${theme.fg('dim', identity)} ${theme.fg('dim', health)}${row.preferred ? theme.fg('accent', ' preferred') : ''}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
body.push('', `${this.selected === this.rows.length ? theme.fg('accent', '▶') : ' '} ${theme.fg('accent', '+ Add provider')}`);
|
|
447
|
+
if (this.notice)
|
|
448
|
+
body.push('', theme.fg('dim', this.notice));
|
|
449
|
+
}
|
|
450
|
+
return framed(body, width);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
export class SetupWizard {
|
|
101
454
|
manifest;
|
|
102
455
|
installedPackageIds;
|
|
103
456
|
systemTools;
|
|
104
457
|
exaInput = new Input();
|
|
458
|
+
composioInput = new Input();
|
|
459
|
+
keyInput = new Input();
|
|
460
|
+
openingBindings;
|
|
461
|
+
keyDraft;
|
|
462
|
+
terminalRows;
|
|
105
463
|
selectedPackageIds = new Set();
|
|
106
464
|
selectedSystemTools = new Set();
|
|
107
|
-
|
|
108
|
-
reloadAuthSummary;
|
|
109
|
-
notice;
|
|
110
|
-
onOpenLogin;
|
|
465
|
+
providers;
|
|
111
466
|
activeTab = 0;
|
|
112
467
|
pluginIndex = 0;
|
|
113
468
|
systemIndex = 0;
|
|
114
469
|
expandedPluginId = null;
|
|
115
470
|
confirming = false;
|
|
471
|
+
keyIndex = 0;
|
|
472
|
+
keyScroll = 0;
|
|
473
|
+
keyFilter = '';
|
|
474
|
+
filteringKeys = false;
|
|
475
|
+
editingKey = null;
|
|
476
|
+
keyEditError;
|
|
477
|
+
keyDiagnostics = [];
|
|
478
|
+
keyConfirm = null;
|
|
479
|
+
keyNotice;
|
|
116
480
|
cachedWidth;
|
|
117
481
|
cachedLines;
|
|
118
482
|
_focused = false;
|
|
119
483
|
onSubmit;
|
|
120
484
|
onCancel;
|
|
121
|
-
constructor(manifest, installedPackageIds, systemTools,
|
|
485
|
+
constructor(manifest, installedPackageIds, systemTools, tui, agentDir, openingBindings, rawKeybindings, terminalRows) {
|
|
122
486
|
this.manifest = manifest;
|
|
487
|
+
this.providers = new ProviderPanel(tui, agentDir);
|
|
488
|
+
// The panel drives OAuth async; bust our line cache when it transitions so the panel's
|
|
489
|
+
// requestRender repaints the NEW frame (progress → success notice → roster) without a keypress.
|
|
490
|
+
this.providers.onChange = () => this.invalidate();
|
|
123
491
|
this.installedPackageIds = installedPackageIds;
|
|
124
492
|
this.systemTools = systemTools;
|
|
125
|
-
this.
|
|
126
|
-
this.
|
|
493
|
+
this.terminalRows = terminalRows;
|
|
494
|
+
this.openingBindings = openingBindings;
|
|
495
|
+
this.keyDraft = new SetupKeybindingsDraft(rawKeybindings);
|
|
496
|
+
this.keyDiagnostics = this.keyDraft.resolution().diagnostics;
|
|
127
497
|
for (const entry of this.manifest)
|
|
128
498
|
this.selectedPackageIds.add(entry.id);
|
|
129
499
|
for (const tool of this.systemTools) {
|
|
@@ -134,10 +504,19 @@ class SetupWizard {
|
|
|
134
504
|
this.systemIndex = firstMissingSystem >= 0 ? firstMissingSystem : 0;
|
|
135
505
|
this.exaInput.onSubmit = () => this.finalize();
|
|
136
506
|
this.exaInput.onEscape = () => this.cancel();
|
|
507
|
+
this.composioInput.onSubmit = () => this.finalize();
|
|
508
|
+
this.composioInput.onEscape = () => this.cancel();
|
|
509
|
+
// Edit-mode Enter/Escape are routed through the opening snapshot's
|
|
510
|
+
// `crtr.setup.keybindings.commit-edit` / `.cancel-edit` bindings in
|
|
511
|
+
// handleKeybindingsInput (before the pi Input sees them), so the pi Input's
|
|
512
|
+
// own submit/escape handlers stay unset — those catalog rows are honored
|
|
513
|
+
// even when remapped away from Enter/Escape.
|
|
137
514
|
}
|
|
138
515
|
set focused(value) {
|
|
139
516
|
this._focused = value;
|
|
140
517
|
this.exaInput.focused = value && this.activeTab === 3 && !this.confirming;
|
|
518
|
+
this.composioInput.focused = value && this.activeTab === 4 && !this.confirming;
|
|
519
|
+
this.keyInput.focused = value && this.activeTab === 5 && this.editingKey !== null;
|
|
141
520
|
}
|
|
142
521
|
get focused() {
|
|
143
522
|
return this._focused;
|
|
@@ -146,23 +525,15 @@ class SetupWizard {
|
|
|
146
525
|
this.cachedWidth = undefined;
|
|
147
526
|
this.cachedLines = undefined;
|
|
148
527
|
}
|
|
149
|
-
/** Show a one-line notice on the login tab (login success/failure feedback). */
|
|
150
|
-
setNotice(message) {
|
|
151
|
-
this.notice = message;
|
|
152
|
-
this.invalidate();
|
|
153
|
-
}
|
|
154
|
-
/** Recompute provider auth status after a login flow closes. */
|
|
155
|
-
refreshAuth() {
|
|
156
|
-
this.authSummary = this.reloadAuthSummary();
|
|
157
|
-
this.invalidate();
|
|
158
|
-
}
|
|
159
528
|
setTab(next) {
|
|
160
529
|
this.activeTab = next;
|
|
161
530
|
this.exaInput.focused = this.focused && next === 3 && !this.confirming;
|
|
531
|
+
this.composioInput.focused = this.focused && next === 4 && !this.confirming;
|
|
532
|
+
this.keyInput.focused = this.focused && next === 5 && this.editingKey !== null;
|
|
162
533
|
this.invalidate();
|
|
163
534
|
}
|
|
164
535
|
moveTab(delta) {
|
|
165
|
-
const next = ((this.activeTab + delta +
|
|
536
|
+
const next = ((this.activeTab + delta + 6) % 6);
|
|
166
537
|
this.setTab(next);
|
|
167
538
|
}
|
|
168
539
|
cancel() {
|
|
@@ -171,14 +542,38 @@ class SetupWizard {
|
|
|
171
542
|
this.invalidate();
|
|
172
543
|
return;
|
|
173
544
|
}
|
|
545
|
+
this.providers.dispose();
|
|
174
546
|
this.onCancel?.();
|
|
175
547
|
}
|
|
548
|
+
dispose() { this.providers.dispose(); }
|
|
549
|
+
matches(id, data) {
|
|
550
|
+
return matchesPiTuiInput(this.openingBindings, id, data, (input, gesture) => matchesKey(input, gesture));
|
|
551
|
+
}
|
|
552
|
+
/** Human hint label for an action, derived from the opening snapshot so footers
|
|
553
|
+
* reflect the effective (possibly remapped) gestures the wizard actually honors. */
|
|
554
|
+
label(id) {
|
|
555
|
+
return formatBinding(this.openingBindings, id);
|
|
556
|
+
}
|
|
557
|
+
/** Like `label`, but drops gestures a focused pi text Input consumes as data
|
|
558
|
+
* (single printable chars and the arrow keys), so the Exa/Composio footers do
|
|
559
|
+
* not claim keys that never reach the wizard while a field owns focus. */
|
|
560
|
+
fieldSafeLabel(id) {
|
|
561
|
+
const survives = this.openingBindings.gestures(id).filter((gesture) => {
|
|
562
|
+
if (gesture.includes(' > '))
|
|
563
|
+
return true;
|
|
564
|
+
const key = gesture.split('+').pop();
|
|
565
|
+
if (key === 'up' || key === 'down' || key === 'left' || key === 'right')
|
|
566
|
+
return false;
|
|
567
|
+
return key.length > 1 || gesture.includes('+');
|
|
568
|
+
});
|
|
569
|
+
return survives.length === 0 ? 'Disabled' : survives.map(formatGesture).join(' / ');
|
|
570
|
+
}
|
|
176
571
|
handleGlobalTabSwitch(data) {
|
|
177
|
-
if (
|
|
572
|
+
if (this.matches('crtr.setup.tab-next', data)) {
|
|
178
573
|
this.moveTab(1);
|
|
179
574
|
return true;
|
|
180
575
|
}
|
|
181
|
-
if (
|
|
576
|
+
if (this.matches('crtr.setup.tab-previous', data)) {
|
|
182
577
|
this.moveTab(-1);
|
|
183
578
|
return true;
|
|
184
579
|
}
|
|
@@ -246,6 +641,8 @@ class SetupWizard {
|
|
|
246
641
|
systemDeps: this.systemTools.filter((tool) => !tool.installed && this.selectedSystemTools.has(tool.tool)).map((tool) => tool.plan),
|
|
247
642
|
exaKey: this.exaInput.getValue().trim(),
|
|
248
643
|
exaKeyPath: resolveExaKeyPath(),
|
|
644
|
+
composioKey: this.composioInput.getValue().trim(),
|
|
645
|
+
composioKeyPath: resolveComposioKeyPath(),
|
|
249
646
|
};
|
|
250
647
|
}
|
|
251
648
|
finalize() {
|
|
@@ -260,81 +657,378 @@ class SetupWizard {
|
|
|
260
657
|
}
|
|
261
658
|
this.onSubmit?.(this.buildSubmission());
|
|
262
659
|
}
|
|
660
|
+
filteredKeybindings() {
|
|
661
|
+
const resolution = this.keyDraft.resolution();
|
|
662
|
+
const needle = this.keyFilter.trim().toLowerCase();
|
|
663
|
+
if (needle === '')
|
|
664
|
+
return BINDING_CATALOG;
|
|
665
|
+
return BINDING_CATALOG.filter((definition) => {
|
|
666
|
+
const rendered = formatBinding(resolution, definition.id);
|
|
667
|
+
return [definition.name, definition.id, definition.group, definition.description, rendered]
|
|
668
|
+
.some((value) => value.toLowerCase().includes(needle));
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
selectedKeybinding() {
|
|
672
|
+
const rows = this.filteredKeybindings();
|
|
673
|
+
if (rows.length === 0)
|
|
674
|
+
return undefined;
|
|
675
|
+
this.keyIndex = Math.max(0, Math.min(this.keyIndex, rows.length - 1));
|
|
676
|
+
return rows[this.keyIndex];
|
|
677
|
+
}
|
|
678
|
+
moveKey(delta) {
|
|
679
|
+
const rows = this.filteredKeybindings();
|
|
680
|
+
if (rows.length === 0)
|
|
681
|
+
return;
|
|
682
|
+
this.keyIndex = Math.max(0, Math.min(rows.length - 1, this.keyIndex + delta));
|
|
683
|
+
const page = this.keyPageSize();
|
|
684
|
+
if (this.keyIndex < this.keyScroll)
|
|
685
|
+
this.keyScroll = this.keyIndex;
|
|
686
|
+
if (this.keyIndex >= this.keyScroll + page)
|
|
687
|
+
this.keyScroll = this.keyIndex - page + 1;
|
|
688
|
+
this.keyEditError = undefined;
|
|
689
|
+
this.invalidate();
|
|
690
|
+
}
|
|
691
|
+
// Max output lines the row region may emit before it starts clipping the
|
|
692
|
+
// confirm prompt / notice / footer. Budgets the fixed chrome around the list:
|
|
693
|
+
// header (3) + blank + tab header/count (2) + blank + footer (1) plus a small
|
|
694
|
+
// reserve for a diagnostic line and the notice/confirm block.
|
|
695
|
+
keyViewportLines() {
|
|
696
|
+
return Math.max(4, this.terminalRows() - 11);
|
|
697
|
+
}
|
|
698
|
+
// Rows moved per PageUp/PageDown and used for scroll clamping. Kept below the
|
|
699
|
+
// raw viewport so interleaved group-heading lines and the selected row's
|
|
700
|
+
// diagnostic line cannot push the selection off-screen.
|
|
701
|
+
keyPageSize() {
|
|
702
|
+
return Math.max(3, this.keyViewportLines() - 4);
|
|
703
|
+
}
|
|
704
|
+
beginKeyEdit() {
|
|
705
|
+
const row = this.selectedKeybinding();
|
|
706
|
+
if (!row)
|
|
707
|
+
return;
|
|
708
|
+
if (this.keyDraft.hasInvalidRoot()) {
|
|
709
|
+
this.keyNotice = `Saved keybindings root is invalid — ${this.label('crtr.setup.keybindings.reset-all')} (reset all) before editing.`;
|
|
710
|
+
this.invalidate();
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
this.editingKey = row.id;
|
|
714
|
+
this.keyInput.setValue(this.keyDraft.expression(row.id));
|
|
715
|
+
// pi Input.setValue only clamps the cursor; seed it, then send ctrl+e
|
|
716
|
+
// (cursorLineEnd) so the cursor sits at end-of-value and the first keystroke
|
|
717
|
+
// appends after the seed instead of prepending before it.
|
|
718
|
+
this.keyInput.handleInput('\x05');
|
|
719
|
+
this.keyInput.focused = this.focused;
|
|
720
|
+
this.keyEditError = undefined;
|
|
721
|
+
this.invalidate();
|
|
722
|
+
}
|
|
723
|
+
commitKeyEdit() {
|
|
724
|
+
if (this.editingKey === null)
|
|
725
|
+
return;
|
|
726
|
+
const result = this.keyDraft.edit(this.editingKey, this.keyInput.getValue());
|
|
727
|
+
if (!result.ok) {
|
|
728
|
+
this.keyEditError = result.diagnostics[0]?.message ?? 'Invalid keybinding';
|
|
729
|
+
this.keyDiagnostics = result.diagnostics;
|
|
730
|
+
this.invalidate();
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
this.editingKey = null;
|
|
734
|
+
this.keyInput.focused = false;
|
|
735
|
+
this.keyEditError = undefined;
|
|
736
|
+
this.keyDiagnostics = this.keyDraft.resolution().diagnostics;
|
|
737
|
+
this.keyNotice = undefined;
|
|
738
|
+
this.invalidate();
|
|
739
|
+
}
|
|
740
|
+
cancelKeyEdit() {
|
|
741
|
+
this.editingKey = null;
|
|
742
|
+
this.keyInput.focused = false;
|
|
743
|
+
this.keyEditError = undefined;
|
|
744
|
+
// Drop the rejected edit's collision diagnostics: the draft never took the
|
|
745
|
+
// offending value, so restore diagnostics from the committed draft state.
|
|
746
|
+
this.keyDiagnostics = this.keyDraft.resolution().diagnostics;
|
|
747
|
+
this.invalidate();
|
|
748
|
+
}
|
|
749
|
+
saveKeybindings() {
|
|
750
|
+
const resolution = this.keyDraft.resolution();
|
|
751
|
+
this.keyDiagnostics = resolution.diagnostics;
|
|
752
|
+
const errors = resolution.diagnostics.filter((diagnostic) => diagnostic.severity === 'error');
|
|
753
|
+
if (errors.length > 0) {
|
|
754
|
+
this.keyNotice = errors[0].message;
|
|
755
|
+
this.invalidate();
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
try {
|
|
759
|
+
const saved = persistUserKeybindings(this.keyDraft.overrides(), {
|
|
760
|
+
expectedKeybindings: this.keyDraft.expectedKeybindings(),
|
|
761
|
+
});
|
|
762
|
+
this.keyDraft.markSaved(saved.overrides);
|
|
763
|
+
this.keyNotice = 'Saved. Reload attach and reopen active views to apply changes; refreshing tmux…';
|
|
764
|
+
// The config is already persisted; a tmux-refresh failure (installer throw)
|
|
765
|
+
// must land in the notice, never as an unhandled rejection that tears down
|
|
766
|
+
// the wizard mid-TUI right after a good save.
|
|
767
|
+
void this.refreshTmuxBindingsAfterSave().catch((error) => {
|
|
768
|
+
this.keyNotice = `Saved, but tmux refresh failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
769
|
+
this.invalidate();
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
catch (error) {
|
|
773
|
+
if (error instanceof KeybindingConcurrentEditError) {
|
|
774
|
+
this.keyNotice = 'Keybindings changed on disk. Reopen setup before saving.';
|
|
775
|
+
}
|
|
776
|
+
else if (error instanceof KeybindingValidationError) {
|
|
777
|
+
this.keyDiagnostics = error.diagnostics;
|
|
778
|
+
this.keyNotice = error.message;
|
|
779
|
+
}
|
|
780
|
+
else {
|
|
781
|
+
this.keyNotice = error instanceof Error ? error.message : String(error);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
this.invalidate();
|
|
785
|
+
}
|
|
786
|
+
async refreshTmuxBindingsAfterSave() {
|
|
787
|
+
// Unit 2's frozen tmux-chrome seam: no-arg install re-reads the
|
|
788
|
+
// just-persisted user config and reconciles the crouter-owned bindings.
|
|
789
|
+
const result = installTmuxBindings();
|
|
790
|
+
const occupied = result.diagnostics.filter((diagnostic) => diagnostic.code === 'occupied');
|
|
791
|
+
if (result.ok && occupied.length === 0) {
|
|
792
|
+
this.keyNotice = 'Saved and applied to tmux. Reload attach and reopen active views.';
|
|
793
|
+
}
|
|
794
|
+
else if (result.ok) {
|
|
795
|
+
this.keyNotice = `Saved; tmux kept occupied keys: ${occupied.map((diagnostic) => diagnostic.message).join('; ')}`;
|
|
796
|
+
}
|
|
797
|
+
else {
|
|
798
|
+
this.keyNotice = `Saved, but tmux refresh failed: ${result.diagnostics.find((diagnostic) => diagnostic.code === 'tmux-error')?.message ?? 'tmux unavailable'}`;
|
|
799
|
+
}
|
|
800
|
+
this.invalidate();
|
|
801
|
+
}
|
|
802
|
+
confirmKeyAction() {
|
|
803
|
+
if (this.keyConfirm === 'reset-all') {
|
|
804
|
+
this.keyDraft.resetAll();
|
|
805
|
+
this.keyDiagnostics = [];
|
|
806
|
+
this.keyNotice = 'All rows reset to catalog defaults. Press save to write.';
|
|
807
|
+
}
|
|
808
|
+
else if (this.keyConfirm === 'discard') {
|
|
809
|
+
this.keyDraft.discard();
|
|
810
|
+
this.keyDiagnostics = this.keyDraft.resolution().diagnostics;
|
|
811
|
+
this.keyNotice = 'Unsaved keybinding changes discarded.';
|
|
812
|
+
}
|
|
813
|
+
this.keyConfirm = null;
|
|
814
|
+
this.invalidate();
|
|
815
|
+
}
|
|
816
|
+
handleKeybindingsInput(data) {
|
|
817
|
+
if (this.keyConfirm !== null) {
|
|
818
|
+
if (this.matches('crtr.setup.confirm.accept', data))
|
|
819
|
+
this.confirmKeyAction();
|
|
820
|
+
else if (this.matches('crtr.setup.confirm.back', data)) {
|
|
821
|
+
this.keyConfirm = null;
|
|
822
|
+
this.invalidate();
|
|
823
|
+
}
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
if (this.editingKey !== null) {
|
|
827
|
+
// Honor the catalog rows for commit/cancel BEFORE the pi Input sees the
|
|
828
|
+
// keystroke, so a remap of `commit-edit`/`cancel-edit` takes effect.
|
|
829
|
+
if (this.matches('crtr.setup.keybindings.commit-edit', data)) {
|
|
830
|
+
this.commitKeyEdit();
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
if (this.matches('crtr.setup.keybindings.cancel-edit', data)) {
|
|
834
|
+
this.cancelKeyEdit();
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if (this.matches('crtr.setup.keybindings.page-up', data)) {
|
|
838
|
+
this.moveKey(-this.keyPageSize());
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
if (this.matches('crtr.setup.keybindings.page-down', data)) {
|
|
842
|
+
this.moveKey(this.keyPageSize());
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
this.keyInput.handleInput(data);
|
|
846
|
+
this.invalidate();
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
if (this.filteringKeys) {
|
|
850
|
+
if (this.matches('crtr.setup.keybindings.filter-clear', data)) {
|
|
851
|
+
this.keyFilter = '';
|
|
852
|
+
this.filteringKeys = false;
|
|
853
|
+
this.keyIndex = 0;
|
|
854
|
+
this.keyScroll = 0;
|
|
855
|
+
}
|
|
856
|
+
else if (this.matches('crtr.setup.keybindings.edit', data)) {
|
|
857
|
+
// Enter commits the filter: leave filter-entry but KEEP keyFilter so the
|
|
858
|
+
// narrowed list can be navigated and its rows edited.
|
|
859
|
+
this.filteringKeys = false;
|
|
860
|
+
}
|
|
861
|
+
else if (this.matches('crtr.setup.keybindings.filter-delete', data)) {
|
|
862
|
+
this.keyFilter = this.keyFilter.slice(0, -1);
|
|
863
|
+
this.keyIndex = 0;
|
|
864
|
+
this.keyScroll = 0;
|
|
865
|
+
}
|
|
866
|
+
else if (data.length === 1 && data.charCodeAt(0) >= 32) {
|
|
867
|
+
this.keyFilter += data;
|
|
868
|
+
this.keyIndex = 0;
|
|
869
|
+
this.keyScroll = 0;
|
|
870
|
+
}
|
|
871
|
+
this.invalidate();
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
if (this.matches('crtr.setup.keybindings.filter', data)) {
|
|
875
|
+
this.filteringKeys = true;
|
|
876
|
+
}
|
|
877
|
+
else if (this.matches('crtr.setup.list-down', data)) {
|
|
878
|
+
this.moveKey(1);
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
else if (this.matches('crtr.setup.list-up', data)) {
|
|
882
|
+
this.moveKey(-1);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
else if (this.matches('crtr.setup.keybindings.page-up', data)) {
|
|
886
|
+
this.moveKey(-this.keyPageSize());
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
else if (this.matches('crtr.setup.keybindings.page-down', data)) {
|
|
890
|
+
this.moveKey(this.keyPageSize());
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
else if (this.matches('crtr.setup.keybindings.edit', data)) {
|
|
894
|
+
this.beginKeyEdit();
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
else if (this.matches('crtr.setup.keybindings.unbind', data)) {
|
|
898
|
+
const row = this.selectedKeybinding();
|
|
899
|
+
if (row) {
|
|
900
|
+
const result = this.keyDraft.unbind(row.id);
|
|
901
|
+
this.keyDiagnostics = result.ok ? this.keyDraft.resolution().diagnostics : result.diagnostics;
|
|
902
|
+
this.keyEditError = result.ok ? undefined : result.diagnostics[0]?.message;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
else if (this.matches('crtr.setup.keybindings.reset-row', data)) {
|
|
906
|
+
const row = this.selectedKeybinding();
|
|
907
|
+
if (row)
|
|
908
|
+
this.keyDraft.resetRow(row.id);
|
|
909
|
+
this.keyDiagnostics = this.keyDraft.resolution().diagnostics;
|
|
910
|
+
}
|
|
911
|
+
else if (this.matches('crtr.setup.keybindings.reset-all', data)) {
|
|
912
|
+
this.keyConfirm = 'reset-all';
|
|
913
|
+
}
|
|
914
|
+
else if (this.matches('crtr.setup.keybindings.save', data)) {
|
|
915
|
+
this.saveKeybindings();
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
else if (this.matches('crtr.setup.keybindings.discard', data)) {
|
|
919
|
+
// A committed (kept) filter is cleared first, before discard is considered.
|
|
920
|
+
if (this.keyFilter !== '') {
|
|
921
|
+
this.keyFilter = '';
|
|
922
|
+
this.keyIndex = 0;
|
|
923
|
+
this.keyScroll = 0;
|
|
924
|
+
this.keyNotice = undefined;
|
|
925
|
+
}
|
|
926
|
+
else if (this.keyDraft.dirty)
|
|
927
|
+
this.keyConfirm = 'discard';
|
|
928
|
+
else
|
|
929
|
+
this.keyNotice = 'No unsaved keybinding changes.';
|
|
930
|
+
}
|
|
931
|
+
else {
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
this.invalidate();
|
|
935
|
+
}
|
|
263
936
|
handleInput(data) {
|
|
937
|
+
// A nested provider login owns all input until it settles. In particular, do not let
|
|
938
|
+
// wizard-wide q/Tab navigation abandon the panel while its OAuth promise can still resolve.
|
|
939
|
+
if (this.activeTab === 0 && this.providers.isNestedFlowActive()) {
|
|
940
|
+
if (this.providers.handleInput(data))
|
|
941
|
+
this.invalidate();
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
264
944
|
if (this.confirming) {
|
|
265
|
-
if (
|
|
945
|
+
if (this.matches('crtr.setup.confirm.accept', data)) {
|
|
266
946
|
this.onSubmit?.(this.buildSubmission());
|
|
267
947
|
return;
|
|
268
948
|
}
|
|
269
|
-
if (
|
|
949
|
+
if (this.matches('crtr.setup.confirm.back', data)) {
|
|
270
950
|
this.confirming = false;
|
|
271
951
|
this.invalidate();
|
|
272
|
-
return;
|
|
273
952
|
}
|
|
274
953
|
return;
|
|
275
954
|
}
|
|
276
|
-
|
|
955
|
+
// Keybindings sub-states (editing / filtering / confirm) own input before the
|
|
956
|
+
// wizard-level cancel and tab gestures can steal it.
|
|
957
|
+
if (this.activeTab === 5 && (this.editingKey !== null || this.filteringKeys || this.keyConfirm !== null)) {
|
|
958
|
+
this.handleKeybindingsInput(data);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
// Printable chars and arrows on the API-key tabs go to the focused field.
|
|
962
|
+
if ((this.activeTab === 3 || this.activeTab === 4)
|
|
963
|
+
&& ((data.length === 1 && data.charCodeAt(0) >= 32)
|
|
964
|
+
|| matchesKey(data, 'left') || matchesKey(data, 'right') || matchesKey(data, 'up') || matchesKey(data, 'down'))) {
|
|
965
|
+
(this.activeTab === 3 ? this.exaInput : this.composioInput).handleInput(data);
|
|
966
|
+
this.invalidate();
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
// On the keybindings tab in list mode, a dirty draft must not be dropped
|
|
970
|
+
// silently by the wizard-level cancel gesture — route it through the same
|
|
971
|
+
// discard-confirm the discard path uses.
|
|
972
|
+
if (this.activeTab === 5 && this.keyDraft.dirty && this.matches('crtr.setup.cancel', data)) {
|
|
973
|
+
this.keyConfirm = 'discard';
|
|
974
|
+
this.invalidate();
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
if (this.matches('crtr.setup.cancel', data)) {
|
|
277
978
|
this.cancel();
|
|
278
979
|
return;
|
|
279
980
|
}
|
|
280
981
|
if (this.handleGlobalTabSwitch(data))
|
|
281
982
|
return;
|
|
282
983
|
if (this.activeTab === 0) {
|
|
283
|
-
if (
|
|
284
|
-
this.
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
984
|
+
if (this.providers.handleInput(data))
|
|
985
|
+
this.invalidate();
|
|
287
986
|
return;
|
|
288
987
|
}
|
|
289
988
|
if (this.activeTab === 1) {
|
|
290
|
-
if (
|
|
989
|
+
if (this.matches('crtr.setup.list-down', data))
|
|
291
990
|
this.movePlugin(1);
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
if (matchesKey(data, 'k') || matchesKey(data, 'up')) {
|
|
991
|
+
else if (this.matches('crtr.setup.list-up', data))
|
|
295
992
|
this.movePlugin(-1);
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
if (matchesKey(data, 'space')) {
|
|
993
|
+
else if (this.matches('crtr.setup.toggle', data))
|
|
299
994
|
this.togglePackage();
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
if (matchesKey(data, '?')) {
|
|
995
|
+
else if (this.matches('crtr.setup.details', data))
|
|
303
996
|
this.toggleDetails();
|
|
304
|
-
|
|
305
|
-
}
|
|
306
|
-
if (matchesKey(data, 'enter')) {
|
|
997
|
+
else if (this.matches('crtr.setup.advance', data))
|
|
307
998
|
this.moveTab(1);
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
999
|
return;
|
|
311
1000
|
}
|
|
312
1001
|
if (this.activeTab === 2) {
|
|
313
|
-
if (
|
|
1002
|
+
if (this.matches('crtr.setup.list-down', data))
|
|
314
1003
|
this.moveSystem(1);
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
if (matchesKey(data, 'k') || matchesKey(data, 'up')) {
|
|
1004
|
+
else if (this.matches('crtr.setup.list-up', data))
|
|
318
1005
|
this.moveSystem(-1);
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
if (matchesKey(data, 'space')) {
|
|
1006
|
+
else if (this.matches('crtr.setup.toggle', data))
|
|
322
1007
|
this.toggleSystem();
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
-
if (matchesKey(data, 'enter')) {
|
|
1008
|
+
else if (this.matches('crtr.setup.advance', data))
|
|
326
1009
|
this.moveTab(1);
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
1010
|
return;
|
|
330
1011
|
}
|
|
331
|
-
this.
|
|
1012
|
+
if (this.activeTab === 3) {
|
|
1013
|
+
this.exaInput.handleInput(data);
|
|
1014
|
+
this.invalidate();
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
if (this.activeTab === 4) {
|
|
1018
|
+
this.composioInput.handleInput(data);
|
|
1019
|
+
this.invalidate();
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
this.handleKeybindingsInput(data);
|
|
332
1023
|
}
|
|
333
1024
|
renderHeader(width) {
|
|
334
|
-
const tabs = ['
|
|
335
|
-
const
|
|
336
|
-
.map((label, index) => tabPill(
|
|
1025
|
+
const tabs = ['Providers', 'Plugins', 'System', 'Exa', 'Composio', 'Keybindings'];
|
|
1026
|
+
const fullTabLine = tabs
|
|
1027
|
+
.map((label, index) => tabPill(label, index === this.activeTab))
|
|
337
1028
|
.join(theme.fg('dim', ' · '));
|
|
1029
|
+
const tabLine = visibleWidth(fullTabLine) <= width
|
|
1030
|
+
? fullTabLine
|
|
1031
|
+
: `${theme.fg('dim', `${this.activeTab + 1}/${tabs.length}`)} ${tabPill(tabs[this.activeTab], true)}`;
|
|
338
1032
|
return [
|
|
339
1033
|
truncateToWidth(theme.fg('accent', theme.bold('crtr sys setup')), width),
|
|
340
1034
|
truncateToWidth(tabLine, width),
|
|
@@ -342,41 +1036,32 @@ class SetupWizard {
|
|
|
342
1036
|
];
|
|
343
1037
|
}
|
|
344
1038
|
renderFooter(width) {
|
|
1039
|
+
const tabs = `${this.label('crtr.setup.tab-next')} next · ${this.label('crtr.setup.tab-previous')} prev`;
|
|
345
1040
|
const line = this.confirming
|
|
346
|
-
? '
|
|
347
|
-
: this.activeTab === 0
|
|
348
|
-
? '
|
|
349
|
-
: this.activeTab ===
|
|
350
|
-
?
|
|
351
|
-
: this.activeTab ===
|
|
352
|
-
? '
|
|
353
|
-
:
|
|
1041
|
+
? `${this.label('crtr.setup.confirm.accept')} confirm · ${this.label('crtr.setup.confirm.back')} go back`
|
|
1042
|
+
: this.activeTab === 0 && this.providers.isNestedFlowActive()
|
|
1043
|
+
? 'OAuth in progress · esc cancel'
|
|
1044
|
+
: this.activeTab === 0
|
|
1045
|
+
? `↑/↓ select · a add · p prefer · r rename · l re-login · x remove · ${this.label('crtr.setup.tab-next')} next · ${this.label('crtr.setup.cancel')} cancel`
|
|
1046
|
+
: this.activeTab === 1
|
|
1047
|
+
? `${this.label('crtr.setup.list-down')}/${this.label('crtr.setup.list-up')} move · ${this.label('crtr.setup.toggle')} toggle · ${this.label('crtr.setup.details')} details · ${this.label('crtr.setup.advance')} advance · ${tabs} · ${this.label('crtr.setup.cancel')} cancel`
|
|
1048
|
+
: this.activeTab === 2
|
|
1049
|
+
? `${this.label('crtr.setup.list-down')}/${this.label('crtr.setup.list-up')} move · ${this.label('crtr.setup.toggle')} toggle · ${this.label('crtr.setup.advance')} advance · ${tabs} · ${this.label('crtr.setup.cancel')} cancel`
|
|
1050
|
+
: this.activeTab === 3
|
|
1051
|
+
? `type EXA_API_KEY · Enter finish · ${this.fieldSafeLabel('crtr.setup.tab-next')}/${this.fieldSafeLabel('crtr.setup.tab-previous')} switch tabs · Esc or ${this.fieldSafeLabel('crtr.setup.cancel')} cancel`
|
|
1052
|
+
: this.activeTab === 4
|
|
1053
|
+
? `type COMPOSIO_API_KEY · Enter finish · ${this.fieldSafeLabel('crtr.setup.tab-next')}/${this.fieldSafeLabel('crtr.setup.tab-previous')} switch tabs · Esc or ${this.fieldSafeLabel('crtr.setup.cancel')} cancel`
|
|
1054
|
+
: this.keyConfirm !== null
|
|
1055
|
+
? `${this.label('crtr.setup.confirm.accept')} confirm · ${this.label('crtr.setup.confirm.back')} go back`
|
|
1056
|
+
: this.editingKey !== null
|
|
1057
|
+
? `type gestures separated by commas · ${this.label('crtr.setup.keybindings.commit-edit')} commit · ${this.label('crtr.setup.keybindings.cancel-edit')} cancel · ${this.label('crtr.setup.keybindings.page-up')}/${this.label('crtr.setup.keybindings.page-down')} scroll`
|
|
1058
|
+
: this.filteringKeys
|
|
1059
|
+
? `filter: ${this.keyFilter || 'type to search'} · ${this.label('crtr.setup.keybindings.edit')} keep · ${this.label('crtr.setup.keybindings.filter-delete')} delete · ${this.label('crtr.setup.keybindings.filter-clear')} clear`
|
|
1060
|
+
: `${this.label('crtr.setup.list-down')}/${this.label('crtr.setup.list-up')} move · ${this.label('crtr.setup.keybindings.edit')} edit · ${this.label('crtr.setup.keybindings.unbind')} disable · ${this.label('crtr.setup.keybindings.reset-row')} reset · ${this.label('crtr.setup.keybindings.reset-all')} reset all · ${this.label('crtr.setup.keybindings.filter')} filter · ${this.label('crtr.setup.keybindings.save')} save · ${this.label('crtr.setup.keybindings.discard')} ${this.keyFilter !== '' ? 'clear filter' : 'discard'}`;
|
|
354
1061
|
return [truncateToWidth(theme.fg('dim', line), width)];
|
|
355
1062
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
out.push(truncateToWidth(`${theme.fg('accent', theme.bold('Login'))}${theme.fg('dim', ' optional · same providers as /login')}`, width));
|
|
359
|
-
for (const line of wrapText('Log in to a model provider so crtr can run turns. With no provider configured, nodes fail to start when you first use the app.', Math.max(1, width))) {
|
|
360
|
-
out.push(truncateToWidth(theme.fg('muted', line), width));
|
|
361
|
-
}
|
|
362
|
-
out.push('');
|
|
363
|
-
const configured = this.authSummary.filter((p) => p.configured);
|
|
364
|
-
if (configured.length > 0) {
|
|
365
|
-
out.push(truncateToWidth(theme.fg('success', `✓ Configured: ${configured.map((p) => p.name).join(', ')}`), width));
|
|
366
|
-
}
|
|
367
|
-
else {
|
|
368
|
-
out.push(truncateToWidth(theme.fg('warning', 'No provider configured yet.'), width));
|
|
369
|
-
}
|
|
370
|
-
out.push('');
|
|
371
|
-
out.push(...framed([
|
|
372
|
-
theme.fg('accent', theme.bold('Press Enter to open the provider picker')),
|
|
373
|
-
theme.fg('dim', 'Pick a provider, then complete its OAuth or API-key login.'),
|
|
374
|
-
], width));
|
|
375
|
-
if (this.notice !== undefined) {
|
|
376
|
-
out.push('');
|
|
377
|
-
out.push(truncateToWidth(theme.fg('success', this.notice), width));
|
|
378
|
-
}
|
|
379
|
-
return out;
|
|
1063
|
+
renderProviders(width) {
|
|
1064
|
+
return this.providers.render(width);
|
|
380
1065
|
}
|
|
381
1066
|
renderPlugins(width) {
|
|
382
1067
|
const out = [];
|
|
@@ -420,21 +1105,33 @@ class SetupWizard {
|
|
|
420
1105
|
}
|
|
421
1106
|
renderExa(width) {
|
|
422
1107
|
const out = [];
|
|
423
|
-
out.push(truncateToWidth(`${theme.fg('accent', theme.bold('Exa key'))}${theme.fg('dim', ' optional')}`, width));
|
|
1108
|
+
out.push(truncateToWidth(`${theme.fg('accent', theme.bold('Exa key'))}${theme.fg('dim', ' optional')}${keyStatusBadge(detectExaKey(), 'EXA_API_KEY')}`, width));
|
|
424
1109
|
out.push(truncateToWidth(`${theme.fg('mdLink', 'Create a key: ')}${theme.fg('mdLinkUrl', 'https://dashboard.exa.ai/api-keys')}`, width));
|
|
425
1110
|
out.push(truncateToWidth(theme.fg('dim', `Will write to: ${resolveExaKeyPath()}`), width));
|
|
426
1111
|
out.push(...framed([
|
|
427
1112
|
theme.fg('accent', theme.bold('Paste EXA_API_KEY here')),
|
|
428
|
-
theme.fg('dim', 'Leave blank to skip.'),
|
|
1113
|
+
theme.fg('dim', detectExaKey().present ? 'Leave blank to keep the existing key; paste to replace it.' : 'Leave blank to skip.'),
|
|
429
1114
|
...this.exaInput.render(Math.max(1, width - 4)),
|
|
430
1115
|
], width));
|
|
431
1116
|
return out;
|
|
432
1117
|
}
|
|
1118
|
+
renderComposio(width) {
|
|
1119
|
+
const out = [];
|
|
1120
|
+
out.push(truncateToWidth(`${theme.fg('accent', theme.bold('Composio key'))}${theme.fg('dim', ' optional · powers /v1/connectors third-party service access')}${keyStatusBadge(detectComposioKey(), 'COMPOSIO_API_KEY')}`, width));
|
|
1121
|
+
out.push(truncateToWidth(`${theme.fg('mdLink', 'Create a key: ')}${theme.fg('mdLinkUrl', 'https://platform.composio.dev/developers')}`, width));
|
|
1122
|
+
out.push(truncateToWidth(theme.fg('dim', `Will write to: ${resolveComposioKeyPath()}`), width));
|
|
1123
|
+
out.push(...framed([
|
|
1124
|
+
theme.fg('accent', theme.bold('Paste COMPOSIO_API_KEY here')),
|
|
1125
|
+
theme.fg('dim', detectComposioKey().present ? 'Leave blank to keep the existing key; paste to replace it.' : 'Leave blank to skip.'),
|
|
1126
|
+
...this.composioInput.render(Math.max(1, width - 4)),
|
|
1127
|
+
], width));
|
|
1128
|
+
return out;
|
|
1129
|
+
}
|
|
433
1130
|
renderConfirm(width) {
|
|
434
1131
|
const out = [truncateToWidth(theme.fg('warning', theme.bold('Confirm installs')), width)];
|
|
435
1132
|
const submission = this.buildSubmission();
|
|
436
|
-
if (submission.packages.length === 0 && submission.systemDeps.length === 0 && submission.exaKey === '') {
|
|
437
|
-
out.push(truncateToWidth(theme.fg('success',
|
|
1133
|
+
if (submission.packages.length === 0 && submission.systemDeps.length === 0 && submission.exaKey === '' && submission.composioKey === '') {
|
|
1134
|
+
out.push(truncateToWidth(theme.fg('success', `Nothing to install; ${this.label('crtr.setup.confirm.accept')} finish.`), width));
|
|
438
1135
|
return out;
|
|
439
1136
|
}
|
|
440
1137
|
if (submission.packages.length > 0) {
|
|
@@ -455,7 +1152,122 @@ class SetupWizard {
|
|
|
455
1152
|
if (submission.exaKey !== '') {
|
|
456
1153
|
out.push(truncateToWidth(`${theme.fg('accent', 'Exa key')} ${theme.fg('dim', '→')} ${theme.fg('warning', `write ${submission.exaKeyPath}`)}`, width));
|
|
457
1154
|
}
|
|
458
|
-
|
|
1155
|
+
if (submission.composioKey !== '') {
|
|
1156
|
+
out.push(truncateToWidth(`${theme.fg('accent', 'Composio key')} ${theme.fg('dim', '→')} ${theme.fg('warning', `write ${submission.composioKeyPath}`)}`, width));
|
|
1157
|
+
}
|
|
1158
|
+
out.push(truncateToWidth(theme.fg('dim', `${this.label('crtr.setup.confirm.accept')} confirm · ${this.label('crtr.setup.confirm.back')} go back`), width));
|
|
1159
|
+
return out;
|
|
1160
|
+
}
|
|
1161
|
+
// Does the row at `index` contribute a diagnostic line in the current state?
|
|
1162
|
+
// Mirrors the emit loop's `message` computation so the line-cost accounting
|
|
1163
|
+
// and the render stay in lockstep.
|
|
1164
|
+
keyRowHasMessage(rows, index) {
|
|
1165
|
+
const definition = rows[index];
|
|
1166
|
+
const selected = index === this.keyIndex;
|
|
1167
|
+
const editing = this.editingKey === definition.id;
|
|
1168
|
+
if (!selected && !editing)
|
|
1169
|
+
return false;
|
|
1170
|
+
if (editing && this.keyEditError)
|
|
1171
|
+
return true;
|
|
1172
|
+
const diagnostic = this.keyDiagnostics.find((entry) => entry.bindingId === definition.id || entry.relatedBindingIds?.includes(definition.id));
|
|
1173
|
+
return diagnostic !== undefined;
|
|
1174
|
+
}
|
|
1175
|
+
// Output lines the visible rows [start..end] cost, counting the per-row group
|
|
1176
|
+
// heading (the first visible row always re-emits its heading) and any
|
|
1177
|
+
// diagnostic line. Used to clamp the scroll so the anchor row always renders.
|
|
1178
|
+
keyLineCost(rows, start, end) {
|
|
1179
|
+
let lines = 0;
|
|
1180
|
+
let previousGroup;
|
|
1181
|
+
for (let index = start; index <= end; index += 1) {
|
|
1182
|
+
const definition = rows[index];
|
|
1183
|
+
lines += (definition.group !== previousGroup ? 1 : 0) + 1 + (this.keyRowHasMessage(rows, index) ? 1 : 0);
|
|
1184
|
+
previousGroup = definition.group;
|
|
1185
|
+
}
|
|
1186
|
+
return lines;
|
|
1187
|
+
}
|
|
1188
|
+
renderKeybindings(width) {
|
|
1189
|
+
const out = [];
|
|
1190
|
+
const rows = this.filteredKeybindings();
|
|
1191
|
+
const resolution = this.keyDraft.resolution();
|
|
1192
|
+
const viewport = this.keyViewportLines();
|
|
1193
|
+
const pageSize = this.keyPageSize();
|
|
1194
|
+
const maxScroll = Math.max(0, rows.length - pageSize);
|
|
1195
|
+
this.keyScroll = Math.max(0, Math.min(this.keyScroll, maxScroll));
|
|
1196
|
+
if (this.keyIndex < this.keyScroll)
|
|
1197
|
+
this.keyScroll = this.keyIndex;
|
|
1198
|
+
if (this.keyIndex >= this.keyScroll + pageSize)
|
|
1199
|
+
this.keyScroll = Math.max(0, this.keyIndex - pageSize + 1);
|
|
1200
|
+
// Anchor that must stay on screen: the row being edited (so the editor never
|
|
1201
|
+
// vanishes while paging moves the selection — m3), else the selection itself.
|
|
1202
|
+
// Injected group-heading and diagnostic lines can make a pageSize-row window
|
|
1203
|
+
// exceed the raw viewport, so instead of a fixed reserve, clamp the scroll
|
|
1204
|
+
// dynamically until the anchor is guaranteed to render within `viewport`.
|
|
1205
|
+
const editIndex = this.editingKey !== null ? rows.findIndex((row) => row.id === this.editingKey) : -1;
|
|
1206
|
+
const anchor = editIndex >= 0 ? editIndex : this.keyIndex;
|
|
1207
|
+
if (anchor >= 0) {
|
|
1208
|
+
if (anchor < this.keyScroll)
|
|
1209
|
+
this.keyScroll = anchor;
|
|
1210
|
+
while (this.keyScroll < anchor && this.keyLineCost(rows, this.keyScroll, anchor) > viewport) {
|
|
1211
|
+
this.keyScroll += 1;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
const dirty = this.keyDraft.dirty ? theme.fg('warning', 'unsaved') : theme.fg('success', 'saved');
|
|
1215
|
+
out.push(truncateToWidth(`${theme.fg('accent', theme.bold('Keybindings'))}${theme.fg('dim', ' crouter-owned only · ')}${dirty}`, width));
|
|
1216
|
+
out.push(truncateToWidth(theme.fg('dim', this.keyFilter === ''
|
|
1217
|
+
? `${rows.length} actions`
|
|
1218
|
+
: `${rows.length} matches for “${this.keyFilter}”${this.filteringKeys ? '' : ` · ${this.label('crtr.setup.keybindings.discard')} clears filter`}`), width));
|
|
1219
|
+
if (resolution.diagnostics.some((diagnostic) => diagnostic.code === 'invalid-keybindings')) {
|
|
1220
|
+
out.push(truncateToWidth(theme.fg('warning', theme.bold(`Saved keybindings root is invalid — ${this.label('crtr.setup.keybindings.reset-all')} (reset all) to clear it before editing.`)), width));
|
|
1221
|
+
}
|
|
1222
|
+
if (rows.length === 0) {
|
|
1223
|
+
out.push('');
|
|
1224
|
+
out.push(truncateToWidth(theme.fg('warning', 'No matching keybindings.'), width));
|
|
1225
|
+
return out;
|
|
1226
|
+
}
|
|
1227
|
+
const nameWidth = Math.max(16, Math.min(28, Math.floor(width * 0.28)));
|
|
1228
|
+
const keyWidth = Math.max(18, Math.min(34, Math.floor(width * 0.34)));
|
|
1229
|
+
const showDescription = width >= 72;
|
|
1230
|
+
let emitted = 0;
|
|
1231
|
+
let previousGroup;
|
|
1232
|
+
for (let index = this.keyScroll; index < rows.length; index += 1) {
|
|
1233
|
+
const definition = rows[index];
|
|
1234
|
+
const selected = index === this.keyIndex;
|
|
1235
|
+
const editing = this.editingKey === definition.id;
|
|
1236
|
+
const diagnostics = (selected || editing)
|
|
1237
|
+
? this.keyDiagnostics.filter((diagnostic) => diagnostic.bindingId === definition.id || diagnostic.relatedBindingIds?.includes(definition.id))
|
|
1238
|
+
: [];
|
|
1239
|
+
const message = editing ? (this.keyEditError ?? diagnostics[0]?.message) : (selected ? diagnostics[0]?.message : undefined);
|
|
1240
|
+
// Budget output lines (heading + row + optional diagnostic) so the list
|
|
1241
|
+
// never clips the confirm prompt / notice / footer below it.
|
|
1242
|
+
const cost = (definition.group !== previousGroup ? 1 : 0) + 1 + (message ? 1 : 0);
|
|
1243
|
+
if (emitted > 0 && emitted + cost > viewport)
|
|
1244
|
+
break;
|
|
1245
|
+
if (definition.group !== previousGroup) {
|
|
1246
|
+
out.push(truncateToWidth(theme.fg('accent', definition.group), width));
|
|
1247
|
+
previousGroup = definition.group;
|
|
1248
|
+
}
|
|
1249
|
+
const marker = selected ? theme.fg('accent', '▶') : ' ';
|
|
1250
|
+
const sourceMark = resolution.get(definition.id).source === 'user' ? theme.fg('warning', '•') : ' ';
|
|
1251
|
+
const name = padAnsi(selected ? theme.fg('accent', definition.name) : definition.name, nameWidth);
|
|
1252
|
+
// Render the edit field whenever THIS row is the one being edited — even if
|
|
1253
|
+
// paging has moved the selection elsewhere — so the active editor stays
|
|
1254
|
+
// visible while it captures input.
|
|
1255
|
+
const keyText = editing ? (this.keyInput.render(keyWidth)[0] ?? '') : formatBinding(resolution, definition.id);
|
|
1256
|
+
const key = padAnsi(keyText, keyWidth);
|
|
1257
|
+
const description = showDescription ? ` ${theme.fg('dim', definition.description)}` : '';
|
|
1258
|
+
out.push(truncateToWidth(`${marker}${sourceMark} ${name} ${key}${description}`, width));
|
|
1259
|
+
if (message)
|
|
1260
|
+
out.push(truncateToWidth(theme.fg('warning', ` ${message}`), width));
|
|
1261
|
+
emitted += cost;
|
|
1262
|
+
}
|
|
1263
|
+
if (this.keyConfirm !== null) {
|
|
1264
|
+
out.push('');
|
|
1265
|
+
out.push(truncateToWidth(theme.fg('warning', theme.bold(this.keyConfirm === 'reset-all'
|
|
1266
|
+
? 'Reset every keybinding to its catalog default?'
|
|
1267
|
+
: 'Discard every unsaved keybinding change?')), width));
|
|
1268
|
+
}
|
|
1269
|
+
if (this.keyNotice)
|
|
1270
|
+
out.push(truncateToWidth(theme.fg('muted', this.keyNotice), width));
|
|
459
1271
|
return out;
|
|
460
1272
|
}
|
|
461
1273
|
render(width) {
|
|
@@ -467,12 +1279,16 @@ class SetupWizard {
|
|
|
467
1279
|
...(this.confirming
|
|
468
1280
|
? this.renderConfirm(width)
|
|
469
1281
|
: this.activeTab === 0
|
|
470
|
-
? this.
|
|
1282
|
+
? this.renderProviders(width)
|
|
471
1283
|
: this.activeTab === 1
|
|
472
1284
|
? this.renderPlugins(width)
|
|
473
1285
|
: this.activeTab === 2
|
|
474
1286
|
? this.renderSystems(width)
|
|
475
|
-
: this.
|
|
1287
|
+
: this.activeTab === 3
|
|
1288
|
+
? this.renderExa(width)
|
|
1289
|
+
: this.activeTab === 4
|
|
1290
|
+
? this.renderComposio(width)
|
|
1291
|
+
: this.renderKeybindings(width)),
|
|
476
1292
|
'',
|
|
477
1293
|
...this.renderFooter(width),
|
|
478
1294
|
];
|
|
@@ -481,52 +1297,6 @@ class SetupWizard {
|
|
|
481
1297
|
return this.cachedLines;
|
|
482
1298
|
}
|
|
483
1299
|
}
|
|
484
|
-
const LOGIN_OVERLAY_OPTIONS = { anchor: 'center', width: '80%', maxHeight: '85%' };
|
|
485
|
-
/** Mount pi's real `/login` provider picker + login dialog as a centered overlay
|
|
486
|
-
* over the setup wizard, driving the SAME OAuth/API-key flow the in-app /login
|
|
487
|
-
* uses (`buildLoginPicker`). Credentials write to `agentDir`'s auth.json; on
|
|
488
|
-
* close, focus returns to the wizard and its auth summary refreshes. */
|
|
489
|
-
function openLoginOverlay(tui, wizard, agentDir) {
|
|
490
|
-
let handle;
|
|
491
|
-
let current;
|
|
492
|
-
let done = false;
|
|
493
|
-
const disposeCurrent = () => {
|
|
494
|
-
const disposable = current?.component;
|
|
495
|
-
try {
|
|
496
|
-
disposable?.dispose?.();
|
|
497
|
-
}
|
|
498
|
-
catch {
|
|
499
|
-
/* ignore dispose errors during teardown */
|
|
500
|
-
}
|
|
501
|
-
};
|
|
502
|
-
const mount = (p) => {
|
|
503
|
-
handle = tui.showOverlay(p.component, LOGIN_OVERLAY_OPTIONS);
|
|
504
|
-
tui.setFocus(p.focus);
|
|
505
|
-
};
|
|
506
|
-
const unmount = () => handle?.hide();
|
|
507
|
-
const replace = (component, focus) => {
|
|
508
|
-
if (done)
|
|
509
|
-
return;
|
|
510
|
-
disposeCurrent();
|
|
511
|
-
unmount();
|
|
512
|
-
current = { component, focus };
|
|
513
|
-
mount(current);
|
|
514
|
-
tui.requestRender();
|
|
515
|
-
};
|
|
516
|
-
const close = () => {
|
|
517
|
-
if (done)
|
|
518
|
-
return;
|
|
519
|
-
done = true;
|
|
520
|
-
disposeCurrent();
|
|
521
|
-
unmount();
|
|
522
|
-
wizard.refreshAuth();
|
|
523
|
-
tui.setFocus(wizard);
|
|
524
|
-
tui.requestRender();
|
|
525
|
-
};
|
|
526
|
-
current = buildLoginPicker(tui, agentDir, () => wizard.refreshAuth(), (msg) => wizard.setNotice(msg), { close, replace });
|
|
527
|
-
mount(current);
|
|
528
|
-
tui.requestRender();
|
|
529
|
-
}
|
|
530
1300
|
async function runSetup() {
|
|
531
1301
|
const state = buildSetupInitialState();
|
|
532
1302
|
if (!process.stdout.isTTY) {
|
|
@@ -535,18 +1305,12 @@ async function runSetup() {
|
|
|
535
1305
|
}
|
|
536
1306
|
applyTheme({ cwd: process.cwd() });
|
|
537
1307
|
const agentDir = defaultAgentDir();
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
}
|
|
542
|
-
catch {
|
|
543
|
-
return [];
|
|
544
|
-
}
|
|
545
|
-
};
|
|
1308
|
+
const rawUserConfig = readRawScopeConfig('user');
|
|
1309
|
+
const rawKeybindings = rawUserConfig?.keybindings;
|
|
1310
|
+
const openingBindings = resolveKeybindings(rawKeybindings);
|
|
546
1311
|
const terminal = new ProcessTerminal();
|
|
547
1312
|
const tui = new TUI(terminal, true);
|
|
548
|
-
const wizard = new SetupWizard(state.manifest, state.installedPackageIds, state.systemTools,
|
|
549
|
-
wizard.onOpenLogin = () => openLoginOverlay(tui, wizard, agentDir);
|
|
1313
|
+
const wizard = new SetupWizard(state.manifest, state.installedPackageIds, state.systemTools, tui, agentDir, openingBindings, rawKeybindings, () => terminal.rows);
|
|
550
1314
|
tui.addChild(wizard);
|
|
551
1315
|
tui.setFocus(wizard);
|
|
552
1316
|
await new Promise((resolve, reject) => {
|
|
@@ -555,6 +1319,7 @@ async function runSetup() {
|
|
|
555
1319
|
if (restored)
|
|
556
1320
|
return;
|
|
557
1321
|
restored = true;
|
|
1322
|
+
wizard.dispose();
|
|
558
1323
|
try {
|
|
559
1324
|
tui.stop();
|
|
560
1325
|
}
|
|
@@ -582,6 +1347,21 @@ async function runSetup() {
|
|
|
582
1347
|
process.once('SIGTERM', onSignal);
|
|
583
1348
|
process.once('SIGHUP', onSignal);
|
|
584
1349
|
process.once('SIGINT', onSignal);
|
|
1350
|
+
// Emergency cancel: the control gestures of `crtr.setup.cancel` (ctrl+c /
|
|
1351
|
+
// ctrl+d in the shipped catalog) must always exit setup — from filter entry,
|
|
1352
|
+
// the confirm prompts, or while a nested ProviderPanel login flow owns focus —
|
|
1353
|
+
// where the focused component would otherwise swallow them. Matching is driven
|
|
1354
|
+
// by the opening snapshot; the control-char guard keeps printable cancel gestures
|
|
1355
|
+
// (e.g. q) as data for text fields and the filter buffer, which the wizard
|
|
1356
|
+
// handles as cancel only in its own non-text states.
|
|
1357
|
+
tui.addInputListener((data) => {
|
|
1358
|
+
if (data.length > 0 && data.charCodeAt(0) < 32
|
|
1359
|
+
&& matchesPiTuiInput(openingBindings, 'crtr.setup.cancel', data, (input, gesture) => matchesKey(input, gesture))) {
|
|
1360
|
+
finish();
|
|
1361
|
+
return { consume: true };
|
|
1362
|
+
}
|
|
1363
|
+
return undefined;
|
|
1364
|
+
});
|
|
585
1365
|
wizard.onCancel = finish;
|
|
586
1366
|
wizard.onSubmit = (submission) => {
|
|
587
1367
|
restore();
|
|
@@ -594,13 +1374,6 @@ async function runSetup() {
|
|
|
594
1374
|
resolve();
|
|
595
1375
|
}, fail);
|
|
596
1376
|
};
|
|
597
|
-
tui.addInputListener((data) => {
|
|
598
|
-
if (matchesKey(data, 'ctrl+c') || matchesKey(data, 'ctrl+d')) {
|
|
599
|
-
finish();
|
|
600
|
-
return { consume: true };
|
|
601
|
-
}
|
|
602
|
-
return undefined;
|
|
603
|
-
});
|
|
604
1377
|
try {
|
|
605
1378
|
tui.start();
|
|
606
1379
|
}
|
|
@@ -611,18 +1384,19 @@ async function runSetup() {
|
|
|
611
1384
|
}
|
|
612
1385
|
export const sysSetupLeaf = defineLeaf({
|
|
613
1386
|
name: 'setup',
|
|
614
|
-
description: '
|
|
615
|
-
whenToUse: 'you want to bootstrap this machine
|
|
1387
|
+
description: 'manage model providers, install bundled pi plugins and system deps, save Exa + Composio keys, and edit crouter keybindings',
|
|
1388
|
+
whenToUse: 'you want to bootstrap this machine in one pass: inspect and manage configured model providers, install vendored pi plugins, confirm tmux and nvim, save Exa and Composio API keys, and edit crouter-owned keybindings.',
|
|
616
1389
|
help: {
|
|
617
1390
|
name: 'sys setup',
|
|
618
|
-
summary: 'open the multi-tab setup wizard for provider
|
|
1391
|
+
summary: 'open the multi-tab setup wizard for provider management, bundled pi plugins, system deps, Exa + Composio keys, and crouter-owned keybindings',
|
|
619
1392
|
inputNote: 'No input parameters.',
|
|
620
1393
|
output: [],
|
|
621
1394
|
outputKind: 'object',
|
|
622
1395
|
effects: [
|
|
623
1396
|
'Opens a pi-tui wizard whose first tab logs in to a model provider (pi\'s real OAuth/API-key flow, same as in-app /login) so a fresh install has credentials before running any node.',
|
|
624
|
-
'Collects companion package installs, system dependency installs, and
|
|
625
|
-
'
|
|
1397
|
+
'Collects companion package installs, system dependency installs, API keys, and sparse user overrides for crouter-owned keybindings across the remaining tabs.',
|
|
1398
|
+
'Saves validated keybinding changes atomically in the user config and refreshes crouter-owned tmux bindings without overwriting occupied user keys.',
|
|
1399
|
+
'Runs the bundled pi CLI to install the selected companion packages, writes EXA_API_KEY to ~/.crouter/exa.key and COMPOSIO_API_KEY to ~/.crouter/composio.key when provided, and prints a summary.',
|
|
626
1400
|
],
|
|
627
1401
|
},
|
|
628
1402
|
run: async () => {
|