@pellux/goodvibes-tui 0.20.3 → 0.21.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 +27 -0
- package/README.md +23 -2
- package/docs/foundation-artifacts/operator-contract.json +78 -1
- package/package.json +3 -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 +662 -0
- package/src/cli/config-overrides.ts +68 -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 +14 -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 +12 -3
- package/src/cli-flags.ts +1 -0
- package/src/config/atomic-write.ts +70 -0
- package/src/config/read-versioned.ts +115 -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/export/cost-utils.ts +71 -0
- package/src/export/gist-uploader.ts +136 -0
- package/src/input/command-registry.ts +31 -1
- package/src/input/commands/control-room-runtime.ts +5 -5
- 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/recall-review.ts +26 -2
- package/src/input/commands/services-runtime.ts +2 -2
- package/src/input/commands/session-workflow.ts +3 -3
- package/src/input/commands/share-runtime.ts +99 -12
- package/src/input/commands/tts-runtime.ts +30 -4
- package/src/input/commands.ts +2 -2
- 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 +91 -12
- package/src/input/handler-modal-token-routes.ts +3 -0
- package/src/input/handler-onboarding-cloudflare.ts +1 -1
- package/src/input/handler-onboarding.ts +55 -69
- package/src/input/handler-types.ts +163 -0
- package/src/input/handler.ts +5 -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 +4 -4
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +2 -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 +18 -25
- package/src/input/onboarding/onboarding-wizard-types.ts +145 -3
- package/src/input/onboarding/onboarding-wizard.ts +3 -3
- package/src/input/settings-modal-data.ts +304 -0
- package/src/input/settings-modal-mutations.ts +154 -0
- package/src/input/settings-modal.ts +182 -220
- package/src/main.ts +57 -57
- package/src/panels/builtin/agent.ts +4 -1
- package/src/panels/builtin/development.ts +4 -1
- 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/knowledge-panel.ts +3 -5
- package/src/panels/local-auth-panel.ts +124 -4
- package/src/panels/project-planning-panel.ts +42 -4
- package/src/panels/search-focus.ts +11 -5
- 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 +11 -10
- 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/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 +77 -8
- 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 +14 -0
- package/src/runtime/bootstrap-core.ts +121 -13
- package/src/runtime/bootstrap.ts +2 -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 +21 -0
- 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/planning/project-planning-coordinator.ts +0 -543
|
@@ -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,6 +236,16 @@ 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;
|
|
@@ -279,42 +298,104 @@ function consumeSettingsPickerRequest(state: SettingsRouteState): void {
|
|
|
279
298
|
export function handleSettingsModalToken(state: SettingsRouteState, token: InputToken): boolean {
|
|
280
299
|
if (!state.settingsModal.active) return false;
|
|
281
300
|
|
|
301
|
+
// Subscription logout confirm gate: routes all keys through the unified
|
|
302
|
+
// confirm contract before normal dispatch when a confirm is pending.
|
|
303
|
+
if (state.settingsModal.handleSubscriptionLogoutKey) {
|
|
304
|
+
const key = token.type === 'key'
|
|
305
|
+
? (token.logicalName ?? '')
|
|
306
|
+
: token.type === 'text'
|
|
307
|
+
? token.value
|
|
308
|
+
: '';
|
|
309
|
+
const logoutResult = state.settingsModal.handleSubscriptionLogoutKey(key);
|
|
310
|
+
if (logoutResult !== 'inactive') {
|
|
311
|
+
state.requestRender();
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
282
316
|
if (token.type === 'key') {
|
|
283
317
|
const focusPane = state.settingsModal.focusPane ?? 'settings';
|
|
284
318
|
if (token.logicalName === 'escape') {
|
|
319
|
+
// Cancel inline edit first — mirrors the global contract in handler-modal-stack.ts.
|
|
320
|
+
// Must check editingMode before searchFocused: the reachable path
|
|
321
|
+
// search→Enter(string/number)→Esc must cancel the edit, NOT just clear search.
|
|
322
|
+
if (state.settingsModal.editingMode) {
|
|
323
|
+
state.settingsModal.cancelEdit();
|
|
324
|
+
state.requestRender();
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
// Two-stage escape: if in search mode, first Esc exits search (clearSearch),
|
|
328
|
+
// second Esc closes the modal.
|
|
329
|
+
if (state.settingsModal.searchFocused) {
|
|
330
|
+
state.settingsModal.clearSearch();
|
|
331
|
+
state.requestRender();
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
285
334
|
state.handleEscape();
|
|
286
335
|
return true;
|
|
287
336
|
}
|
|
288
|
-
if (token.logicalName === 'enter' || (token.logicalName === 'space' && !state.settingsModal.editingMode)) {
|
|
337
|
+
if (token.logicalName === 'enter' || (token.logicalName === 'space' && !state.settingsModal.editingMode && !state.settingsModal.searchFocused)) {
|
|
289
338
|
if (state.settingsModal.editingMode) state.settingsModal.commitEdit();
|
|
290
|
-
else if (
|
|
339
|
+
else if (state.settingsModal.searchFocused) {
|
|
340
|
+
// Enter in search mode: activate the selected search result
|
|
341
|
+
state.settingsModal.activateSelected();
|
|
342
|
+
consumeSettingsPickerRequest(state);
|
|
343
|
+
} else if (focusPane === 'categories') state.settingsModal.focusSettings?.();
|
|
291
344
|
else if (state.settingsModal.currentCategory === 'flags') state.settingsModal.toggleSelectedFlag();
|
|
292
345
|
else {
|
|
293
346
|
state.settingsModal.activateSelected();
|
|
294
347
|
consumeSettingsPickerRequest(state);
|
|
295
348
|
}
|
|
296
|
-
} else if ((token.logicalName === 'left' || token.logicalName === 'right') && !state.settingsModal.editingMode) {
|
|
349
|
+
} else if ((token.logicalName === 'left' || token.logicalName === 'right') && !state.settingsModal.editingMode && !state.settingsModal.searchFocused) {
|
|
297
350
|
if (token.logicalName === 'left') state.settingsModal.focusCategories?.();
|
|
298
351
|
else state.settingsModal.focusSettings?.();
|
|
299
352
|
} else if (token.logicalName === 'up') {
|
|
300
|
-
if (state.settingsModal.
|
|
353
|
+
if (state.settingsModal.searchFocused) {
|
|
354
|
+
state.settingsModal.moveUp?.();
|
|
355
|
+
} else if (state.settingsModal.moveFocusedUp) state.settingsModal.moveFocusedUp();
|
|
301
356
|
else state.settingsModal.moveUp?.();
|
|
302
357
|
} else if (token.logicalName === 'down') {
|
|
303
|
-
if (state.settingsModal.
|
|
358
|
+
if (state.settingsModal.searchFocused) {
|
|
359
|
+
state.settingsModal.moveDown?.();
|
|
360
|
+
} else if (state.settingsModal.moveFocusedDown) state.settingsModal.moveFocusedDown();
|
|
304
361
|
else state.settingsModal.moveDown?.();
|
|
305
362
|
}
|
|
306
|
-
else if (token.logicalName === 'r' && !state.settingsModal.editingMode) {
|
|
363
|
+
else if (token.logicalName === 'r' && !state.settingsModal.editingMode && !state.settingsModal.searchFocused) {
|
|
307
364
|
const reset = state.settingsModal.resetSelected?.();
|
|
308
365
|
if (reset) syncRuntimeAfterSettingReset(state.commandContext, reset.key, reset.value);
|
|
309
366
|
}
|
|
310
|
-
else if (token.logicalName === 'tab') {
|
|
367
|
+
else if (token.logicalName === 'tab' && !state.settingsModal.searchFocused) {
|
|
311
368
|
if (state.settingsModal.toggleFocusPane) state.settingsModal.toggleFocusPane();
|
|
312
369
|
else if (focusPane === 'categories') state.settingsModal.focusSettings?.();
|
|
313
370
|
else state.settingsModal.focusCategories?.();
|
|
314
371
|
}
|
|
315
|
-
else if (token.logicalName
|
|
372
|
+
else if (isTextBackspace(token.logicalName ?? '')) {
|
|
373
|
+
if (state.settingsModal.editingMode) {
|
|
374
|
+
state.settingsModal.editBackspace();
|
|
375
|
+
} else if (state.settingsModal.searchFocused) {
|
|
376
|
+
// Backspace in search mode: trim query
|
|
377
|
+
const trimmed = state.settingsModal.searchQuery.slice(0, -1);
|
|
378
|
+
state.settingsModal.setSearchQuery(trimmed);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// token.logicalName === 'delete' is intentionally absent: search filters
|
|
382
|
+
// are end-anchored with no cursor, so forward-delete is a no-op per
|
|
383
|
+
// delete-key policy (src/input/delete-key-policy.ts).
|
|
384
|
+
else if (!state.settingsModal.editingMode && !state.settingsModal.searchFocused && token.logicalName === '/') {
|
|
385
|
+
state.settingsModal.focusSearch();
|
|
386
|
+
}
|
|
316
387
|
} else if (token.type === 'text') {
|
|
317
|
-
if (
|
|
388
|
+
if (state.settingsModal.editingMode) {
|
|
389
|
+
// editingMode takes priority over search — Enter on a string/number search
|
|
390
|
+
// result enters inline edit; subsequent chars must go to editChar, not the query.
|
|
391
|
+
state.settingsModal.editChar(token.value);
|
|
392
|
+
} else if (state.settingsModal.searchFocused) {
|
|
393
|
+
// Any printable char in search mode appends to the query
|
|
394
|
+
state.settingsModal.setSearchQuery(state.settingsModal.searchQuery + token.value);
|
|
395
|
+
} else if (token.value === '/' && !state.settingsModal.editingMode) {
|
|
396
|
+
// / enters search mode
|
|
397
|
+
state.settingsModal.focusSearch();
|
|
398
|
+
} else if (token.value === ' ' && !state.settingsModal.editingMode) {
|
|
318
399
|
const focusPane = state.settingsModal.focusPane ?? 'settings';
|
|
319
400
|
if (focusPane === 'categories') state.settingsModal.focusSettings?.();
|
|
320
401
|
else if (state.settingsModal.currentCategory === 'flags') state.settingsModal.toggleSelectedFlag();
|
|
@@ -322,8 +403,6 @@ export function handleSettingsModalToken(state: SettingsRouteState, token: Input
|
|
|
322
403
|
state.settingsModal.activateSelected();
|
|
323
404
|
consumeSettingsPickerRequest(state);
|
|
324
405
|
}
|
|
325
|
-
} else if (state.settingsModal.editingMode) {
|
|
326
|
-
state.settingsModal.editChar(token.value);
|
|
327
406
|
} else if (token.value === 'r') {
|
|
328
407
|
const reset = state.settingsModal.resetSelected?.();
|
|
329
408
|
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
|
}
|
|
@@ -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,
|
|
@@ -5,11 +5,17 @@ import { getProviderIdFromModel } from '../config/provider-model.ts';
|
|
|
5
5
|
import { buildProviderAccountSnapshot } from '@/runtime/index.ts';
|
|
6
6
|
import { OnboardingWizardController, type OnboardingWizardAction, type OnboardingWizardApplyFeedback } from './onboarding/onboarding-wizard.ts';
|
|
7
7
|
import { handleCloudflareOnboardingActionForHandler, maybeProvisionCloudflareOnFinalApplyForHandler } from './handler-onboarding-cloudflare.ts';
|
|
8
|
-
import { applyOnboardingRequest, collectOnboardingSnapshot, verifyOnboardingRequest } from '../runtime/onboarding/index.ts';
|
|
8
|
+
import { applyOnboardingRequest, collectOnboardingSnapshot, deleteWizardProgress, verifyOnboardingRequest, writeOnboardingCheckMarker, writeWizardProgress } from '../runtime/onboarding/index.ts';
|
|
9
9
|
import type { OnboardingApplyRequest, OnboardingVerificationItem } from '../runtime/onboarding/index.ts';
|
|
10
|
+
import {
|
|
11
|
+
dedupeOnboardingVerificationItems,
|
|
12
|
+
extractAuthorizationCode,
|
|
13
|
+
formatOnboardingApplyCompletionMessage,
|
|
14
|
+
isLoopbackHostValue,
|
|
15
|
+
} from './onboarding/onboarding-verification-helpers.ts';
|
|
10
16
|
import type { ModelPickerTarget } from './model-picker.ts';
|
|
11
17
|
import { captureOnboardingWizardSnapshot, restoreOnboardingWizardSnapshot } from './handler-ui-state.ts';
|
|
12
|
-
import type { InputHandler } from './handler.ts';
|
|
18
|
+
import type { InputHandlerLike as InputHandler, OnboardingRuntimePosture } from './handler-types.ts';
|
|
13
19
|
import {
|
|
14
20
|
formatRuntimeActiveSuccessMessage,
|
|
15
21
|
getRuntimeEndpointStatus,
|
|
@@ -20,73 +26,6 @@ import {
|
|
|
20
26
|
type OnboardingRuntimeEndpoint,
|
|
21
27
|
} from './onboarding/onboarding-runtime-status.ts';
|
|
22
28
|
|
|
23
|
-
export interface OnboardingRuntimePosture {
|
|
24
|
-
readonly serviceEnabled: boolean;
|
|
25
|
-
readonly serviceAutostart: boolean;
|
|
26
|
-
readonly restartOnFailure: boolean;
|
|
27
|
-
readonly expectedDaemon: boolean;
|
|
28
|
-
readonly expectedHttpListener: boolean;
|
|
29
|
-
readonly serverBacked: boolean;
|
|
30
|
-
readonly remoteExposure: boolean;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function extractAuthorizationCode(input: string): string | null {
|
|
34
|
-
const trimmed = input.trim();
|
|
35
|
-
if (!trimmed) return null;
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
const url = new URL(trimmed);
|
|
39
|
-
return url.searchParams.get('code');
|
|
40
|
-
} catch {
|
|
41
|
-
return trimmed;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function isLoopbackHostValue(value: string | null | undefined): boolean {
|
|
46
|
-
const normalized = (value ?? '').trim().toLowerCase();
|
|
47
|
-
if (normalized.length === 0) return false;
|
|
48
|
-
return normalized === 'localhost'
|
|
49
|
-
|| normalized === '::1'
|
|
50
|
-
|| normalized === '[::1]'
|
|
51
|
-
|| normalized === '0:0:0:0:0:0:0:1'
|
|
52
|
-
|| /^127(?:\.\d{1,3}){3}$/.test(normalized);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function onboardingVerificationStatusRank(item: OnboardingVerificationItem): number {
|
|
56
|
-
if (item.status === 'fail') return 3;
|
|
57
|
-
if (item.status === 'warn') return 2;
|
|
58
|
-
return 1;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function dedupeOnboardingVerificationItems(
|
|
62
|
-
items: readonly OnboardingVerificationItem[],
|
|
63
|
-
): OnboardingVerificationItem[] {
|
|
64
|
-
const order: string[] = [];
|
|
65
|
-
const byId = new Map<string, OnboardingVerificationItem>();
|
|
66
|
-
for (const item of items) {
|
|
67
|
-
const existing = byId.get(item.id);
|
|
68
|
-
if (!existing) {
|
|
69
|
-
order.push(item.id);
|
|
70
|
-
byId.set(item.id, item);
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
if (onboardingVerificationStatusRank(item) > onboardingVerificationStatusRank(existing)) {
|
|
74
|
-
byId.set(item.id, item);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return order.map((id) => byId.get(id)).filter((item): item is OnboardingVerificationItem => Boolean(item));
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function formatOnboardingApplyCompletionMessage(items: readonly OnboardingVerificationItem[]): string {
|
|
81
|
-
const warnings = items.filter((item) => item.status === 'warn');
|
|
82
|
-
if (warnings.length === 0) return `Onboarding applied and verified ${items.length} item(s).`;
|
|
83
|
-
const passed = items.filter((item) => item.status === 'pass').length;
|
|
84
|
-
return [
|
|
85
|
-
`Onboarding settings applied. ${passed} verification item(s) passed; ${warnings.length} warning(s) need attention.`,
|
|
86
|
-
...warnings.map((warning) => ` warning ${warning.id}: ${warning.message}`),
|
|
87
|
-
].join('\n');
|
|
88
|
-
}
|
|
89
|
-
|
|
90
29
|
function getRuntimeEndpointBinding(
|
|
91
30
|
handler: InputHandler,
|
|
92
31
|
request: OnboardingApplyRequest,
|
|
@@ -292,6 +231,18 @@ export async function handleOnboardingActionForHandler(handler: InputHandler, ac
|
|
|
292
231
|
}
|
|
293
232
|
|
|
294
233
|
if (appliedErrors.length === 0) {
|
|
234
|
+
try {
|
|
235
|
+
writeOnboardingCheckMarker(handler.uiServices.environment.shellPaths, {
|
|
236
|
+
scope: 'user',
|
|
237
|
+
source: 'wizard',
|
|
238
|
+
mode: request.mode,
|
|
239
|
+
});
|
|
240
|
+
deleteWizardProgress(handler.uiServices.environment.shellPaths);
|
|
241
|
+
} catch (markerError) {
|
|
242
|
+
handler.commandContext?.print?.(
|
|
243
|
+
`Onboarding check marker could not be written: ${markerError instanceof Error ? markerError.message : String(markerError)}`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
295
246
|
const activationVerification = await handler.restartOnboardingExternalServicesIfNeeded(request);
|
|
296
247
|
runtimeWarnings = dedupeOnboardingVerificationItems([...activationVerification, ...handler.verifyOnboardingRuntimePosture(request)]
|
|
297
248
|
.map((item): OnboardingVerificationItem => item.status === 'fail'
|
|
@@ -540,6 +491,41 @@ export async function handleOpenAiSubscriptionFinishForHandler(handler: InputHan
|
|
|
540
491
|
}
|
|
541
492
|
}
|
|
542
493
|
|
|
494
|
+
/**
|
|
495
|
+
* Persist the current wizard field state to onboarding-progress.json.
|
|
496
|
+
*
|
|
497
|
+
* Called after each step navigation so the user can resume a partially
|
|
498
|
+
* completed wizard after a restart. Masked fields (kind === 'masked') are
|
|
499
|
+
* excluded from the persisted textState to avoid writing secrets to disk.
|
|
500
|
+
*
|
|
501
|
+
* This is a best-effort write: if it fails the wizard continues normally.
|
|
502
|
+
*/
|
|
503
|
+
export function saveWizardProgressForHandler(handler: InputHandler): void {
|
|
504
|
+
const wizard = handler.onboardingWizard;
|
|
505
|
+
if (!wizard.active) return;
|
|
506
|
+
try {
|
|
507
|
+
// Exclude masked fields to avoid persisting secrets.
|
|
508
|
+
const maskedFieldIds = new Set<string>();
|
|
509
|
+
for (const step of wizard.steps) {
|
|
510
|
+
for (const field of step.fields) {
|
|
511
|
+
if (field.kind === 'masked') maskedFieldIds.add(field.id);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const safeTextState = [...wizard.textState.entries()]
|
|
515
|
+
.filter(([id]) => !maskedFieldIds.has(id)) as Array<[string, string]>;
|
|
516
|
+
|
|
517
|
+
writeWizardProgress(handler.uiServices.environment.shellPaths, {
|
|
518
|
+
mode: wizard.mode,
|
|
519
|
+
stepIndex: wizard.stepIndex,
|
|
520
|
+
toggleState: [...wizard.toggleState.entries()],
|
|
521
|
+
radioState: [...wizard.radioState.entries()],
|
|
522
|
+
textState: safeTextState,
|
|
523
|
+
});
|
|
524
|
+
} catch {
|
|
525
|
+
// Best-effort: never block wizard interaction on a progress write failure.
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
543
529
|
export function syncRuntimeFromOnboardingRequestForHandler(handler: InputHandler, request: ReturnType<OnboardingWizardController['buildApplyRequest']>): void {
|
|
544
530
|
const runtime = handler.commandContext?.session.runtime;
|
|
545
531
|
if (!runtime) return;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* handler-types.ts — Leaf interface for InputHandler.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from handler.ts to break circular import chains between handler.ts
|
|
5
|
+
* and handler-interactions.ts / handler-onboarding.ts / handler-onboarding-cloudflare.ts.
|
|
6
|
+
*
|
|
7
|
+
* The interface is the union of all `handler.*` accesses in those three files.
|
|
8
|
+
* InputHandler declares `implements InputHandlerLike`; no cycle is created
|
|
9
|
+
* because this file imports only from leaf modules (no import from handler.ts).
|
|
10
|
+
*/
|
|
11
|
+
import { type createOAuthLocalListener } from '@pellux/goodvibes-sdk/platform/config';
|
|
12
|
+
import type { OnboardingWizardController, OnboardingWizardAction } from './onboarding/onboarding-wizard.ts';
|
|
13
|
+
import type { OnboardingWizardSnapshot, OpenOnboardingWizardOptions } from './handler-ui-state.ts';
|
|
14
|
+
import type {
|
|
15
|
+
OnboardingApplyRequest,
|
|
16
|
+
OnboardingVerificationItem,
|
|
17
|
+
} from '../runtime/onboarding/index.ts';
|
|
18
|
+
import type { UiRuntimeServices } from '../runtime/ui-services.ts';
|
|
19
|
+
import type { CommandContext } from './command-registry.ts';
|
|
20
|
+
import type { ConversationManager } from '../core/conversation';
|
|
21
|
+
import type { ModelPickerModal, ModelPickerTarget } from './model-picker.ts';
|
|
22
|
+
import type { SelectionManager } from './selection.ts';
|
|
23
|
+
import type { InfiniteBuffer } from '../core/history.ts';
|
|
24
|
+
import type { AutocompleteEngine } from './autocomplete.ts';
|
|
25
|
+
import type { BookmarkModal } from './bookmark-modal.ts';
|
|
26
|
+
import type { AgentDetailModal } from '../renderer/agent-detail-modal.ts';
|
|
27
|
+
import type { LiveTailModal } from '../renderer/live-tail-modal.ts';
|
|
28
|
+
import type { SettingsModal } from './settings-modal.ts';
|
|
29
|
+
import type { McpWorkspace } from './mcp-workspace.ts';
|
|
30
|
+
import type { SessionPickerModal } from './session-picker-modal.ts';
|
|
31
|
+
import type { ProfilePickerModal } from './profile-picker-modal.ts';
|
|
32
|
+
import type { ContextInspectorModal } from '../renderer/context-inspector.ts';
|
|
33
|
+
import type { ProcessModal } from '../renderer/process-modal.ts';
|
|
34
|
+
import type { FilePickerModal } from './file-picker.ts';
|
|
35
|
+
import type { BlockActionsMenu } from '../renderer/block-actions.ts';
|
|
36
|
+
import type { SelectionModal } from './selection-modal.ts';
|
|
37
|
+
import type { SelectionResult } from './selection-modal.ts';
|
|
38
|
+
export interface OnboardingRuntimePosture {
|
|
39
|
+
readonly serviceEnabled: boolean;
|
|
40
|
+
readonly serviceAutostart: boolean;
|
|
41
|
+
readonly restartOnFailure: boolean;
|
|
42
|
+
readonly expectedDaemon: boolean;
|
|
43
|
+
readonly expectedHttpListener: boolean;
|
|
44
|
+
readonly serverBacked: boolean;
|
|
45
|
+
readonly remoteExposure: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type SelectionModalCallback = (result: SelectionResult | null) => void;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Public surface of InputHandler consumed by handler-interactions.ts,
|
|
52
|
+
* handler-onboarding.ts, and handler-onboarding-cloudflare.ts.
|
|
53
|
+
*/
|
|
54
|
+
export interface InputHandlerLike {
|
|
55
|
+
// ── Core render / lifecycle ──────────────────────────────────────────────
|
|
56
|
+
requestRender: () => void;
|
|
57
|
+
exitApp: () => void;
|
|
58
|
+
|
|
59
|
+
// ── Services ─────────────────────────────────────────────────────────────
|
|
60
|
+
readonly uiServices: Pick<UiRuntimeServices,
|
|
61
|
+
| 'agents'
|
|
62
|
+
| 'environment'
|
|
63
|
+
| 'platform'
|
|
64
|
+
| 'providers'
|
|
65
|
+
| 'sessions'
|
|
66
|
+
| 'shell'
|
|
67
|
+
>;
|
|
68
|
+
commandContext: CommandContext | undefined;
|
|
69
|
+
|
|
70
|
+
// ── Prompt / cursor state ────────────────────────────────────────────────
|
|
71
|
+
prompt: string;
|
|
72
|
+
cursorPos: number;
|
|
73
|
+
showExitNotice: boolean;
|
|
74
|
+
|
|
75
|
+
// ── Selection / history ──────────────────────────────────────────────────
|
|
76
|
+
selection: SelectionManager;
|
|
77
|
+
getHistory: () => InfiniteBuffer;
|
|
78
|
+
getScrollTop: () => number;
|
|
79
|
+
conversationManager: ConversationManager | null;
|
|
80
|
+
|
|
81
|
+
// ── Paste / image registries ─────────────────────────────────────────────
|
|
82
|
+
pasteRegistry: Map<string, string>;
|
|
83
|
+
nextPasteId: number;
|
|
84
|
+
imageRegistry: Map<string, { data: string; mediaType: string }>;
|
|
85
|
+
nextImageId: number;
|
|
86
|
+
|
|
87
|
+
// ── Timing ───────────────────────────────────────────────────────────────
|
|
88
|
+
lastCopyTime: number;
|
|
89
|
+
lastBlockCopyTime: number;
|
|
90
|
+
lastCtrlCTime: number;
|
|
91
|
+
|
|
92
|
+
// ── Modal state ───────────────────────────────────────────────────────────
|
|
93
|
+
commandMode: boolean;
|
|
94
|
+
modalStack: string[];
|
|
95
|
+
modalReturnFocus: 'prompt' | 'panel' | 'indicator';
|
|
96
|
+
panelFocused: boolean;
|
|
97
|
+
indicatorFocused: boolean;
|
|
98
|
+
helpOverlayActive: boolean;
|
|
99
|
+
helpScrollOffset: number;
|
|
100
|
+
shortcutsOverlayActive: boolean;
|
|
101
|
+
shortcutsScrollOffset: number;
|
|
102
|
+
selectionCallback: SelectionModalCallback | null;
|
|
103
|
+
autocomplete: AutocompleteEngine | null;
|
|
104
|
+
|
|
105
|
+
// ── Modal objects ─────────────────────────────────────────────────────────
|
|
106
|
+
bookmarkModal: BookmarkModal;
|
|
107
|
+
agentDetailModal: AgentDetailModal;
|
|
108
|
+
liveTailModal: LiveTailModal;
|
|
109
|
+
settingsModal: SettingsModal;
|
|
110
|
+
mcpWorkspace: McpWorkspace;
|
|
111
|
+
sessionPickerModal: SessionPickerModal;
|
|
112
|
+
profilePickerModal: ProfilePickerModal;
|
|
113
|
+
contextInspectorModal: ContextInspectorModal;
|
|
114
|
+
processModal: ProcessModal;
|
|
115
|
+
modelPicker: ModelPickerModal;
|
|
116
|
+
filePicker: FilePickerModal;
|
|
117
|
+
blockActionsMenu: BlockActionsMenu;
|
|
118
|
+
selectionModal: SelectionModal;
|
|
119
|
+
|
|
120
|
+
// ── Onboarding ────────────────────────────────────────────────────────────
|
|
121
|
+
onboardingWizard: OnboardingWizardController;
|
|
122
|
+
onboardingModelPickerCancelSnapshot: OnboardingWizardSnapshot | null;
|
|
123
|
+
onboardingHydrationSerial: number;
|
|
124
|
+
onboardingApplyPending: boolean;
|
|
125
|
+
onboardingOpenAiListenerSerial: number;
|
|
126
|
+
|
|
127
|
+
// ── Methods: modal lifecycle ──────────────────────────────────────────────
|
|
128
|
+
modalOpened(name: string): void;
|
|
129
|
+
saveUndoState(): void;
|
|
130
|
+
|
|
131
|
+
// ── Methods: block actions (dispatched in executeBlockAction) ────────────
|
|
132
|
+
handleBlockCopy(): void;
|
|
133
|
+
handleBookmark(): void;
|
|
134
|
+
handleBlockToggle(): void;
|
|
135
|
+
handleDiffApply(): boolean;
|
|
136
|
+
handleBlockRerun(): void;
|
|
137
|
+
|
|
138
|
+
// ── Methods: onboarding ───────────────────────────────────────────────────
|
|
139
|
+
hydrateOnboardingWizardFromRuntime(hydrationSerial: number): Promise<void>;
|
|
140
|
+
clearOnboardingModelPickerCancelState(): void;
|
|
141
|
+
restoreOnboardingModelPickerCancelState(): void;
|
|
142
|
+
clearOnboardingPendingModelPickerTarget(): void;
|
|
143
|
+
refreshOnboardingHydration(options?: { readonly preserveValues?: boolean; readonly targetStepId?: string }): Promise<void>;
|
|
144
|
+
handleOpenAiSubscriptionStart(): Promise<void>;
|
|
145
|
+
handleOpenAiSubscriptionFinish(): Promise<void>;
|
|
146
|
+
syncRuntimeFromOnboardingRequest(request: ReturnType<OnboardingWizardController['buildApplyRequest']>): void;
|
|
147
|
+
getOnboardingConfigValue(request: OnboardingApplyRequest, key: string): unknown;
|
|
148
|
+
getOnboardingRuntimePosture(request: OnboardingApplyRequest): OnboardingRuntimePosture;
|
|
149
|
+
restartOnboardingExternalServicesIfNeeded(request: OnboardingApplyRequest): Promise<OnboardingVerificationItem[]>;
|
|
150
|
+
verifyOnboardingRuntimePosture(request: OnboardingApplyRequest): OnboardingVerificationItem[];
|
|
151
|
+
|
|
152
|
+
// ── Method: model picker ──────────────────────────────────────────────────
|
|
153
|
+
openModelPickerWithTarget(target: ModelPickerTarget, source?: 'settings' | 'onboarding'): boolean;
|
|
154
|
+
openProviderModelPickerWithTarget(target: ModelPickerTarget, source?: 'settings' | 'onboarding'): boolean;
|
|
155
|
+
|
|
156
|
+
// ── Method: onboarding action ─────────────────────────────────────────────
|
|
157
|
+
completeOpenAiSubscriptionFromListener(
|
|
158
|
+
listener: Awaited<ReturnType<typeof createOAuthLocalListener>>,
|
|
159
|
+
verifier: string,
|
|
160
|
+
serial: number,
|
|
161
|
+
): Promise<void>;
|
|
162
|
+
handleOnboardingAction(action: OnboardingWizardAction): Promise<void>;
|
|
163
|
+
}
|
package/src/input/handler.ts
CHANGED
|
@@ -3,7 +3,8 @@ import { dirname } from 'node:path';
|
|
|
3
3
|
import { InputTokenizer } from '@pellux/goodvibes-sdk/platform/core';
|
|
4
4
|
import { createOAuthLocalListener } from '@pellux/goodvibes-sdk/platform/config';
|
|
5
5
|
import { clearModalStackForHandler, cleanupMarkerRegistryForHandler, executeBlockActionForHandler, expandPromptForHandler, findMarkerAtPosForHandler, getImageAttachmentsForHandler, handleBlockCopyForHandler, handleBlockRerunForHandler, handleBlockSaveForHandler, handleBlockToggleForHandler, handleBookmarkForHandler, handleCopyForHandler, handleCtrlCForHandler, handleDiffApplyForHandler, handleEscapeForHandler, hydrateOnboardingWizardFromRuntimeForHandler, modalOpenedForHandler, openOnboardingWizardForHandler, registerPasteForHandler } from './handler-interactions.ts';
|
|
6
|
-
import { clearOnboardingModelPickerCancelStateForHandler, clearOnboardingPendingModelPickerTargetForHandler, completeOpenAiSubscriptionFromListenerForHandler, getOnboardingConfigValueForHandler, getOnboardingRuntimePostureForHandler, handleModelPickerCommitForHandler, handleOnboardingActionForHandler, handleOpenAiSubscriptionFinishForHandler, handleOpenAiSubscriptionStartForHandler, openModelPickerWithTargetForHandler, openProviderModelPickerWithTargetForHandler, refreshOnboardingHydrationForHandler, restartOnboardingExternalServicesIfNeededForHandler, restoreOnboardingModelPickerCancelStateForHandler, syncRuntimeFromOnboardingRequestForHandler, verifyOnboardingRuntimePostureForHandler,
|
|
6
|
+
import { clearOnboardingModelPickerCancelStateForHandler, clearOnboardingPendingModelPickerTargetForHandler, completeOpenAiSubscriptionFromListenerForHandler, getOnboardingConfigValueForHandler, getOnboardingRuntimePostureForHandler, handleModelPickerCommitForHandler, handleOnboardingActionForHandler, handleOpenAiSubscriptionFinishForHandler, handleOpenAiSubscriptionStartForHandler, openModelPickerWithTargetForHandler, openProviderModelPickerWithTargetForHandler, refreshOnboardingHydrationForHandler, restartOnboardingExternalServicesIfNeededForHandler, restoreOnboardingModelPickerCancelStateForHandler, saveWizardProgressForHandler, syncRuntimeFromOnboardingRequestForHandler, verifyOnboardingRuntimePostureForHandler, } from './handler-onboarding.ts';
|
|
7
|
+
import type { OnboardingRuntimePosture } from './handler-types.ts';
|
|
7
8
|
import { beginOpenAICodexLogin, exchangeOpenAICodexCode } from '@pellux/goodvibes-sdk/platform/config';
|
|
8
9
|
import { openExternalUrl } from '@pellux/goodvibes-sdk/platform/utils';
|
|
9
10
|
import { buildProviderAccountSnapshot } from '@/runtime/index.ts';
|
|
@@ -109,6 +110,7 @@ import {
|
|
|
109
110
|
} from './handler-picker-routes.ts';
|
|
110
111
|
import { handleGlobalShortcutToken } from './handler-shortcuts.ts';
|
|
111
112
|
import { feedInputTokens } from './handler-feed.ts';
|
|
113
|
+
import type { InputHandlerLike } from './handler-types.ts';
|
|
112
114
|
import { buildInitialFeedContext, syncFeedContextMutableFields } from './feed-context-factory.ts';
|
|
113
115
|
import { handlePanelIntegrationAction as runPanelIntegrationAction } from './panel-integration-actions.ts';
|
|
114
116
|
import type { Panel } from '../panels/types.ts';
|
|
@@ -123,7 +125,7 @@ type SelectionModalCallback = (result: SelectionResult | null) => void;
|
|
|
123
125
|
* InputHandler - Owns prompt text, paste registry, and keyboard/mouse handling.
|
|
124
126
|
* Extracted from main.ts and StateManager.
|
|
125
127
|
*/
|
|
126
|
-
export class InputHandler {
|
|
128
|
+
export class InputHandler implements InputHandlerLike {
|
|
127
129
|
public prompt = '';
|
|
128
130
|
public cursorPos = 0;
|
|
129
131
|
public showExitNotice = false;
|
|
@@ -335,6 +337,7 @@ export class InputHandler {
|
|
|
335
337
|
this.openProviderModelPickerWithTarget(target, source),
|
|
336
338
|
onModelPickerCommit: () => this.handleModelPickerCommit(),
|
|
337
339
|
onOnboardingAction: (action: OnboardingWizardAction) => { void this.handleOnboardingAction(action); },
|
|
340
|
+
onStepChange: () => { saveWizardProgressForHandler(this); },
|
|
338
341
|
},
|
|
339
342
|
);
|
|
340
343
|
}
|