@pellux/goodvibes-tui 0.20.3 → 0.22.0
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 +50 -0
- package/README.md +23 -2
- package/docs/foundation-artifacts/operator-contract.json +78 -1
- package/package.json +4 -2
- package/src/audio/spoken-turn-controller.ts +31 -1
- package/src/audio/spoken-turn-wiring.ts +26 -4
- package/src/cli/bundle-command.ts +1 -1
- package/src/cli/completions/generate.ts +658 -0
- package/src/cli/config-overrides.ts +68 -0
- package/src/cli/entrypoint.ts +6 -0
- package/src/cli/help.ts +4 -2
- package/src/cli/management-commands.ts +1 -1
- package/src/cli/management.ts +1 -8
- package/src/cli/parser.ts +31 -18
- package/src/cli/service-command.ts +1 -1
- package/src/cli/surface-command.ts +1 -1
- package/src/cli/tui-startup.ts +72 -10
- package/src/cli/types.ts +14 -3
- package/src/cli-flags.ts +1 -0
- package/src/config/atomic-write.ts +70 -0
- package/src/config/goodvibes-home-audit.ts +2 -0
- package/src/config/read-versioned.ts +115 -0
- package/src/core/context-auto-compact.ts +77 -0
- package/src/core/conversation-rendering.ts +49 -15
- package/src/core/conversation.ts +101 -16
- package/src/core/format-user-error.ts +192 -0
- package/src/core/stream-event-wiring.ts +144 -0
- package/src/core/stream-stall-watchdog.ts +103 -0
- package/src/core/system-message-router.ts +5 -1
- package/src/core/turn-event-wiring.ts +124 -0
- package/src/daemon/cli.ts +5 -0
- package/src/export/cost-utils.ts +71 -0
- package/src/export/gist-uploader.ts +136 -0
- package/src/input/command-registry.ts +32 -1
- package/src/input/commands/control-room-runtime.ts +10 -10
- package/src/input/commands/experience-runtime.ts +5 -4
- package/src/input/commands/knowledge.ts +1 -1
- package/src/input/commands/local-auth-runtime.ts +27 -5
- package/src/input/commands/local-setup.ts +4 -6
- package/src/input/commands/memory-product-runtime.ts +8 -6
- package/src/input/commands/operator-panel-runtime.ts +1 -1
- package/src/input/commands/operator-runtime.ts +3 -10
- package/src/input/commands/{integration-runtime.ts → plugin-runtime.ts} +1 -1
- package/src/input/commands/provider.ts +57 -3
- package/src/input/commands/recall-review.ts +26 -2
- package/src/input/commands/services-runtime.ts +2 -2
- package/src/input/commands/session-workflow.ts +8 -16
- package/src/input/commands/session.ts +70 -20
- package/src/input/commands/share-runtime.ts +99 -12
- package/src/input/commands/tts-runtime.ts +30 -4
- package/src/input/commands.ts +2 -4
- package/src/input/delete-key-policy.ts +46 -0
- package/src/input/feed-context-factory.ts +2 -0
- package/src/input/handler-feed.ts +3 -0
- package/src/input/handler-interactions.ts +2 -15
- package/src/input/handler-modal-routes.ts +128 -12
- package/src/input/handler-modal-token-routes.ts +22 -5
- package/src/input/handler-onboarding-cloudflare.ts +1 -1
- package/src/input/handler-onboarding.ts +73 -69
- package/src/input/handler-types.ts +163 -0
- package/src/input/handler.ts +6 -2
- package/src/input/input-history.ts +76 -6
- package/src/input/model-picker-filter.ts +265 -0
- package/src/input/model-picker-items.ts +208 -0
- package/src/input/model-picker.ts +92 -325
- package/src/input/onboarding/handler-onboarding-routes.ts +7 -2
- package/src/input/onboarding/onboarding-verification-helpers.ts +76 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +14 -4
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +16 -2
- package/src/input/onboarding/onboarding-wizard-cloudflare.ts +8 -8
- package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +1 -1
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +2 -29
- package/src/input/onboarding/onboarding-wizard-rules.ts +28 -28
- package/src/input/onboarding/onboarding-wizard-state.ts +20 -20
- package/src/input/onboarding/onboarding-wizard-steps.ts +24 -25
- package/src/input/onboarding/onboarding-wizard-types.ts +145 -3
- package/src/input/onboarding/onboarding-wizard-validation.ts +77 -0
- package/src/input/onboarding/onboarding-wizard.ts +3 -3
- package/src/input/settings-modal-behavior.ts +5 -0
- package/src/input/settings-modal-data.ts +378 -0
- package/src/input/settings-modal-mutations.ts +157 -0
- package/src/input/settings-modal-reset.ts +154 -0
- package/src/input/settings-modal.ts +236 -232
- package/src/main.ts +93 -85
- package/src/panels/agent-inspector-panel.ts +120 -18
- package/src/panels/agent-inspector-shared.ts +29 -0
- package/src/panels/builtin/agent.ts +4 -1
- package/src/panels/builtin/development.ts +5 -1
- package/src/panels/builtin/knowledge.ts +14 -13
- package/src/panels/builtin/operations.ts +22 -1
- package/src/panels/builtin/shared.ts +7 -0
- package/src/panels/cockpit-panel.ts +123 -3
- package/src/panels/cockpit-read-model.ts +232 -0
- package/src/panels/confirm-state.ts +27 -12
- package/src/panels/cost-tracker-panel.ts +23 -67
- package/src/panels/eval-panel.ts +10 -9
- package/src/panels/index.ts +1 -1
- package/src/panels/knowledge-graph-panel.ts +84 -0
- package/src/panels/local-auth-panel.ts +124 -4
- package/src/panels/memory-panel.ts +370 -40
- package/src/panels/project-planning-panel.ts +42 -4
- package/src/panels/search-focus.ts +11 -5
- package/src/panels/session-maintenance.ts +66 -15
- package/src/panels/subscription-panel.ts +33 -25
- package/src/panels/types.ts +28 -1
- package/src/panels/wrfc-panel.ts +224 -41
- package/src/renderer/agent-detail-modal.ts +118 -13
- package/src/renderer/code-block.ts +10 -2
- package/src/renderer/compositor.ts +18 -4
- package/src/renderer/context-inspector.ts +1 -5
- package/src/renderer/context-status-hint.ts +54 -0
- package/src/renderer/diff.ts +94 -21
- package/src/renderer/markdown.ts +29 -13
- package/src/renderer/settings-modal-helpers.ts +1 -1
- package/src/renderer/settings-modal.ts +90 -10
- package/src/renderer/shell-surface.ts +10 -0
- package/src/renderer/syntax-highlighter.ts +10 -3
- package/src/renderer/term-caps.ts +318 -0
- package/src/renderer/theme.ts +158 -0
- package/src/renderer/tool-call.ts +12 -2
- package/src/renderer/ui-factory.ts +50 -6
- package/src/runtime/bootstrap-command-context.ts +1 -0
- package/src/runtime/bootstrap-command-parts.ts +18 -0
- package/src/runtime/bootstrap-core.ts +145 -13
- package/src/runtime/bootstrap-shell.ts +11 -0
- package/src/runtime/bootstrap.ts +9 -0
- package/src/runtime/onboarding/apply.ts +4 -6
- package/src/runtime/onboarding/index.ts +1 -0
- package/src/runtime/onboarding/markers.ts +42 -49
- package/src/runtime/onboarding/progress.ts +148 -0
- package/src/runtime/onboarding/state.ts +133 -55
- package/src/runtime/onboarding/types.ts +20 -0
- package/src/runtime/services.ts +27 -1
- package/src/runtime/wrfc-persistence.ts +237 -0
- package/src/shell/blocking-input.ts +20 -5
- package/src/tools/wrfc-agent-guard.ts +64 -3
- package/src/utils/format-elapsed.ts +30 -0
- package/src/utils/terminal-width.ts +45 -0
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +4 -6
- package/src/panels/knowledge-panel.ts +0 -345
- package/src/planning/project-planning-coordinator.ts +0 -543
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { writeFile } from 'node:fs/promises';
|
|
2
|
-
import { resolve } from 'path';
|
|
3
2
|
import type { CommandRegistry } from '../command-registry.ts';
|
|
4
3
|
import {
|
|
5
4
|
type ExportMessage,
|
|
@@ -11,13 +10,21 @@ import {
|
|
|
11
10
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
12
11
|
import { requireShellPaths } from './runtime-services.ts';
|
|
13
12
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
13
|
+
import { calcSessionCost } from '../../export/cost-utils.ts';
|
|
14
|
+
import {
|
|
15
|
+
GistUploadTarget,
|
|
16
|
+
NO_TOKEN_GUIDANCE,
|
|
17
|
+
resolveGithubToken,
|
|
18
|
+
} from '../../export/gist-uploader.ts';
|
|
19
|
+
import { copyToClipboard } from '../../utils/clipboard.ts';
|
|
20
|
+
import { openBrowser } from '../../cli/management.ts';
|
|
14
21
|
|
|
15
22
|
export function registerShareRuntimeCommands(registry: CommandRegistry): void {
|
|
16
23
|
registry.register({
|
|
17
24
|
name: 'share',
|
|
18
25
|
aliases: [],
|
|
19
26
|
description: 'Export the current session to a shareable format (html, json, md)',
|
|
20
|
-
usage: '<html|json|md> [path] [--redact]',
|
|
27
|
+
usage: '<html|json|md> [path] [--redact] [--upload] [--copy] [--open]',
|
|
21
28
|
argsHint: '<html|json|md> [path]',
|
|
22
29
|
async handler(args, ctx) {
|
|
23
30
|
const shellPaths = requireShellPaths(ctx);
|
|
@@ -27,20 +34,28 @@ export function registerShareRuntimeCommands(registry: CommandRegistry): void {
|
|
|
27
34
|
const format = args[0]?.toLowerCase() as Format | undefined;
|
|
28
35
|
if (!format || !FORMATS.includes(format)) {
|
|
29
36
|
ctx.print(
|
|
30
|
-
'Usage: /share <html|json|md> [path] [--redact]\n'
|
|
37
|
+
'Usage: /share <html|json|md> [path] [--redact] [--upload] [--copy] [--open]\n'
|
|
31
38
|
+ ' html — self-contained HTML with syntax highlighting\n'
|
|
32
39
|
+ ' json — structured JSON (machine-readable)\n'
|
|
33
40
|
+ ' md — Markdown\n\n'
|
|
34
41
|
+ 'Options:\n'
|
|
35
|
-
+ ' --redact Redact API keys and personal paths from output\n
|
|
42
|
+
+ ' --redact Redact API keys and personal paths from output\n'
|
|
43
|
+
+ ' --upload Upload export as a secret GitHub Gist and print the share link\n'
|
|
44
|
+
+ ' --copy Copy the export file path to the clipboard\n'
|
|
45
|
+
+ ' --open Open HTML export in the default browser\n\n'
|
|
36
46
|
+ 'Default path: ~/goodvibes-exports/session-<timestamp>.<ext>',
|
|
37
47
|
);
|
|
38
48
|
return;
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
const remainingArgs = args.slice(1);
|
|
42
|
-
const redact
|
|
43
|
-
const
|
|
52
|
+
const redact = remainingArgs.includes('--redact');
|
|
53
|
+
const upload = remainingArgs.includes('--upload');
|
|
54
|
+
const doCopy = remainingArgs.includes('--copy');
|
|
55
|
+
const doOpen = remainingArgs.includes('--open');
|
|
56
|
+
const pathArgs = remainingArgs.filter(
|
|
57
|
+
(a) => a !== '--redact' && a !== '--upload' && a !== '--copy' && a !== '--open',
|
|
58
|
+
);
|
|
44
59
|
const outputPath = pathArgs.length > 0
|
|
45
60
|
? shellPaths.resolveWorkspacePath(pathArgs[0])
|
|
46
61
|
: defaultExportPath(format, shellPaths.homeDirectory);
|
|
@@ -69,7 +84,7 @@ export function registerShareRuntimeCommands(registry: CommandRegistry): void {
|
|
|
69
84
|
return;
|
|
70
85
|
}
|
|
71
86
|
|
|
72
|
-
const messages: ExportMessage[] = convData.messages.map(m => ({
|
|
87
|
+
const messages: ExportMessage[] = convData.messages.map((m) => ({
|
|
73
88
|
role: m.role as ExportMessage['role'],
|
|
74
89
|
content: m.content as string,
|
|
75
90
|
toolCalls: m.toolCalls,
|
|
@@ -80,13 +95,32 @@ export function registerShareRuntimeCommands(registry: CommandRegistry): void {
|
|
|
80
95
|
usage: m.usage,
|
|
81
96
|
cancelled: m.cancelled,
|
|
82
97
|
}));
|
|
98
|
+
|
|
99
|
+
// Accumulate per-message usage totals for session cost calculation.
|
|
100
|
+
let totalInput = 0;
|
|
101
|
+
let totalOutput = 0;
|
|
102
|
+
let totalCacheRead = 0;
|
|
103
|
+
let totalCacheWrite = 0;
|
|
104
|
+
for (const m of convData.messages) {
|
|
105
|
+
if (m.usage) {
|
|
106
|
+
totalInput += m.usage.inputTokens ?? 0;
|
|
107
|
+
totalOutput += m.usage.outputTokens ?? 0;
|
|
108
|
+
totalCacheRead += m.usage.cacheReadTokens ?? 0;
|
|
109
|
+
totalCacheWrite += m.usage.cacheWriteTokens ?? 0;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const sessionModel = ctx.session.runtime.model;
|
|
113
|
+
const sessionCostUsd = calcSessionCost(
|
|
114
|
+
totalInput, totalOutput, totalCacheRead, totalCacheWrite, sessionModel,
|
|
115
|
+
);
|
|
116
|
+
|
|
83
117
|
const metadata = {
|
|
84
|
-
model:
|
|
118
|
+
model: sessionModel,
|
|
85
119
|
provider: ctx.session.runtime.provider,
|
|
86
120
|
sessionId: ctx.session.runtime.sessionId,
|
|
87
121
|
title: ctx.session.conversationManager.title || undefined,
|
|
88
122
|
};
|
|
89
|
-
const options = { redact };
|
|
123
|
+
const options = { redact, cost: sessionCostUsd };
|
|
90
124
|
|
|
91
125
|
let outputContent: string;
|
|
92
126
|
try {
|
|
@@ -99,11 +133,14 @@ export function registerShareRuntimeCommands(registry: CommandRegistry): void {
|
|
|
99
133
|
}
|
|
100
134
|
|
|
101
135
|
const { mkdirSync } = await import('node:fs');
|
|
102
|
-
const { dirname } = await import('node:path');
|
|
136
|
+
const { dirname, basename } = await import('node:path');
|
|
103
137
|
try {
|
|
104
138
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
105
139
|
} catch (mkdirErr) {
|
|
106
|
-
logger.warn(
|
|
140
|
+
logger.warn(
|
|
141
|
+
`[share] mkdir failed for ${dirname(outputPath)}:`,
|
|
142
|
+
mkdirErr instanceof Error ? { message: mkdirErr.message } : undefined,
|
|
143
|
+
);
|
|
107
144
|
}
|
|
108
145
|
|
|
109
146
|
try {
|
|
@@ -113,7 +150,57 @@ export function registerShareRuntimeCommands(registry: CommandRegistry): void {
|
|
|
113
150
|
return;
|
|
114
151
|
}
|
|
115
152
|
|
|
116
|
-
|
|
153
|
+
// Optional Gist upload: resolve auth token then push content as a secret gist.
|
|
154
|
+
let shareLink: string | undefined;
|
|
155
|
+
if (upload) {
|
|
156
|
+
let authHeaders: Record<string, string> | null = null;
|
|
157
|
+
try {
|
|
158
|
+
const svcRegistry = ctx.platform.serviceRegistry;
|
|
159
|
+
if (svcRegistry) {
|
|
160
|
+
authHeaders = await svcRegistry.resolveAuth('github').catch(() => null);
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
// serviceRegistry absent or resolveAuth threw — fall through to env var
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const token = resolveGithubToken(authHeaders ?? undefined);
|
|
167
|
+
if (!token) {
|
|
168
|
+
ctx.print(NO_TOKEN_GUIDANCE);
|
|
169
|
+
} else {
|
|
170
|
+
const gistFilename = basename(outputPath);
|
|
171
|
+
const description = metadata.title
|
|
172
|
+
? `GoodVibes session: ${metadata.title}`
|
|
173
|
+
: 'GoodVibes session export';
|
|
174
|
+
const uploader = new GistUploadTarget(token, description);
|
|
175
|
+
const result = await uploader.upload(outputContent, gistFilename);
|
|
176
|
+
if (result.ok) {
|
|
177
|
+
shareLink = result.url;
|
|
178
|
+
} else {
|
|
179
|
+
ctx.print(`Upload failed: ${result.error}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Copy export path to clipboard if requested.
|
|
185
|
+
if (doCopy) {
|
|
186
|
+
copyToClipboard(outputPath);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Open the exported HTML in the default browser if requested.
|
|
190
|
+
if (doOpen && format === 'html') {
|
|
191
|
+
openBrowser(`file://${outputPath}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Emit the summary line, with all post-export hints inline.
|
|
195
|
+
const hints: string[] = [];
|
|
196
|
+
if (redact) hints.push('(sensitive data redacted)');
|
|
197
|
+
if (shareLink) hints.push(`Share link: ${shareLink}`);
|
|
198
|
+
if (doCopy) hints.push('(path copied to clipboard)');
|
|
199
|
+
if (doOpen && format === 'html') hints.push('(opened in browser)');
|
|
200
|
+
if (doOpen && format !== 'html') hints.push('(--open ignored: only applies to html)');
|
|
201
|
+
|
|
202
|
+
const hint = hints.length > 0 ? ' ' + hints.join(' ') : '';
|
|
203
|
+
ctx.print(`Exported ${format.toUpperCase()} session to ${outputPath}${hint}`);
|
|
117
204
|
},
|
|
118
205
|
});
|
|
119
206
|
}
|
|
@@ -3,19 +3,46 @@ import type { CommandRegistry } from '../command-registry.ts';
|
|
|
3
3
|
export function registerTtsRuntimeCommands(registry: CommandRegistry): void {
|
|
4
4
|
registry.register({
|
|
5
5
|
name: 'tts',
|
|
6
|
-
description: 'Submit a
|
|
7
|
-
usage: '<prompt>|stop',
|
|
6
|
+
description: 'Submit a prompt for live TTS playback, or control always-speak mode',
|
|
7
|
+
usage: '<prompt>|stop|on|off',
|
|
8
8
|
handler(args, ctx) {
|
|
9
9
|
const first = (args[0] ?? '').toLowerCase();
|
|
10
|
+
|
|
11
|
+
// /tts stop — cancel active playback
|
|
10
12
|
if (first === 'stop' || first === 'cancel') {
|
|
11
13
|
ctx.stopSpokenOutput?.();
|
|
12
14
|
ctx.print('Live TTS playback stopped.');
|
|
13
15
|
return;
|
|
14
16
|
}
|
|
15
17
|
|
|
18
|
+
// /tts on — enable always-speak mode (every turn spoken automatically)
|
|
19
|
+
if (first === 'on') {
|
|
20
|
+
if (!ctx.platform.voiceService) {
|
|
21
|
+
ctx.print('Live TTS is not available in this runtime.');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
ctx.platform.configManager.setDynamic('ui.voiceEnabled', true);
|
|
25
|
+
ctx.platform.configManager.save();
|
|
26
|
+
ctx.print('Always-speak mode enabled. Every submitted turn will be played through live TTS.');
|
|
27
|
+
ctx.renderRequest();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// /tts off — disable always-speak mode
|
|
32
|
+
if (first === 'off') {
|
|
33
|
+
ctx.platform.configManager.setDynamic('ui.voiceEnabled', false);
|
|
34
|
+
ctx.platform.configManager.save();
|
|
35
|
+
ctx.print('Always-speak mode disabled. Use /tts <prompt> to speak individual turns.');
|
|
36
|
+
ctx.renderRequest();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// /tts <prompt> — mark this prompt for spoken output
|
|
16
41
|
const prompt = args.join(' ').trim();
|
|
17
42
|
if (!prompt) {
|
|
18
|
-
ctx.
|
|
43
|
+
const enabled = ctx.platform.configManager.get('ui.voiceEnabled');
|
|
44
|
+
ctx.print(`Usage: /tts <prompt>, /tts stop, /tts on, /tts off
|
|
45
|
+
Always-speak mode is currently ${enabled ? 'on' : 'off'}.`);
|
|
19
46
|
return;
|
|
20
47
|
}
|
|
21
48
|
if (!ctx.submitSpokenInput) {
|
|
@@ -25,5 +52,4 @@ export function registerTtsRuntimeCommands(registry: CommandRegistry): void {
|
|
|
25
52
|
ctx.submitSpokenInput(prompt);
|
|
26
53
|
},
|
|
27
54
|
});
|
|
28
|
-
|
|
29
55
|
}
|
package/src/input/commands.ts
CHANGED
|
@@ -7,13 +7,12 @@ import { recallCommand } from './commands/memory.ts';
|
|
|
7
7
|
import { knowledgeCommand } from './commands/knowledge.ts';
|
|
8
8
|
import { registerShellCoreCommands } from './commands/shell-core.ts';
|
|
9
9
|
import { registerConfigCommand } from './commands/config.ts';
|
|
10
|
-
import { registerSessionWorkflowCommands } from './commands/session-workflow.ts';
|
|
11
10
|
import { registerDiscoveryRuntimeCommands } from './commands/discovery-runtime.ts';
|
|
12
11
|
import { registerPlanningRuntimeCommands } from './commands/planning-runtime.ts';
|
|
13
12
|
import { registerScheduleRuntimeCommands } from './commands/schedule-runtime.ts';
|
|
14
13
|
import { registerBranchRuntimeCommands } from './commands/branch-runtime.ts';
|
|
15
14
|
import { registerOperatorRuntimeCommands } from './commands/operator-runtime.ts';
|
|
16
|
-
import {
|
|
15
|
+
import { registerPluginRuntimeCommands } from './commands/plugin-runtime.ts';
|
|
17
16
|
import { registerDiffRuntimeCommands } from './commands/diff-runtime.ts';
|
|
18
17
|
import { registerGitRuntimeCommands } from './commands/git-runtime.ts';
|
|
19
18
|
import { registerNotifyRuntimeCommands } from './commands/notify-runtime.ts';
|
|
@@ -65,7 +64,7 @@ export function registerBuiltinCommands(registry: CommandRegistry): void {
|
|
|
65
64
|
registerShellCoreCommands(registry);
|
|
66
65
|
registerConfigCommand(registry);
|
|
67
66
|
registerOperatorRuntimeCommands(registry);
|
|
68
|
-
|
|
67
|
+
registerPluginRuntimeCommands(registry);
|
|
69
68
|
registerDiffRuntimeCommands(registry);
|
|
70
69
|
registerGitRuntimeCommands(registry);
|
|
71
70
|
registerNotifyRuntimeCommands(registry);
|
|
@@ -107,7 +106,6 @@ export function registerBuiltinCommands(registry: CommandRegistry): void {
|
|
|
107
106
|
registerCloudflareRuntimeCommands(registry);
|
|
108
107
|
registerWorkPlanRuntimeCommands(registry);
|
|
109
108
|
registerLocalRuntimeCommands(registry);
|
|
110
|
-
registerSessionWorkflowCommands(registry);
|
|
111
109
|
registerDiscoveryRuntimeCommands(registry);
|
|
112
110
|
registerPlanningRuntimeCommands(registry);
|
|
113
111
|
registerScheduleRuntimeCommands(registry);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Delete-key policy — single source of truth for text-editing key semantics
|
|
3
|
+
//
|
|
4
|
+
// In text-editing contexts:
|
|
5
|
+
// 'backspace' → delete one character backward (the character before cursor /
|
|
6
|
+
// the last character in an end-anchored buffer)
|
|
7
|
+
// 'delete' → forward-delete (the character after cursor) when a moveable
|
|
8
|
+
// cursor exists; a no-op in cursorless/end-anchored contexts,
|
|
9
|
+
// EXCEPT where it opens a confirmation-gated clear action
|
|
10
|
+
// (see planning panel: Delete opens the ConfirmState gate that
|
|
11
|
+
// lets the user clear their entire draft answer).
|
|
12
|
+
//
|
|
13
|
+
// Consequences for each surface:
|
|
14
|
+
// Panel search filters (end-anchored, no cursor)
|
|
15
|
+
// 'backspace' → remove last char ✓
|
|
16
|
+
// 'delete' → no-op ✓ (no cursor; nothing is "forward")
|
|
17
|
+
//
|
|
18
|
+
// Selection modal filters (end-anchored, no cursor)
|
|
19
|
+
// 'backspace' → remove last char ✓
|
|
20
|
+
// 'delete' → no-op ✓
|
|
21
|
+
//
|
|
22
|
+
// Planning panel draft answer (end-anchored, no cursor)
|
|
23
|
+
// 'backspace' → remove last char ✓
|
|
24
|
+
// 'delete' → open ConfirmState gate (y/Enter confirms clear; n/Esc cancels)
|
|
25
|
+
// The draft is NOT wiped until the user confirms.
|
|
26
|
+
//
|
|
27
|
+
// NEVER assign bulk-destructive actions (clear / wipe) to the Delete key
|
|
28
|
+
// without an explicit confirmation gate.
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Returns true when `key` should perform a backward-delete (remove the
|
|
33
|
+
* character before the cursor / at end of an end-anchored buffer).
|
|
34
|
+
*/
|
|
35
|
+
export function isTextBackspace(key: string): boolean {
|
|
36
|
+
return key === 'backspace';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns true when `key` should perform a forward-delete (remove the
|
|
41
|
+
* character after the cursor). Only meaningful when a moveable cursor
|
|
42
|
+
* exists; callers should treat this as a no-op when there is no cursor.
|
|
43
|
+
*/
|
|
44
|
+
export function isTextForwardDelete(key: string): boolean {
|
|
45
|
+
return key === 'delete';
|
|
46
|
+
}
|
|
@@ -161,6 +161,8 @@ export interface FeedContextClosures {
|
|
|
161
161
|
openProviderModelPickerWithTarget: (target: ModelPickerTarget, source?: 'settings' | 'onboarding') => boolean;
|
|
162
162
|
onModelPickerCommit: () => boolean;
|
|
163
163
|
onOnboardingAction: (action: import('./onboarding/onboarding-wizard.ts').OnboardingWizardAction) => void;
|
|
164
|
+
/** Called after any wizard step navigation so the handler can persist progress. */
|
|
165
|
+
onStepChange?: () => void;
|
|
164
166
|
}
|
|
165
167
|
|
|
166
168
|
/**
|
|
@@ -162,6 +162,8 @@ export interface InputFeedContext {
|
|
|
162
162
|
readonly openProviderModelPickerWithTarget: (target: ModelPickerTarget, source?: 'settings' | 'onboarding') => boolean;
|
|
163
163
|
readonly onModelPickerCommit: () => boolean;
|
|
164
164
|
readonly onOnboardingAction: (action: OnboardingWizardAction) => void;
|
|
165
|
+
/** Called after any wizard step navigation so the handler can persist progress. */
|
|
166
|
+
readonly onStepChange?: () => void;
|
|
165
167
|
readonly exitApp: () => void;
|
|
166
168
|
}
|
|
167
169
|
|
|
@@ -228,6 +230,7 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
|
|
|
228
230
|
openProviderModelPickerWithTarget: context.openProviderModelPickerWithTarget,
|
|
229
231
|
onModelPickerCommit: context.onModelPickerCommit,
|
|
230
232
|
onOnboardingAction: context.onOnboardingAction,
|
|
233
|
+
onStepChange: context.onStepChange,
|
|
231
234
|
}, token);
|
|
232
235
|
context.selectionCallback = modalRoute.selectionCallback;
|
|
233
236
|
context.helpOverlayActive = modalRoute.helpOverlayActive;
|
|
@@ -1,29 +1,16 @@
|
|
|
1
1
|
import { buildProviderAccountSnapshot } from '@/runtime/index.ts';
|
|
2
2
|
import type { OnboardingWizardMode } from './onboarding/onboarding-wizard.ts';
|
|
3
|
-
import { collectOnboardingSnapshot
|
|
3
|
+
import { collectOnboardingSnapshot } from '../runtime/onboarding/index.ts';
|
|
4
4
|
import { cleanupMarkerRegistry, expandPrompt, findMarkerAtPos, handleBlockCopy, handleBlockRerun, handleBlockSave, handleBlockToggle, handleBookmark, handleClipboardPaste, handleCopy, handleCtrlC, handleDiffApply, registerPaste } from './handler-content-actions.ts';
|
|
5
5
|
import { clearModalStack, handleEscape, modalOpened } from './handler-modal-stack.ts';
|
|
6
6
|
import { openOnboardingWizardState, type OpenOnboardingWizardOptions } from './handler-ui-state.ts';
|
|
7
|
-
import type { InputHandler } from './handler.ts';
|
|
7
|
+
import type { InputHandlerLike as InputHandler } from './handler-types.ts';
|
|
8
8
|
|
|
9
9
|
export function openOnboardingWizardForHandler(
|
|
10
10
|
handler: InputHandler,
|
|
11
11
|
modeOrOptions: OnboardingWizardMode | OpenOnboardingWizardOptions = 'new',
|
|
12
12
|
): void {
|
|
13
13
|
const options = typeof modeOrOptions === 'string' ? { mode: modeOrOptions } : modeOrOptions;
|
|
14
|
-
const userMarker = readOnboardingCheckMarker(handler.uiServices.environment.shellPaths, 'user');
|
|
15
|
-
if (!userMarker.payload) {
|
|
16
|
-
try {
|
|
17
|
-
writeOnboardingCheckMarker(handler.uiServices.environment.shellPaths, {
|
|
18
|
-
scope: 'user',
|
|
19
|
-
source: 'wizard',
|
|
20
|
-
mode: options.mode ?? 'new',
|
|
21
|
-
});
|
|
22
|
-
} catch (error) {
|
|
23
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
24
|
-
handler.commandContext?.print?.(`Onboarding check marker could not be written: ${message}`);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
14
|
if (!handler.modalStack.includes('onboarding')) handler.modalOpened('onboarding');
|
|
28
15
|
handler.clearOnboardingModelPickerCancelState();
|
|
29
16
|
openOnboardingWizardState(handler.onboardingWizard, options);
|
|
@@ -2,6 +2,7 @@ import type { InputToken } from '@pellux/goodvibes-sdk/platform/core';
|
|
|
2
2
|
import type { SelectionResult, SelectionAction } from './selection-modal.ts';
|
|
3
3
|
import type { CommandContext } from './command-registry.ts';
|
|
4
4
|
import { openTtsProviderPicker, openTtsVoicePicker } from './tts-settings-actions.ts';
|
|
5
|
+
import { isTextBackspace } from './delete-key-policy.ts';
|
|
5
6
|
|
|
6
7
|
type SelectionRouteState = {
|
|
7
8
|
selectionModal: {
|
|
@@ -136,10 +137,13 @@ export function handleSelectionModalToken(state: SelectionRouteState, token: Inp
|
|
|
136
137
|
getAdjustmentStep(selected, token.shift),
|
|
137
138
|
);
|
|
138
139
|
}
|
|
139
|
-
} else if (token.logicalName
|
|
140
|
+
} else if (isTextBackspace(token.logicalName ?? '')) {
|
|
140
141
|
if (state.selectionModal.allowSearch && state.selectionModal.searchFocused && state.selectionModal.query.length > 0) {
|
|
141
142
|
state.selectionModal.setQuery(state.selectionModal.query.slice(0, -1));
|
|
142
143
|
}
|
|
144
|
+
// 'delete' is intentionally absent here: modal search filters are
|
|
145
|
+
// end-anchored with no cursor, so forward-delete is a no-op per the
|
|
146
|
+
// delete-key policy (src/input/delete-key-policy.ts).
|
|
143
147
|
} else if (state.selectionModal.allowSearch && !state.selectionModal.searchFocused && token.logicalName === '/') {
|
|
144
148
|
state.selectionModal.focusSearch();
|
|
145
149
|
} else if (!state.selectionModal.searchFocused && token.logicalName && token.logicalName.length === 1) {
|
|
@@ -212,9 +216,14 @@ type SettingsRouteState = {
|
|
|
212
216
|
editingMode: boolean;
|
|
213
217
|
currentCategory: string;
|
|
214
218
|
focusPane?: 'categories' | 'settings';
|
|
219
|
+
/** True when the user is actively typing into the search input bar. */
|
|
220
|
+
searchFocused: boolean;
|
|
221
|
+
/** Current cross-category search query. */
|
|
222
|
+
searchQuery: string;
|
|
215
223
|
commitEdit: () => void;
|
|
216
224
|
toggleSelectedFlag: () => void;
|
|
217
225
|
activateSelected: () => void;
|
|
226
|
+
handleSubscriptionLogoutKey?: (key: string) => 'confirmed' | 'cancelled' | 'absorbed' | 'inactive';
|
|
218
227
|
adjustSelected: (direction: 'left' | 'right', step?: number) => void;
|
|
219
228
|
moveFocusedUp?: () => void;
|
|
220
229
|
moveFocusedDown?: () => void;
|
|
@@ -227,10 +236,29 @@ type SettingsRouteState = {
|
|
|
227
236
|
prevCategory?: () => void;
|
|
228
237
|
editBackspace: () => void;
|
|
229
238
|
editChar: (char: string) => void;
|
|
239
|
+
/** Enter search mode (focus the search input bar). */
|
|
240
|
+
focusSearch: () => void;
|
|
241
|
+
/** Exit search mode without clearing the query. */
|
|
242
|
+
blurSearch: () => void;
|
|
243
|
+
/** Set search query and recompute results. */
|
|
244
|
+
setSearchQuery: (query: string) => void;
|
|
245
|
+
/** Clear search query, results, and exit search mode. */
|
|
246
|
+
clearSearch: () => void;
|
|
247
|
+
/** Cancel inline edit without saving (mirrors SettingsModal.cancelEdit). */
|
|
248
|
+
cancelEdit: () => void;
|
|
230
249
|
pendingModelPickerTarget: import('./model-picker.ts').ModelPickerTarget | null;
|
|
231
250
|
pendingProviderModelPickerTarget?: import('./model-picker.ts').ModelPickerTarget | null;
|
|
232
251
|
pendingSettingsPickerAction?: 'tts-provider' | 'tts-voice' | null;
|
|
233
252
|
resetSelected?: () => { key: string; value: unknown } | null;
|
|
253
|
+
initiateResetCategory?: () => void;
|
|
254
|
+
initiateResetAll?: () => void;
|
|
255
|
+
handleResetConfirmKey?: (
|
|
256
|
+
key: string,
|
|
257
|
+
) =>
|
|
258
|
+
| { result: 'confirmed'; entries: ReadonlyArray<{ key: string; value: unknown }> }
|
|
259
|
+
| 'cancelled'
|
|
260
|
+
| 'absorbed'
|
|
261
|
+
| 'inactive';
|
|
234
262
|
};
|
|
235
263
|
commandContext?: CommandContext;
|
|
236
264
|
/** Called when the settings modal requests the model picker for a non-main target. */
|
|
@@ -279,42 +307,132 @@ function consumeSettingsPickerRequest(state: SettingsRouteState): void {
|
|
|
279
307
|
export function handleSettingsModalToken(state: SettingsRouteState, token: InputToken): boolean {
|
|
280
308
|
if (!state.settingsModal.active) return false;
|
|
281
309
|
|
|
310
|
+
// Subscription logout confirm gate: routes all keys through the unified
|
|
311
|
+
// confirm contract before normal dispatch when a confirm is pending.
|
|
312
|
+
if (state.settingsModal.handleSubscriptionLogoutKey) {
|
|
313
|
+
const key = token.type === 'key'
|
|
314
|
+
? (token.logicalName ?? '')
|
|
315
|
+
: token.type === 'text'
|
|
316
|
+
? token.value
|
|
317
|
+
: '';
|
|
318
|
+
const logoutResult = state.settingsModal.handleSubscriptionLogoutKey(key);
|
|
319
|
+
if (logoutResult !== 'inactive') {
|
|
320
|
+
state.requestRender();
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Reset confirm gate: routes all keys through the confirm contract before
|
|
326
|
+
// normal dispatch when a category or all-settings reset is pending.
|
|
327
|
+
if (state.settingsModal.handleResetConfirmKey) {
|
|
328
|
+
const key = token.type === 'key'
|
|
329
|
+
? (token.logicalName ?? '')
|
|
330
|
+
: token.type === 'text'
|
|
331
|
+
? token.value
|
|
332
|
+
: '';
|
|
333
|
+
const resetResult = state.settingsModal.handleResetConfirmKey(key);
|
|
334
|
+
if (resetResult !== 'inactive') {
|
|
335
|
+
if (typeof resetResult === 'object' && resetResult.result === 'confirmed') {
|
|
336
|
+
// Sync runtime for every reset entry so provider.model / reasoningEffort
|
|
337
|
+
// stay consistent with the live session without requiring a restart.
|
|
338
|
+
for (const entry of resetResult.entries) {
|
|
339
|
+
syncRuntimeAfterSettingReset(state.commandContext, entry.key, entry.value);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
state.requestRender();
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
282
347
|
if (token.type === 'key') {
|
|
283
348
|
const focusPane = state.settingsModal.focusPane ?? 'settings';
|
|
284
349
|
if (token.logicalName === 'escape') {
|
|
350
|
+
// Cancel inline edit first — mirrors the global contract in handler-modal-stack.ts.
|
|
351
|
+
// Must check editingMode before searchFocused: the reachable path
|
|
352
|
+
// search→Enter(string/number)→Esc must cancel the edit, NOT just clear search.
|
|
353
|
+
if (state.settingsModal.editingMode) {
|
|
354
|
+
state.settingsModal.cancelEdit();
|
|
355
|
+
state.requestRender();
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
// Two-stage escape: if in search mode, first Esc exits search (clearSearch),
|
|
359
|
+
// second Esc closes the modal.
|
|
360
|
+
if (state.settingsModal.searchFocused) {
|
|
361
|
+
state.settingsModal.clearSearch();
|
|
362
|
+
state.requestRender();
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
285
365
|
state.handleEscape();
|
|
286
366
|
return true;
|
|
287
367
|
}
|
|
288
|
-
if (token.logicalName === 'enter' || (token.logicalName === 'space' && !state.settingsModal.editingMode)) {
|
|
368
|
+
if (token.logicalName === 'enter' || (token.logicalName === 'space' && !state.settingsModal.editingMode && !state.settingsModal.searchFocused)) {
|
|
289
369
|
if (state.settingsModal.editingMode) state.settingsModal.commitEdit();
|
|
290
|
-
else if (
|
|
370
|
+
else if (state.settingsModal.searchFocused) {
|
|
371
|
+
// Enter in search mode: activate the selected search result
|
|
372
|
+
state.settingsModal.activateSelected();
|
|
373
|
+
consumeSettingsPickerRequest(state);
|
|
374
|
+
} else if (focusPane === 'categories') state.settingsModal.focusSettings?.();
|
|
291
375
|
else if (state.settingsModal.currentCategory === 'flags') state.settingsModal.toggleSelectedFlag();
|
|
292
376
|
else {
|
|
293
377
|
state.settingsModal.activateSelected();
|
|
294
378
|
consumeSettingsPickerRequest(state);
|
|
295
379
|
}
|
|
296
|
-
} else if ((token.logicalName === 'left' || token.logicalName === 'right') && !state.settingsModal.editingMode) {
|
|
380
|
+
} else if ((token.logicalName === 'left' || token.logicalName === 'right') && !state.settingsModal.editingMode && !state.settingsModal.searchFocused) {
|
|
297
381
|
if (token.logicalName === 'left') state.settingsModal.focusCategories?.();
|
|
298
382
|
else state.settingsModal.focusSettings?.();
|
|
299
383
|
} else if (token.logicalName === 'up') {
|
|
300
|
-
if (state.settingsModal.
|
|
384
|
+
if (state.settingsModal.searchFocused) {
|
|
385
|
+
state.settingsModal.moveUp?.();
|
|
386
|
+
} else if (state.settingsModal.moveFocusedUp) state.settingsModal.moveFocusedUp();
|
|
301
387
|
else state.settingsModal.moveUp?.();
|
|
302
388
|
} else if (token.logicalName === 'down') {
|
|
303
|
-
if (state.settingsModal.
|
|
389
|
+
if (state.settingsModal.searchFocused) {
|
|
390
|
+
state.settingsModal.moveDown?.();
|
|
391
|
+
} else if (state.settingsModal.moveFocusedDown) state.settingsModal.moveFocusedDown();
|
|
304
392
|
else state.settingsModal.moveDown?.();
|
|
305
393
|
}
|
|
306
|
-
else if (token.logicalName === 'r' && !state.settingsModal.editingMode) {
|
|
394
|
+
else if (token.logicalName === 'r' && token.shift && token.ctrl && !state.settingsModal.editingMode && !state.settingsModal.searchFocused) {
|
|
395
|
+
state.settingsModal.initiateResetAll?.();
|
|
396
|
+
}
|
|
397
|
+
else if (token.logicalName === 'r' && token.shift && !state.settingsModal.editingMode && !state.settingsModal.searchFocused) {
|
|
398
|
+
state.settingsModal.initiateResetCategory?.();
|
|
399
|
+
}
|
|
400
|
+
else if (token.logicalName === 'r' && !state.settingsModal.editingMode && !state.settingsModal.searchFocused) {
|
|
307
401
|
const reset = state.settingsModal.resetSelected?.();
|
|
308
402
|
if (reset) syncRuntimeAfterSettingReset(state.commandContext, reset.key, reset.value);
|
|
309
403
|
}
|
|
310
|
-
else if (token.logicalName === 'tab') {
|
|
404
|
+
else if (token.logicalName === 'tab' && !state.settingsModal.searchFocused) {
|
|
311
405
|
if (state.settingsModal.toggleFocusPane) state.settingsModal.toggleFocusPane();
|
|
312
406
|
else if (focusPane === 'categories') state.settingsModal.focusSettings?.();
|
|
313
407
|
else state.settingsModal.focusCategories?.();
|
|
314
408
|
}
|
|
315
|
-
else if (token.logicalName
|
|
409
|
+
else if (isTextBackspace(token.logicalName ?? '')) {
|
|
410
|
+
if (state.settingsModal.editingMode) {
|
|
411
|
+
state.settingsModal.editBackspace();
|
|
412
|
+
} else if (state.settingsModal.searchFocused) {
|
|
413
|
+
// Backspace in search mode: trim query
|
|
414
|
+
const trimmed = state.settingsModal.searchQuery.slice(0, -1);
|
|
415
|
+
state.settingsModal.setSearchQuery(trimmed);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
// token.logicalName === 'delete' is intentionally absent: search filters
|
|
419
|
+
// are end-anchored with no cursor, so forward-delete is a no-op per
|
|
420
|
+
// delete-key policy (src/input/delete-key-policy.ts).
|
|
421
|
+
else if (!state.settingsModal.editingMode && !state.settingsModal.searchFocused && token.logicalName === '/') {
|
|
422
|
+
state.settingsModal.focusSearch();
|
|
423
|
+
}
|
|
316
424
|
} else if (token.type === 'text') {
|
|
317
|
-
if (
|
|
425
|
+
if (state.settingsModal.editingMode) {
|
|
426
|
+
// editingMode takes priority over search — Enter on a string/number search
|
|
427
|
+
// result enters inline edit; subsequent chars must go to editChar, not the query.
|
|
428
|
+
state.settingsModal.editChar(token.value);
|
|
429
|
+
} else if (state.settingsModal.searchFocused) {
|
|
430
|
+
// Any printable char in search mode appends to the query
|
|
431
|
+
state.settingsModal.setSearchQuery(state.settingsModal.searchQuery + token.value);
|
|
432
|
+
} else if (token.value === '/' && !state.settingsModal.editingMode) {
|
|
433
|
+
// / enters search mode
|
|
434
|
+
state.settingsModal.focusSearch();
|
|
435
|
+
} else if (token.value === ' ' && !state.settingsModal.editingMode) {
|
|
318
436
|
const focusPane = state.settingsModal.focusPane ?? 'settings';
|
|
319
437
|
if (focusPane === 'categories') state.settingsModal.focusSettings?.();
|
|
320
438
|
else if (state.settingsModal.currentCategory === 'flags') state.settingsModal.toggleSelectedFlag();
|
|
@@ -322,8 +440,6 @@ export function handleSettingsModalToken(state: SettingsRouteState, token: Input
|
|
|
322
440
|
state.settingsModal.activateSelected();
|
|
323
441
|
consumeSettingsPickerRequest(state);
|
|
324
442
|
}
|
|
325
|
-
} else if (state.settingsModal.editingMode) {
|
|
326
|
-
state.settingsModal.editChar(token.value);
|
|
327
443
|
} else if (token.value === 'r') {
|
|
328
444
|
const reset = state.settingsModal.resetSelected?.();
|
|
329
445
|
if (reset) syncRuntimeAfterSettingReset(state.commandContext, reset.key, reset.value);
|
|
@@ -92,6 +92,8 @@ export type ModalTokenRouteState = {
|
|
|
92
92
|
restoreOnboardingModelPickerCancelState?: () => void;
|
|
93
93
|
onModelPickerCommit?: () => boolean;
|
|
94
94
|
onOnboardingAction?: (action: OnboardingWizardAction) => void;
|
|
95
|
+
/** Called after any wizard step navigation so the handler can persist progress. */
|
|
96
|
+
onStepChange?: () => void;
|
|
95
97
|
};
|
|
96
98
|
|
|
97
99
|
export function handleModalTokenRoutes(state: ModalTokenRouteState, token: InputToken): {
|
|
@@ -216,6 +218,7 @@ export function handleModalTokenRoutes(state: ModalTokenRouteState, token: Input
|
|
|
216
218
|
handleEscape: state.handleEscape,
|
|
217
219
|
openModelPickerWithTarget: state.openModelPickerWithTarget,
|
|
218
220
|
onAction: state.onOnboardingAction,
|
|
221
|
+
onStepChange: state.onStepChange,
|
|
219
222
|
}, token)) {
|
|
220
223
|
return withState(state, true);
|
|
221
224
|
}
|
|
@@ -229,11 +232,25 @@ export function handleModalTokenRoutes(state: ModalTokenRouteState, token: Input
|
|
|
229
232
|
return withState(state, true);
|
|
230
233
|
}
|
|
231
234
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
235
|
+
// Agent detail modal: route c + confirm keys before escape-close.
|
|
236
|
+
// handleKey() consumes confirm-flow keys (y, Enter, n, Esc) and the 'c'
|
|
237
|
+
// initiator; unhandled keys (including Esc when no confirm is pending)
|
|
238
|
+
// fall through to escape-close below.
|
|
239
|
+
if (state.agentDetailModal.active) {
|
|
240
|
+
const keyStr: string =
|
|
241
|
+
token.type === 'key' ? (token.logicalName ?? '') :
|
|
242
|
+
token.type === 'text' ? token.value : '';
|
|
243
|
+
if (keyStr && state.agentDetailModal.handleKey(keyStr)) {
|
|
244
|
+
state.requestRender();
|
|
245
|
+
return withState(state, true);
|
|
246
|
+
}
|
|
247
|
+
// 'c' was not consumed (non-cancellable), or any other key.
|
|
248
|
+
// Esc closes the modal; all other keys are absorbed by the active modal.
|
|
249
|
+
if (token.type === 'key' && token.logicalName === 'escape') {
|
|
250
|
+
state.handleEscape();
|
|
251
|
+
return withState(state, true);
|
|
252
|
+
}
|
|
253
|
+
state.requestRender();
|
|
237
254
|
return withState(state, true);
|
|
238
255
|
}
|
|
239
256
|
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
type CloudflareVerifyResult,
|
|
13
13
|
} from '../runtime/cloudflare-control-plane.ts';
|
|
14
14
|
import type { OnboardingVerificationItem } from '../runtime/onboarding/index.ts';
|
|
15
|
-
import type { InputHandler } from './handler.ts';
|
|
15
|
+
import type { InputHandlerLike as InputHandler } from './handler-types.ts';
|
|
16
16
|
import type { OnboardingWizardAction, OnboardingWizardApplyFeedback } from './onboarding/onboarding-wizard.ts';
|
|
17
17
|
import {
|
|
18
18
|
buildCloudflareApiTokenRef,
|