@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
|
@@ -5,11 +5,18 @@ 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';
|
|
16
|
+
import { focusFirstOffendingField, getStepValidationErrors } from './onboarding/onboarding-wizard-validation.ts';
|
|
10
17
|
import type { ModelPickerTarget } from './model-picker.ts';
|
|
11
18
|
import { captureOnboardingWizardSnapshot, restoreOnboardingWizardSnapshot } from './handler-ui-state.ts';
|
|
12
|
-
import type { InputHandler } from './handler.ts';
|
|
19
|
+
import type { InputHandlerLike as InputHandler, OnboardingRuntimePosture } from './handler-types.ts';
|
|
13
20
|
import {
|
|
14
21
|
formatRuntimeActiveSuccessMessage,
|
|
15
22
|
getRuntimeEndpointStatus,
|
|
@@ -20,73 +27,6 @@ import {
|
|
|
20
27
|
type OnboardingRuntimeEndpoint,
|
|
21
28
|
} from './onboarding/onboarding-runtime-status.ts';
|
|
22
29
|
|
|
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
30
|
function getRuntimeEndpointBinding(
|
|
91
31
|
handler: InputHandler,
|
|
92
32
|
request: OnboardingApplyRequest,
|
|
@@ -143,6 +83,23 @@ function showOnboardingApplyFeedbackForHandler(handler: InputHandler, feedback:
|
|
|
143
83
|
|
|
144
84
|
function continueOnboardingSection(handler: InputHandler): void {
|
|
145
85
|
handler.onboardingWizard.commitEdit();
|
|
86
|
+
|
|
87
|
+
const step = handler.onboardingWizard.currentStep;
|
|
88
|
+
const { errors, firstOffendingFieldId } = getStepValidationErrors(handler.onboardingWizard, step);
|
|
89
|
+
if (errors.length > 0) {
|
|
90
|
+
handler.onboardingWizard.setApplyFeedback({
|
|
91
|
+
severity: 'error',
|
|
92
|
+
title: 'Required fields missing',
|
|
93
|
+
summary: 'Fill in the required fields below before continuing.',
|
|
94
|
+
messages: errors,
|
|
95
|
+
});
|
|
96
|
+
if (firstOffendingFieldId !== null) {
|
|
97
|
+
focusFirstOffendingField(handler.onboardingWizard, firstOffendingFieldId);
|
|
98
|
+
}
|
|
99
|
+
handler.requestRender();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
146
103
|
handler.onboardingWizard.clearApplyFeedback();
|
|
147
104
|
handler.onboardingWizard.nextStep();
|
|
148
105
|
handler.requestRender();
|
|
@@ -292,6 +249,18 @@ export async function handleOnboardingActionForHandler(handler: InputHandler, ac
|
|
|
292
249
|
}
|
|
293
250
|
|
|
294
251
|
if (appliedErrors.length === 0) {
|
|
252
|
+
try {
|
|
253
|
+
writeOnboardingCheckMarker(handler.uiServices.environment.shellPaths, {
|
|
254
|
+
scope: 'user',
|
|
255
|
+
source: 'wizard',
|
|
256
|
+
mode: request.mode,
|
|
257
|
+
});
|
|
258
|
+
deleteWizardProgress(handler.uiServices.environment.shellPaths);
|
|
259
|
+
} catch (markerError) {
|
|
260
|
+
handler.commandContext?.print?.(
|
|
261
|
+
`Onboarding check marker could not be written: ${markerError instanceof Error ? markerError.message : String(markerError)}`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
295
264
|
const activationVerification = await handler.restartOnboardingExternalServicesIfNeeded(request);
|
|
296
265
|
runtimeWarnings = dedupeOnboardingVerificationItems([...activationVerification, ...handler.verifyOnboardingRuntimePosture(request)]
|
|
297
266
|
.map((item): OnboardingVerificationItem => item.status === 'fail'
|
|
@@ -540,6 +509,41 @@ export async function handleOpenAiSubscriptionFinishForHandler(handler: InputHan
|
|
|
540
509
|
}
|
|
541
510
|
}
|
|
542
511
|
|
|
512
|
+
/**
|
|
513
|
+
* Persist the current wizard field state to onboarding-progress.json.
|
|
514
|
+
*
|
|
515
|
+
* Called after each step navigation so the user can resume a partially
|
|
516
|
+
* completed wizard after a restart. Masked fields (kind === 'masked') are
|
|
517
|
+
* excluded from the persisted textState to avoid writing secrets to disk.
|
|
518
|
+
*
|
|
519
|
+
* This is a best-effort write: if it fails the wizard continues normally.
|
|
520
|
+
*/
|
|
521
|
+
export function saveWizardProgressForHandler(handler: InputHandler): void {
|
|
522
|
+
const wizard = handler.onboardingWizard;
|
|
523
|
+
if (!wizard.active) return;
|
|
524
|
+
try {
|
|
525
|
+
// Exclude masked fields to avoid persisting secrets.
|
|
526
|
+
const maskedFieldIds = new Set<string>();
|
|
527
|
+
for (const step of wizard.steps) {
|
|
528
|
+
for (const field of step.fields) {
|
|
529
|
+
if (field.kind === 'masked') maskedFieldIds.add(field.id);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
const safeTextState = [...wizard.textState.entries()]
|
|
533
|
+
.filter(([id]) => !maskedFieldIds.has(id)) as Array<[string, string]>;
|
|
534
|
+
|
|
535
|
+
writeWizardProgress(handler.uiServices.environment.shellPaths, {
|
|
536
|
+
mode: wizard.mode,
|
|
537
|
+
stepIndex: wizard.stepIndex,
|
|
538
|
+
toggleState: [...wizard.toggleState.entries()],
|
|
539
|
+
radioState: [...wizard.radioState.entries()],
|
|
540
|
+
textState: safeTextState,
|
|
541
|
+
});
|
|
542
|
+
} catch {
|
|
543
|
+
// Best-effort: never block wizard interaction on a progress write failure.
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
543
547
|
export function syncRuntimeFromOnboardingRequestForHandler(handler: InputHandler, request: ReturnType<OnboardingWizardController['buildApplyRequest']>): void {
|
|
544
548
|
const runtime = handler.commandContext?.session.runtime;
|
|
545
549
|
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;
|
|
@@ -246,6 +248,7 @@ export class InputHandler {
|
|
|
246
248
|
sessionLogPathResolver: (agentId) => uiServices.environment.shellPaths.resolveProjectPath('tui', 'sessions', `${agentId}.jsonl`),
|
|
247
249
|
// SDK 0.23.0: supply wrfcController so the modal can show constraint data
|
|
248
250
|
wrfcController: uiServices.agents.wrfcController,
|
|
251
|
+
cancelAgent: (agentId: string) => uiServices.agents.agentManager.cancel(agentId),
|
|
249
252
|
});
|
|
250
253
|
this.bookmarkModal = new BookmarkModal(uiServices.shell.bookmarkManager);
|
|
251
254
|
this.sessionPickerModal = new SessionPickerModal(uiServices.sessions.sessionManager);
|
|
@@ -335,6 +338,7 @@ export class InputHandler {
|
|
|
335
338
|
this.openProviderModelPickerWithTarget(target, source),
|
|
336
339
|
onModelPickerCommit: () => this.handleModelPickerCommit(),
|
|
337
340
|
onOnboardingAction: (action: OnboardingWizardAction) => { void this.handleOnboardingAction(action); },
|
|
341
|
+
onStepChange: () => { saveWizardProgressForHandler(this); },
|
|
338
342
|
},
|
|
339
343
|
);
|
|
340
344
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { existsSync,
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { atomicWriteFileSync } from '@/config/atomic-write.ts';
|
|
3
4
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
4
5
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
5
6
|
|
|
@@ -100,11 +101,39 @@ export class HistorySearch {
|
|
|
100
101
|
}
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
/**
|
|
105
|
+
* A redaction rule applied to command text before it is stored or saved.
|
|
106
|
+
* Any match of `pattern` in the raw text is replaced with `replacement`.
|
|
107
|
+
*/
|
|
108
|
+
export interface HistoryRedactionRule {
|
|
109
|
+
readonly pattern: RegExp;
|
|
110
|
+
readonly replacement: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Built-in redaction rules that apply regardless of caller-supplied rules.
|
|
115
|
+
* Scrubs passwords from local-auth commands before they reach disk.
|
|
116
|
+
*/
|
|
117
|
+
const BUILTIN_REDACTION_RULES: readonly HistoryRedactionRule[] = [
|
|
118
|
+
{
|
|
119
|
+
// /auth local add-user <user> <password> [roles]
|
|
120
|
+
pattern: /(\/auth\s+local\s+add-user\s+\S+)\s+(\S+)/i,
|
|
121
|
+
replacement: '$1 <redacted>',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
// /auth local rotate-password <user> <password>
|
|
125
|
+
pattern: /(\/auth\s+local\s+rotate-password\s+\S+)\s+(\S+)/i,
|
|
126
|
+
replacement: '$1 <redacted>',
|
|
127
|
+
},
|
|
128
|
+
];
|
|
129
|
+
|
|
103
130
|
export interface InputHistoryOptions {
|
|
104
131
|
readonly historyPath?: string;
|
|
105
132
|
readonly userRoot?: string;
|
|
106
133
|
readonly homeDirectory?: string;
|
|
107
134
|
readonly persist?: boolean;
|
|
135
|
+
/** Additional redaction rules applied on top of the built-in set. */
|
|
136
|
+
readonly redactionRules?: readonly HistoryRedactionRule[];
|
|
108
137
|
}
|
|
109
138
|
|
|
110
139
|
type StoredInputHistoryEntry = string | {
|
|
@@ -130,25 +159,44 @@ export class InputHistory {
|
|
|
130
159
|
private maxEntries = 500;
|
|
131
160
|
private historyPath: string;
|
|
132
161
|
private persist: boolean;
|
|
162
|
+
private redactionRules: readonly HistoryRedactionRule[];
|
|
133
163
|
|
|
134
164
|
constructor(options: InputHistoryOptions) {
|
|
135
165
|
this.persist = options.persist ?? true;
|
|
136
166
|
this.historyPath = resolveHistoryPath(options);
|
|
167
|
+
this.redactionRules = [
|
|
168
|
+
...BUILTIN_REDACTION_RULES,
|
|
169
|
+
...(options.redactionRules ?? []),
|
|
170
|
+
];
|
|
137
171
|
if (this.persist) {
|
|
138
172
|
this.load();
|
|
139
173
|
}
|
|
140
174
|
}
|
|
141
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Apply all active redaction rules to a text string.
|
|
178
|
+
* Returns the scrubbed text (password arguments replaced with `<redacted>`).
|
|
179
|
+
*/
|
|
180
|
+
private applyRedaction(text: string): string {
|
|
181
|
+
let result = text;
|
|
182
|
+
for (const rule of this.redactionRules) {
|
|
183
|
+
result = result.replace(rule.pattern, rule.replacement);
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
|
|
142
188
|
/**
|
|
143
189
|
* Add a new entry. Called on submit.
|
|
144
190
|
* - Ignores empty/whitespace-only strings.
|
|
145
191
|
* - Deduplicates consecutive identical entries.
|
|
192
|
+
* - Applies redaction rules to scrub sensitive arguments before persistence.
|
|
146
193
|
* - Resets browsing position.
|
|
147
194
|
*/
|
|
148
195
|
add(text: string, options: { readonly recallText?: string } = {}): void {
|
|
149
|
-
const trimmed = text.trim();
|
|
196
|
+
const trimmed = this.applyRedaction(text.trim());
|
|
150
197
|
if (!trimmed) return;
|
|
151
|
-
const
|
|
198
|
+
const rawRecallText = options.recallText?.trim();
|
|
199
|
+
const recallText = rawRecallText ? this.applyRedaction(rawRecallText) : undefined;
|
|
152
200
|
const entry: StoredInputHistoryEntry = recallText && recallText !== trimmed
|
|
153
201
|
? { text: trimmed, recallText }
|
|
154
202
|
: trimmed;
|
|
@@ -240,8 +288,7 @@ export class InputHistory {
|
|
|
240
288
|
*/
|
|
241
289
|
save(): void {
|
|
242
290
|
try {
|
|
243
|
-
|
|
244
|
-
writeFileSync(this.historyPath, JSON.stringify(this.entries), 'utf-8');
|
|
291
|
+
atomicWriteFileSync(this.historyPath, JSON.stringify(this.entries), { mkdirp: true });
|
|
245
292
|
} catch (err) {
|
|
246
293
|
logger.debug('InputHistory save failed (non-fatal)', { error: summarizeError(err) });
|
|
247
294
|
}
|
|
@@ -249,6 +296,10 @@ export class InputHistory {
|
|
|
249
296
|
|
|
250
297
|
/**
|
|
251
298
|
* Load history from disk.
|
|
299
|
+
*
|
|
300
|
+
* Redaction is applied to every loaded entry so that cleartext passwords
|
|
301
|
+
* persisted before the redaction rules were deployed are scrubbed on first
|
|
302
|
+
* load and will not be re-persisted on the next save().
|
|
252
303
|
*/
|
|
253
304
|
load(): void {
|
|
254
305
|
try {
|
|
@@ -259,6 +310,7 @@ export class InputHistory {
|
|
|
259
310
|
this.entries = (parsed as unknown[])
|
|
260
311
|
.map((entry) => this.normalizeStoredEntry(entry))
|
|
261
312
|
.filter((entry): entry is StoredInputHistoryEntry => entry !== null)
|
|
313
|
+
.map((entry) => this.redactEntry(entry))
|
|
262
314
|
.slice(0, this.maxEntries);
|
|
263
315
|
}
|
|
264
316
|
}
|
|
@@ -281,6 +333,24 @@ export class InputHistory {
|
|
|
281
333
|
&& this.getRecallText(a) === this.getRecallText(b);
|
|
282
334
|
}
|
|
283
335
|
|
|
336
|
+
/**
|
|
337
|
+
* Apply redaction rules to a loaded entry, scrubbing any sensitive text that
|
|
338
|
+
* was persisted before redaction was deployed.
|
|
339
|
+
*/
|
|
340
|
+
private redactEntry(entry: StoredInputHistoryEntry): StoredInputHistoryEntry {
|
|
341
|
+
if (typeof entry === 'string') {
|
|
342
|
+
return this.applyRedaction(entry);
|
|
343
|
+
}
|
|
344
|
+
const redactedText = this.applyRedaction(entry.text);
|
|
345
|
+
const redactedRecallText = entry.recallText !== undefined
|
|
346
|
+
? this.applyRedaction(entry.recallText)
|
|
347
|
+
: undefined;
|
|
348
|
+
if (redactedRecallText !== undefined && redactedRecallText !== redactedText) {
|
|
349
|
+
return { text: redactedText, recallText: redactedRecallText };
|
|
350
|
+
}
|
|
351
|
+
return redactedText;
|
|
352
|
+
}
|
|
353
|
+
|
|
284
354
|
private normalizeStoredEntry(entry: unknown): StoredInputHistoryEntry | null {
|
|
285
355
|
if (typeof entry === 'string') return entry;
|
|
286
356
|
if (!entry || typeof entry !== 'object') return null;
|