@pellux/goodvibes-tui 1.16.1 → 1.18.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 +138 -9
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +5490 -1806
- package/package.json +7 -7
- package/src/cli/completions/generate.ts +5 -4
- package/src/cli/config-overrides.ts +51 -11
- package/src/cli/doctor.ts +4 -5
- package/src/cli/entrypoint.ts +13 -8
- package/src/cli/help.ts +7 -6
- package/src/cli/launch-auto-update.ts +214 -0
- package/src/cli/management-commands.ts +82 -15
- package/src/cli/management.ts +0 -2
- package/src/cli/parser.ts +5 -0
- package/src/cli/status.ts +13 -0
- package/src/cli/surface-command.ts +5 -5
- package/src/cli/tui-startup.ts +60 -97
- package/src/cli/types.ts +2 -0
- package/src/config/index.ts +1 -1
- package/src/config/tui-extension-settings.ts +33 -0
- package/src/core/composer-state.ts +1 -1
- package/src/core/consolidation-receipt.ts +41 -0
- package/src/core/conversation-line-cache.ts +3 -3
- package/src/core/conversation-rendering.ts +18 -9
- package/src/core/conversation.ts +2 -2
- package/src/core/long-task-notifier.ts +7 -7
- package/src/core/memory-provenance.ts +67 -0
- package/src/core/pairing-devices.ts +49 -0
- package/src/core/pairing-handoff.ts +101 -0
- package/src/core/pairing-offers.ts +83 -0
- package/src/core/pairing-origin.ts +78 -0
- package/src/core/pairing-tailscale-gateway.ts +56 -0
- package/src/core/power-chip-source.ts +91 -0
- package/src/core/power-status.ts +95 -0
- package/src/core/stable-host.ts +171 -0
- package/src/core/stream-event-wiring.ts +29 -6
- package/src/core/system-message-noise.ts +1 -1
- package/src/core/system-message-router.ts +6 -6
- package/src/core/turn-budget-outcome.ts +60 -0
- package/src/core/turn-cancellation.ts +40 -1
- package/src/core/turn-event-wiring.ts +14 -8
- package/src/daemon/cli.ts +64 -58
- package/src/daemon/lifecycle.ts +41 -0
- package/src/daemon/service-commands.ts +5 -5
- package/src/export/cost-utils.ts +128 -6
- package/src/input/autocomplete.ts +2 -2
- package/src/input/command-registry.ts +55 -9
- package/src/input/commands/acp-runtime.ts +72 -0
- package/src/input/commands/ci-runtime.ts +5 -1
- package/src/input/commands/codebase-runtime.ts +6 -6
- package/src/input/commands/cost-runtime.ts +1 -1
- package/src/input/commands/devices-runtime.ts +109 -0
- package/src/input/commands/git-runtime.ts +28 -4
- package/src/input/commands/graph-runtime.ts +45 -0
- package/src/input/commands/health-runtime.ts +2 -2
- package/src/input/commands/intelligence-runtime.ts +1 -1
- package/src/input/commands/local-auth-runtime.ts +1 -1
- package/src/input/commands/local-provider-runtime.ts +49 -10
- package/src/input/commands/local-runtime.ts +53 -5
- package/src/input/commands/memory.ts +2 -2
- package/src/input/commands/operator-panel-runtime.ts +5 -5
- package/src/input/commands/operator-rpc.ts +32 -11
- package/src/input/commands/permissions-provenance.ts +1 -1
- package/src/input/commands/permissions-runtime.ts +99 -8
- package/src/input/commands/planning-runtime.ts +3 -3
- package/src/input/commands/platform-sandbox-runtime.ts +1 -1
- package/src/input/commands/power-runtime.ts +56 -0
- package/src/input/commands/provider-accounts-runtime.ts +71 -6
- package/src/input/commands/provider.ts +7 -5
- package/src/input/commands/qrcode-runtime.ts +14 -19
- package/src/input/commands/queue-runtime.ts +70 -0
- package/src/input/commands/recall-files-git.ts +13 -51
- package/src/input/commands/recall-files.ts +8 -11
- package/src/input/commands/recall-review.ts +2 -2
- package/src/input/commands/relay-runtime.ts +1 -1
- package/src/input/commands/remote-runtime.ts +1 -1
- package/src/input/commands/runtime-services.ts +4 -4
- package/src/input/commands/schedule-runtime.ts +14 -7
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-workflow.ts +2 -2
- package/src/input/commands/settings-sync-runtime.ts +2 -2
- package/src/input/commands/share-runtime.ts +1 -1
- package/src/input/commands/shell-core.ts +46 -68
- package/src/input/commands/subscription-runtime.ts +1 -1
- package/src/input/commands/update-runtime.ts +153 -96
- package/src/input/commands/work-plan-runtime.ts +1 -1
- package/src/input/commands/workstream-attempts.ts +6 -1
- package/src/input/commands/workstream-runtime.ts +46 -27
- package/src/input/commands/worktree-runtime.ts +35 -8
- package/src/input/commands.ts +10 -2
- package/src/input/config-modal-types.ts +14 -3
- package/src/input/config-modal.ts +75 -13
- package/src/input/feature-unit-layout.ts +93 -82
- package/src/input/feed-context-factory.ts +2 -2
- package/src/input/handler-command-route.ts +2 -2
- package/src/input/handler-content-actions.ts +1 -1
- package/src/input/handler-feed-routes.ts +7 -7
- package/src/input/handler-feed.ts +4 -4
- package/src/input/handler-interactions.ts +3 -2
- package/src/input/handler-modal-routes.ts +12 -4
- package/src/input/handler-modal-token-routes.ts +1 -1
- package/src/input/handler-onboarding-daemon-adopt.ts +2 -2
- package/src/input/handler-onboarding.ts +3 -2
- package/src/input/handler-picker-routes.ts +26 -13
- package/src/input/handler-shortcuts.ts +29 -6
- package/src/input/handler.ts +2 -2
- package/src/input/keybindings.ts +21 -3
- package/src/input/onboarding/onboarding-feature-units.ts +34 -27
- package/src/input/onboarding/onboarding-provider-key-fields.ts +112 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +22 -21
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +17 -0
- package/src/input/onboarding/onboarding-wizard-network-adopt.ts +2 -2
- package/src/input/onboarding/onboarding-wizard-steps.ts +30 -42
- package/src/input/panel-integration-actions.ts +2 -2
- package/src/input/panel-mouse-geometry.ts +1 -1
- package/src/input/panel-paste-flood-guard.ts +1 -1
- package/src/input/provider-key-intake.ts +167 -0
- package/src/input/sandbox-exec-config.ts +2 -2
- package/src/input/selection-modal.ts +1 -1
- package/src/input/settings-modal-activation.ts +5 -0
- package/src/input/settings-modal-adjustment.ts +4 -31
- package/src/input/settings-modal-data.ts +52 -57
- package/src/input/settings-modal-mutations.ts +12 -68
- package/src/input/settings-modal-types.ts +27 -17
- package/src/input/settings-modal.ts +74 -55
- package/src/main.ts +90 -90
- package/src/panels/agent-inspector-shared.ts +4 -4
- package/src/panels/base-panel.ts +2 -2
- package/src/panels/builtin/agent.ts +4 -4
- package/src/panels/builtin/development.ts +4 -4
- package/src/panels/builtin/knowledge.ts +1 -1
- package/src/panels/builtin/operations.ts +83 -11
- package/src/panels/builtin/session.ts +5 -5
- package/src/panels/builtin/shared.ts +16 -4
- package/src/panels/builtin-modals.ts +59 -42
- package/src/panels/builtin-panels.ts +1 -1
- package/src/panels/cost-tracker-panel.ts +124 -7
- package/src/panels/diff-panel.ts +39 -6
- package/src/panels/eval-registry.ts +2 -2
- package/src/panels/fleet-acts.ts +446 -0
- package/src/panels/fleet-deep-link.ts +1 -1
- package/src/panels/fleet-gateway.ts +117 -0
- package/src/panels/fleet-observed-render.ts +128 -0
- package/src/panels/fleet-panel-format.ts +163 -11
- package/src/panels/fleet-panel-worktree-detail.ts +14 -0
- package/src/panels/fleet-panel.ts +73 -57
- package/src/panels/fleet-read-model.ts +84 -14
- package/src/panels/fleet-spawn.ts +266 -0
- package/src/panels/fleet-steer.ts +2 -2
- package/src/panels/fleet-stop.ts +41 -5
- package/src/panels/fleet-tabs.ts +15 -16
- package/src/panels/fleet-transcript.ts +5 -5
- package/src/panels/git-panel.ts +2 -2
- package/src/panels/index.ts +4 -1
- package/src/panels/local-auth-panel.ts +1 -1
- package/src/panels/memory-consolidation-gateway.ts +94 -0
- package/src/panels/modals/devices-modal.ts +161 -0
- package/src/panels/modals/hooks-modal.ts +1 -1
- package/src/panels/modals/keybindings-modal.ts +2 -2
- package/src/panels/modals/knowledge-modal.ts +1 -1
- package/src/panels/modals/marketplace-modal.ts +4 -4
- package/src/panels/modals/memory-modal.ts +153 -9
- package/src/panels/modals/modal-surface-helpers.ts +1 -1
- package/src/panels/modals/modal-theme.ts +1 -1
- package/src/panels/modals/pairing-modal.ts +198 -68
- package/src/panels/modals/planning-modal.ts +2 -2
- package/src/panels/modals/plugins-modal.ts +1 -1
- package/src/panels/modals/policy-modal.ts +1 -1
- package/src/panels/modals/security-modal.ts +7 -1
- package/src/panels/modals/skills-modal.ts +1 -1
- package/src/panels/modals/work-plan-modal.ts +3 -3
- package/src/panels/notifications-feed.ts +127 -0
- package/src/panels/notifications-panel.ts +160 -0
- package/src/panels/panel-manager.ts +9 -9
- package/src/panels/plugins-panel.ts +4 -4
- package/src/panels/project-planning-answer-actions.ts +1 -1
- package/src/panels/provider-health-routes.ts +2 -2
- package/src/panels/skills-panel.ts +4 -5
- package/src/panels/token-budget-panel.ts +11 -3
- package/src/panels/types.ts +5 -5
- package/src/panels/workstream-graph-render.ts +123 -0
- package/src/permissions/broker-approval-card.ts +201 -0
- package/src/permissions/hunk-selection.ts +28 -4
- package/src/permissions/prompt.ts +244 -29
- package/src/renderer/autocomplete-overlay.ts +107 -54
- package/src/renderer/code-block.ts +2 -2
- package/src/renderer/compaction-history-modal.ts +1 -1
- package/src/renderer/compaction-preview.ts +1 -1
- package/src/renderer/compaction-quality.ts +1 -1
- package/src/renderer/composer-fragments.ts +69 -0
- package/src/renderer/compositor.ts +2 -2
- package/src/renderer/config-modal.ts +1 -1
- package/src/renderer/conversation-overlays.ts +1 -1
- package/src/renderer/footer-tips.ts +3 -3
- package/src/renderer/hint-grammar.ts +1 -1
- package/src/renderer/modal-factory.ts +1 -1
- package/src/renderer/model-picker-overlay.ts +1 -1
- package/src/renderer/model-workspace.ts +7 -2
- package/src/renderer/panel-composite.ts +2 -2
- package/src/renderer/panel-workspace-bar.ts +1 -1
- package/src/renderer/process-indicator.ts +1 -1
- package/src/renderer/selection-modal-overlay.ts +1 -1
- package/src/renderer/session-picker-modal.ts +2 -2
- package/src/renderer/settings-modal-helpers.ts +23 -5
- package/src/renderer/settings-modal.ts +129 -39
- package/src/renderer/shell-surface.ts +6 -0
- package/src/renderer/terminal-bg-probe.ts +1 -1
- package/src/renderer/theme.ts +3 -3
- package/src/renderer/tool-call.ts +13 -7
- package/src/renderer/tool-result-summary.ts +1 -1
- package/src/renderer/turn-injection.ts +7 -6
- package/src/renderer/ui-factory.ts +38 -1
- package/src/runtime/bootstrap-command-context.ts +7 -2
- package/src/runtime/bootstrap-command-parts.ts +9 -5
- package/src/runtime/bootstrap-core.ts +41 -41
- package/src/runtime/bootstrap-hook-bridge.ts +1 -1
- package/src/runtime/bootstrap-shell.ts +13 -5
- package/src/runtime/bootstrap.ts +34 -32
- package/src/runtime/channel-composition.ts +46 -0
- package/src/runtime/code-index-services.ts +3 -4
- package/src/runtime/daemon-attach-notices.ts +120 -0
- package/src/runtime/durability-services.ts +90 -0
- package/src/runtime/embedded-service-factories.ts +40 -0
- package/src/runtime/feature-settings.ts +147 -0
- package/src/runtime/fleet-services.ts +41 -0
- package/src/runtime/idle-power-services.ts +85 -0
- package/src/runtime/index.ts +6 -21
- package/src/runtime/interaction-seams.ts +109 -0
- package/src/runtime/legacy-daemon-migration.ts +29 -19
- package/src/runtime/memory-fold.ts +1 -1
- package/src/runtime/notification-dispatch.ts +110 -0
- package/src/runtime/onboarding/apply.ts +3 -29
- package/src/runtime/onboarding/fast-path.ts +39 -6
- package/src/runtime/onboarding/provider-key-capture.ts +126 -0
- package/src/runtime/onboarding/snapshot.ts +0 -1
- package/src/runtime/onboarding/types.ts +9 -3
- package/src/runtime/power-keepawake-remote.ts +75 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/provider-fallback.ts +55 -0
- package/src/runtime/provider-optimizer-wiring.ts +55 -0
- package/src/runtime/release-artifacts.ts +19 -103
- package/src/runtime/resume-notice.ts +1 -1
- package/src/runtime/services.ts +113 -113
- package/src/runtime/session-continuity-hints.ts +38 -0
- package/src/runtime/surface-feature-flags.ts +40 -9
- package/src/runtime/terminal-output-guard.ts +2 -2
- package/src/runtime/trust/workspace-registration.ts +10 -2
- package/src/runtime/trust/workspace-trust.ts +58 -31
- package/src/runtime/ui-services.ts +6 -1
- package/src/runtime/update-check.ts +15 -69
- package/src/runtime/workstream-services.ts +2 -2
- package/src/shell/blocking-input.ts +108 -132
- package/src/shell/recovery-input-helpers.ts +53 -166
- package/src/shell/ui-openers.ts +38 -6
- package/src/utils/format-duration.ts +1 -1
- package/src/verification/verification-ledger.ts +21 -4
- package/src/version.ts +1 -1
- package/src/input/commands/flags-runtime.ts +0 -328
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// interaction-seams.ts — the command-context interaction seams and the memory-
|
|
3
|
+
// provenance UI controller, extracted from main.ts (file-size hygiene).
|
|
4
|
+
//
|
|
5
|
+
// The seams expose the in-process orchestrator's per-tool cancel and mid-turn
|
|
6
|
+
// queue edit/delete, the power toggle/status, and the provenance-chip drill-in
|
|
7
|
+
// to the command layer. The memory-provenance controller owns the "used N
|
|
8
|
+
// memories" chip's state (the latest turn's metadata.memory.recordIds and the
|
|
9
|
+
// expand flag), its per-turn capture, and its render.
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
import type { CommandContext } from '../input/command-registry.ts';
|
|
13
|
+
import type { PowerManager } from '@pellux/goodvibes-sdk/platform/power';
|
|
14
|
+
import type { Line } from '../types/grid.ts';
|
|
15
|
+
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
16
|
+
import { createCancelToolCall, type ToolCancelOrchestrator } from '../core/turn-cancellation.ts';
|
|
17
|
+
import { powerSurfaceFromState } from '../core/power-status.ts';
|
|
18
|
+
import { memoryRecordIdsFromTurn, readMemoryShowProvenance } from '../core/memory-provenance.ts';
|
|
19
|
+
import { UIFactory } from '../renderer/ui-factory.ts';
|
|
20
|
+
|
|
21
|
+
/** The orchestrator surface the interaction seams need. */
|
|
22
|
+
export interface InteractionOrchestrator extends ToolCancelOrchestrator {
|
|
23
|
+
listQueuedMessages(): ReadonlyArray<{ id: string; queuedAt: number; text: string }>;
|
|
24
|
+
editQueuedMessage(id: string, text: string): boolean;
|
|
25
|
+
deleteQueuedMessage(id: string): boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface InteractionSeamDeps {
|
|
29
|
+
readonly orchestrator: InteractionOrchestrator;
|
|
30
|
+
readonly powerManager: PowerManager;
|
|
31
|
+
/** Topology-aware power projection (the chip source): the DAEMON's state in adopted-external mode. Falls back to the local manager when absent. */
|
|
32
|
+
readonly readPowerSurface?: (() => ReturnType<typeof powerSurfaceFromState>) | undefined;
|
|
33
|
+
readonly render: () => void;
|
|
34
|
+
readonly notify: (message: string) => void;
|
|
35
|
+
readonly getActiveToolCallId: () => string | undefined;
|
|
36
|
+
readonly toggleMemoryProvenance: () => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Wire the per-tool cancel, queue edit/delete, power, and memory-provenance seams onto the command context. */
|
|
40
|
+
export function wireInteractionSeams(cc: CommandContext, deps: InteractionSeamDeps): void {
|
|
41
|
+
cc.cancelToolCall = createCancelToolCall(deps.orchestrator, deps.getActiveToolCallId, () => {
|
|
42
|
+
deps.notify('[Tool] Cancelled the running tool call — the turn continues.');
|
|
43
|
+
deps.render();
|
|
44
|
+
});
|
|
45
|
+
cc.listQueuedMessages = () => deps.orchestrator.listQueuedMessages();
|
|
46
|
+
cc.editQueuedMessage = (id, text) => { const ok = deps.orchestrator.editQueuedMessage(id, text); if (ok) deps.render(); return ok; };
|
|
47
|
+
cc.deleteQueuedMessage = (id) => { const ok = deps.orchestrator.deleteQueuedMessage(id); if (ok) deps.render(); return ok; };
|
|
48
|
+
cc.toggleMemoryProvenance = deps.toggleMemoryProvenance;
|
|
49
|
+
cc.getPowerState = () => (deps.readPowerSurface ? deps.readPowerSurface() : powerSurfaceFromState(deps.powerManager.getState()));
|
|
50
|
+
cc.setKeepAwake = async (enabled) => { const next = powerSurfaceFromState(await deps.powerManager.setKeepAwake(enabled)); deps.render(); return next; };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The memory-provenance chip UI: per-turn capture, drill-in state, and render. */
|
|
54
|
+
export interface MemoryProvenanceUi {
|
|
55
|
+
/** Toggle the drill-in expand state (Alt+M). */
|
|
56
|
+
toggle(): void;
|
|
57
|
+
/** Capture the memory record ids from a completed turn's payload (metadata.memory.recordIds). */
|
|
58
|
+
onTurnCompleted(evt: unknown): void;
|
|
59
|
+
/** The chip lines for the current turn — empty when the setting is off or the turn used no memories. */
|
|
60
|
+
renderChip(width: number, configManager: Pick<ConfigManager, 'get'>): Line[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A resolved provenance record summary (null = the record is no longer available). */
|
|
64
|
+
type ResolvedRecord = { readonly summary: string; readonly cls: string } | null;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create the memory-provenance chip controller (default OFF; reads nothing when
|
|
68
|
+
* off). When the drill-in is expanded it RESOLVES each memory record id to its
|
|
69
|
+
* summary through the memory spine (the same host-or-daemon access the recall
|
|
70
|
+
* commands use, so an adopted daemon resolves over the wire) — per-id and
|
|
71
|
+
* lazily, so one forgotten record never blanks the rest and a collapsed chip
|
|
72
|
+
* costs nothing. Mirrors the webui's provenance detail fetch.
|
|
73
|
+
*/
|
|
74
|
+
export function createMemoryProvenanceUi(deps: {
|
|
75
|
+
readonly render: () => void;
|
|
76
|
+
readonly memorySpine: { get(id: string): Promise<{ summary: string; cls: string } | null> };
|
|
77
|
+
}): MemoryProvenanceUi {
|
|
78
|
+
let latestRecordIds: readonly string[] = [];
|
|
79
|
+
let expanded = false;
|
|
80
|
+
// id -> resolved record (null = not found). Absent = not yet fetched.
|
|
81
|
+
const resolved = new Map<string, ResolvedRecord>();
|
|
82
|
+
const inFlight = new Set<string>();
|
|
83
|
+
|
|
84
|
+
const resolveExpanded = (): void => {
|
|
85
|
+
for (const id of latestRecordIds) {
|
|
86
|
+
if (resolved.has(id) || inFlight.has(id)) continue;
|
|
87
|
+
inFlight.add(id);
|
|
88
|
+
void deps.memorySpine.get(id)
|
|
89
|
+
.then((record) => { resolved.set(id, record ? { summary: record.summary, cls: record.cls } : null); })
|
|
90
|
+
.catch(() => { resolved.set(id, null); })
|
|
91
|
+
.finally(() => { inFlight.delete(id); deps.render(); });
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
toggle() {
|
|
97
|
+
expanded = !expanded;
|
|
98
|
+
if (expanded) resolveExpanded();
|
|
99
|
+
deps.render();
|
|
100
|
+
},
|
|
101
|
+
onTurnCompleted(evt) { latestRecordIds = memoryRecordIdsFromTurn(evt); expanded = false; },
|
|
102
|
+
renderChip(width, configManager) {
|
|
103
|
+
if (!readMemoryShowProvenance(configManager) || latestRecordIds.length === 0) return [];
|
|
104
|
+
if (expanded) resolveExpanded(); // lazy: fetch any still-unresolved ids (idempotent)
|
|
105
|
+
const entries = latestRecordIds.map((id) => ({ id, record: resolved.has(id) ? resolved.get(id) : undefined }));
|
|
106
|
+
return UIFactory.createMemoryProvenanceChip(width, latestRecordIds.length, entries, expanded);
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The `goodvibes-daemon.service` detect/migrate engine, shared
|
|
3
3
|
* by the daemon CLI (`src/daemon/service-commands.ts`, the `migrate-service`
|
|
4
4
|
* subcommand) and the interactive TUI's onboarding guided UX
|
|
5
5
|
* (`src/input/handler-onboarding-daemon-adopt.ts`).
|
|
6
6
|
*
|
|
7
|
+
* NAMING, load-bearing: this module's identifiers say "legacy" because the
|
|
8
|
+
* engine migrates AWAY from the `goodvibes-daemon.service` unit name toward
|
|
9
|
+
* the runtime-managed unit — but that same name is what scripts/install.sh
|
|
10
|
+
* actively creates for curl-installed hosts TODAY. It is a parallel,
|
|
11
|
+
* first-class install path, not an obsolete one. User-facing copy therefore
|
|
12
|
+
* describes it as "the install-script unit" and never labels it legacy or
|
|
13
|
+
* implies it should be removed unless the user is explicitly migrating.
|
|
14
|
+
*
|
|
7
15
|
* This lives under `src/runtime/` — not `src/daemon/` — specifically so the
|
|
8
16
|
* `input` layer can consume it directly: the architecture gate's
|
|
9
17
|
* `input-no-entrypoints` rule forbids `src/input/**` from importing
|
|
@@ -250,13 +258,15 @@ export function resolveManagedUnitName(status: ManagedServiceStatus): string {
|
|
|
250
258
|
return basename(status.path).replace(/\.(service|plist)$/, '');
|
|
251
259
|
}
|
|
252
260
|
|
|
253
|
-
/** Honest one-line disclosure of the
|
|
261
|
+
/** Honest one-line disclosure of the install-script unit's presence/state plus a manual migration hint — never auto-acted-on. */
|
|
254
262
|
export function legacyUnitNote(legacy: LegacyUnitInfo, trackedServiceName: string): string {
|
|
255
263
|
const stateWord = legacy.active ? 'installed and RUNNING' : 'installed (not currently active)';
|
|
256
264
|
return (
|
|
257
|
-
`note: a service
|
|
258
|
-
`
|
|
259
|
-
|
|
265
|
+
`note: a separate service named ${LEGACY_SERVICE_UNIT_NAME}.service is ${stateWord} at ${legacy.path} — ` +
|
|
266
|
+
`that unit name is managed by the goodvibes install script (older installs used it too), while this tool manages ` +
|
|
267
|
+
`${trackedServiceName}.service and will not touch the other unit automatically. Keep whichever one you use; running ` +
|
|
268
|
+
`both would start two daemons competing for the same port. To retire the install-script unit in favor of this ` +
|
|
269
|
+
`tool's: systemctl --user disable --now ${LEGACY_SERVICE_UNIT_NAME}.service && rm ${legacy.path} && systemctl --user daemon-reload`
|
|
260
270
|
);
|
|
261
271
|
}
|
|
262
272
|
|
|
@@ -317,7 +327,7 @@ function assertUnitIsNotLegacy(status: ManagedServiceStatus, legacy: LegacyUnitI
|
|
|
317
327
|
if (status.path === legacy.path || resolveManagedUnitName(status) === LEGACY_SERVICE_UNIT_NAME) {
|
|
318
328
|
throw new Error(
|
|
319
329
|
`refusing to ${action}: the resolved managed unit (${resolveManagedUnitName(status)} at ${status.path}) is the ` +
|
|
320
|
-
`
|
|
330
|
+
`install-script ${LEGACY_SERVICE_UNIT_NAME}.service unit — this should already have been caught by the pre-flight ` +
|
|
321
331
|
'collision check in runLegacyDaemonMigration',
|
|
322
332
|
);
|
|
323
333
|
}
|
|
@@ -356,7 +366,7 @@ export async function runLegacyDaemonMigration(
|
|
|
356
366
|
ok: false,
|
|
357
367
|
exitCode: 1,
|
|
358
368
|
lines: [
|
|
359
|
-
`migrate-service: no
|
|
369
|
+
`migrate-service: no install-script ${LEGACY_SERVICE_UNIT_NAME}.service unit was found, but something is already ` +
|
|
360
370
|
`listening on ${params.host}:${params.port}.`,
|
|
361
371
|
"That looks like a process this tool doesn't manage (for example, a manually-started `nohup` daemon) rather " +
|
|
362
372
|
'than a systemd unit — there is nothing here to stop or disable, and this tool will not attempt to kill an ' +
|
|
@@ -371,7 +381,7 @@ export async function runLegacyDaemonMigration(
|
|
|
371
381
|
ok: true,
|
|
372
382
|
exitCode: 0,
|
|
373
383
|
lines: [
|
|
374
|
-
`migrate-service: no
|
|
384
|
+
`migrate-service: no install-script ${LEGACY_SERVICE_UNIT_NAME}.service unit was found and ${params.host}:${params.port} ` +
|
|
375
385
|
'is free — there is nothing to migrate.',
|
|
376
386
|
`Run install-service to set up the managed ${resolvedUnitName}.service directly.`,
|
|
377
387
|
],
|
|
@@ -385,8 +395,8 @@ export async function runLegacyDaemonMigration(
|
|
|
385
395
|
exitCode: 1,
|
|
386
396
|
lines: [
|
|
387
397
|
`migrate-service: this host's detected service platform is '${currentStatus.platform}', not systemd, but a ` +
|
|
388
|
-
`
|
|
389
|
-
'
|
|
398
|
+
`unit file with the install-script name exists at ${legacy.path}.`,
|
|
399
|
+
'That unit is systemd-specific and this tool only knows how to migrate a systemd unit today — ' +
|
|
390
400
|
'nothing was changed.',
|
|
391
401
|
],
|
|
392
402
|
status: currentStatus,
|
|
@@ -409,9 +419,9 @@ export async function runLegacyDaemonMigration(
|
|
|
409
419
|
exitCode: 1,
|
|
410
420
|
lines: [
|
|
411
421
|
`migrate-service aborted: this host's 'service.serviceName' config key resolves to '${resolvedUnitName}', which ` +
|
|
412
|
-
`is the exact
|
|
422
|
+
`is the exact install-script unit name (${LEGACY_SERVICE_UNIT_NAME}.service at ${legacy.path}) this migration is ` +
|
|
413
423
|
'supposed to retire.',
|
|
414
|
-
'Installing or rolling back a unit under that name would overwrite or delete the
|
|
424
|
+
'Installing or rolling back a unit under that name would overwrite or delete the install-script unit instead of ' +
|
|
415
425
|
'managing a separate one, so nothing has been changed.',
|
|
416
426
|
`Fix: set the 'service.serviceName' config key to something other than '${LEGACY_SERVICE_UNIT_NAME}' (for ` +
|
|
417
427
|
`example, the default '${trackedServiceName}') and re-run migrate-service.`,
|
|
@@ -429,7 +439,7 @@ export async function runLegacyDaemonMigration(
|
|
|
429
439
|
'migrate-service (dry run — re-run with confirmation to execute): this would',
|
|
430
440
|
` 1. install and start the new ${resolvedUnitName}.service unit`,
|
|
431
441
|
' 2. verify it comes up healthy (a fresh, honest systemd is-active check)',
|
|
432
|
-
` 3. only if that succeeds, stop, disable, and remove the
|
|
442
|
+
` 3. only if that succeeds, stop, disable, and remove the install-script ${LEGACY_SERVICE_UNIT_NAME}.service unit ` +
|
|
433
443
|
'and run `systemctl --user daemon-reload`',
|
|
434
444
|
'Nothing has been changed. Nothing is migrated automatically — re-run with explicit confirmation ' +
|
|
435
445
|
"(the CLI's -y/--yes flag) to execute this plan.",
|
|
@@ -452,7 +462,7 @@ export async function runLegacyDaemonMigration(
|
|
|
452
462
|
exitCode: 1,
|
|
453
463
|
lines: [
|
|
454
464
|
`migrate-service aborted: could not write the new ${resolvedUnitName}.service unit (${installed.actionError}).`,
|
|
455
|
-
`The
|
|
465
|
+
`The install-script ${LEGACY_SERVICE_UNIT_NAME}.service unit was never touched.`,
|
|
456
466
|
],
|
|
457
467
|
status: installed,
|
|
458
468
|
};
|
|
@@ -473,7 +483,7 @@ export async function runLegacyDaemonMigration(
|
|
|
473
483
|
`migrate-service aborted: the new ${resolvedUnitName}.service unit did not come up healthy` +
|
|
474
484
|
(started.actionError ? ` (${started.actionError}).` : '.'),
|
|
475
485
|
rollbackNote,
|
|
476
|
-
`The
|
|
486
|
+
`The install-script ${LEGACY_SERVICE_UNIT_NAME}.service unit was never touched and should still be running as before.`,
|
|
477
487
|
],
|
|
478
488
|
status: healthCheck,
|
|
479
489
|
};
|
|
@@ -496,20 +506,20 @@ export async function runLegacyDaemonMigration(
|
|
|
496
506
|
const lines = [`migrated: the new ${resolvedUnitName}.service unit is installed, enabled, and running.`];
|
|
497
507
|
if ((stopResult.status ?? 1) !== 0) {
|
|
498
508
|
lines.push(
|
|
499
|
-
`note: stopping the
|
|
509
|
+
`note: stopping the install-script unit reported a non-zero exit (${stopResult.stderr ?? stopResult.stdout ?? 'no output'}); ` +
|
|
500
510
|
'it may already have been stopped.',
|
|
501
511
|
);
|
|
502
512
|
}
|
|
503
513
|
if ((disableResult.status ?? 1) !== 0) {
|
|
504
514
|
lines.push(
|
|
505
|
-
`note: disabling the
|
|
515
|
+
`note: disabling the install-script unit reported a non-zero exit (${disableResult.stderr ?? disableResult.stdout ?? 'no output'}); ` +
|
|
506
516
|
'it may already have been disabled.',
|
|
507
517
|
);
|
|
508
518
|
}
|
|
509
519
|
if (removeError) {
|
|
510
|
-
lines.push(`note: could not remove the
|
|
520
|
+
lines.push(`note: could not remove the install-script unit file at ${legacy.path}: ${removeError} — remove it by hand.`);
|
|
511
521
|
} else {
|
|
512
|
-
lines.push(`the
|
|
522
|
+
lines.push(`the install-script ${LEGACY_SERVICE_UNIT_NAME}.service unit has been stopped, disabled, and removed.`);
|
|
513
523
|
}
|
|
514
524
|
lines.push('ran `systemctl --user daemon-reload`.');
|
|
515
525
|
return { ok: true, exitCode: 0, lines, status: healthCheck };
|
|
@@ -21,7 +21,7 @@ export async function foldTuiLegacyMemory(
|
|
|
21
21
|
): Promise<MemoryFoldReport> {
|
|
22
22
|
const legacyTuiProject = join(workingDirectory, '.goodvibes', 'tui', 'memory.sqlite');
|
|
23
23
|
const sources: LegacyMemorySource[] = [
|
|
24
|
-
{ label: `tui:${workingDirectory} (
|
|
24
|
+
{ label: `tui:${workingDirectory} (legacy per-project store)`, dbPath: legacyTuiProject },
|
|
25
25
|
];
|
|
26
26
|
return foldMemoryStores(memoryStore, sources, { embeddingRegistry: memoryEmbeddingRegistry });
|
|
27
27
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notification dispatch — the production wiring that finally gives the
|
|
3
|
+
* panel_only notification target a live producer.
|
|
4
|
+
*
|
|
5
|
+
* The SDK's NotificationRouter decides where each domain notification goes
|
|
6
|
+
* (conversation / status_bar / panel_only) and collapses bursts and batches.
|
|
7
|
+
* Nothing in the running app routed real notifications before this: the
|
|
8
|
+
* panel_only feed (panels/notifications-feed.ts) and its panel existed with no
|
|
9
|
+
* upstream. This module builds the router, records every panel_only decision
|
|
10
|
+
* (including burst-collapsed ones) into the shared feed, and bridges the
|
|
11
|
+
* runtime event bus so real domain events become notifications.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createNotificationRouter, type NotificationRouter } from '@pellux/goodvibes-sdk/platform/runtime/ui';
|
|
15
|
+
import type { Notification, RoutingDecision, RuntimeEventBus, RuntimeEventDomain } from '@/runtime/index.ts';
|
|
16
|
+
import type { ConfigManager } from '../config/index.ts';
|
|
17
|
+
import { getSharedNotificationFeed, type PanelNotificationFeed } from '../panels/notifications-feed.ts';
|
|
18
|
+
|
|
19
|
+
export interface NotificationDispatcher {
|
|
20
|
+
/** Route a notification; a panel_only (or burst-collapsed) decision lands in the feed. Returns the decision. */
|
|
21
|
+
dispatch(notification: Notification): RoutingDecision;
|
|
22
|
+
/** Surface batch-held notifications (call on a timer / when quiet-typing ends). */
|
|
23
|
+
flush(): void;
|
|
24
|
+
readonly router: NotificationRouter;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The runtime event domains whose events surface as operational notifications.
|
|
29
|
+
* Deliberately a curated set of user-relevant, completion/attention-shaped
|
|
30
|
+
* domains — not every domain — so the panel reflects meaningful operational
|
|
31
|
+
* activity rather than raw event churn. The router's per-domain verbosity and
|
|
32
|
+
* burst/batch policies still collapse floods within these.
|
|
33
|
+
*/
|
|
34
|
+
export const NOTIFICATION_BRIDGE_DOMAINS: readonly RuntimeEventDomain[] = [
|
|
35
|
+
'agents',
|
|
36
|
+
'tasks',
|
|
37
|
+
'workflows',
|
|
38
|
+
'automation',
|
|
39
|
+
'deliveries',
|
|
40
|
+
'security',
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/** Turn an UPPER_SNAKE event type into a short human title ("AGENT_COMPLETED" → "Agent completed"). */
|
|
44
|
+
export function humanizeEventType(type: string): string {
|
|
45
|
+
const words = type.toLowerCase().split(/[_\s]+/).filter(Boolean);
|
|
46
|
+
if (words.length === 0) return type;
|
|
47
|
+
return words.map((word, index) => (index === 0 ? word[0]!.toUpperCase() + word.slice(1) : word)).join(' ');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Derive a notification severity from an event type — errors/failures warn, the rest are informational. */
|
|
51
|
+
export function levelForEventType(type: string): Notification['level'] {
|
|
52
|
+
const upper = type.toUpperCase();
|
|
53
|
+
if (/(FAILED|ERROR|CRASH|DENIED|BLOCKED)/.test(upper)) return 'warning';
|
|
54
|
+
if (/(CRITICAL|FATAL|BREACH)/.test(upper)) return 'critical';
|
|
55
|
+
return 'info';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function createNotificationDispatcher(
|
|
59
|
+
configManager: Pick<ConfigManager, 'get'>,
|
|
60
|
+
feed: PanelNotificationFeed = getSharedNotificationFeed(),
|
|
61
|
+
): NotificationDispatcher {
|
|
62
|
+
const router = createNotificationRouter(undefined, undefined, configManager);
|
|
63
|
+
const recordIfPanel = (notification: Notification, decision: RoutingDecision): void => {
|
|
64
|
+
if (decision.suppressed) return;
|
|
65
|
+
if (decision.target === 'panel_only') feed.record(notification, decision);
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
router,
|
|
69
|
+
dispatch(notification) {
|
|
70
|
+
const decision = router.route(notification);
|
|
71
|
+
recordIfPanel(notification, decision);
|
|
72
|
+
return decision;
|
|
73
|
+
},
|
|
74
|
+
flush() {
|
|
75
|
+
for (const { notification } of router.flush()) {
|
|
76
|
+
// A flushed batch head surfaces as a batch-collapsed panel entry; the
|
|
77
|
+
// feed folds all sharing this batch key into one running-count row.
|
|
78
|
+
feed.record(notification, {
|
|
79
|
+
target: 'panel_only',
|
|
80
|
+
reasonCode: 'batch_window_collapsed',
|
|
81
|
+
batchKey: `${notification.domain}:${notification.level}`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Bridge the runtime event bus to the dispatcher: each event in a curated
|
|
90
|
+
* domain becomes a notification and is routed. Returns an unsubscribe function
|
|
91
|
+
* that detaches every domain listener.
|
|
92
|
+
*/
|
|
93
|
+
export function wireRuntimeNotificationBridge(
|
|
94
|
+
runtimeBus: RuntimeEventBus,
|
|
95
|
+
dispatcher: Pick<NotificationDispatcher, 'dispatch'>,
|
|
96
|
+
domains: readonly RuntimeEventDomain[] = NOTIFICATION_BRIDGE_DOMAINS,
|
|
97
|
+
): () => void {
|
|
98
|
+
const unsubscribes = domains.map((domain) =>
|
|
99
|
+
runtimeBus.onDomain(domain, (envelope) => {
|
|
100
|
+
dispatcher.dispatch({
|
|
101
|
+
id: envelope.traceId ?? `${domain}-${envelope.type}-${envelope.ts}`,
|
|
102
|
+
domain,
|
|
103
|
+
level: levelForEventType(envelope.type),
|
|
104
|
+
title: humanizeEventType(envelope.type),
|
|
105
|
+
timestamp: envelope.ts,
|
|
106
|
+
});
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
return () => { for (const unsubscribe of unsubscribes) unsubscribe(); };
|
|
110
|
+
}
|
|
@@ -2,7 +2,6 @@ import { existsSync, readFileSync, unlinkSync } from 'node:fs';
|
|
|
2
2
|
import { atomicWriteFileSync } from '../../config/atomic-write.ts';
|
|
3
3
|
import { isSecretRefInput } from '@pellux/goodvibes-sdk/platform/config';
|
|
4
4
|
import { CONFIG_SCHEMA, DEFAULT_CONFIG } from '../../config/index.ts';
|
|
5
|
-
import type { FeatureFlagConfigKey } from '../surface-feature-flags.ts';
|
|
6
5
|
import {
|
|
7
6
|
getOnboardingRuntimeStatePath,
|
|
8
7
|
readOnboardingRuntimeState,
|
|
@@ -133,39 +132,14 @@ function isMalformedGoodVibesSecretReferenceValue(value: string): boolean {
|
|
|
133
132
|
return normalized.startsWith('goodvibes://') && !isGoodVibesSecretReferenceValue(normalized);
|
|
134
133
|
}
|
|
135
134
|
|
|
136
|
-
function isFeatureFlagConfigKey(key: string): key is FeatureFlagConfigKey {
|
|
137
|
-
return key === 'featureFlags' || key.startsWith('featureFlags.');
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function validateFeatureFlagConfigValue(operation: Extract<OnboardingApplyOperation, { kind: 'set-config' }>): boolean {
|
|
141
|
-
if (!isFeatureFlagConfigKey(operation.key)) return false;
|
|
142
|
-
|
|
143
|
-
if (operation.key === 'featureFlags') {
|
|
144
|
-
if (!isPlainObject(operation.value)) throw new Error('featureFlags expects an object value.');
|
|
145
|
-
for (const [flagId, state] of Object.entries(operation.value)) {
|
|
146
|
-
if (flagId.trim().length === 0) throw new Error('featureFlags cannot contain an empty feature id.');
|
|
147
|
-
if (state !== 'enabled' && state !== 'disabled') {
|
|
148
|
-
throw new Error(`featureFlags.${flagId} expects enabled or disabled.`);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
return true;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const flagId = operation.key.slice('featureFlags.'.length);
|
|
155
|
-
if (flagId.trim().length === 0) throw new Error('featureFlags requires a feature id.');
|
|
156
|
-
if (operation.value !== 'enabled' && operation.value !== 'disabled') {
|
|
157
|
-
throw new Error(`Config key ${operation.key} expects enabled or disabled.`);
|
|
158
|
-
}
|
|
159
|
-
return true;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
135
|
function validateConfigValue(operation: Extract<OnboardingApplyOperation, { kind: 'set-config' }>): void {
|
|
163
136
|
if (typeof operation.value === 'string' && isMalformedGoodVibesSecretReferenceValue(operation.value)) {
|
|
164
137
|
throw new Error(`Config key ${operation.key} only accepts goodvibes://secrets/... secret references.`);
|
|
165
138
|
}
|
|
166
139
|
|
|
167
|
-
|
|
168
|
-
|
|
140
|
+
// Feature enablement arrives as plain domain settings writes (e.g.
|
|
141
|
+
// sandbox.enabled, controlPlane.gateway), validated by the schema path below
|
|
142
|
+
// like any other key — there is no separate enablement namespace.
|
|
169
143
|
const schema = CONFIG_SCHEMA.find((entry) => entry.key === operation.key);
|
|
170
144
|
if (!schema) {
|
|
171
145
|
const defaultValue = operation.key.split('.').reduce<unknown>((cursor, part) => (
|
|
@@ -7,15 +7,24 @@
|
|
|
7
7
|
* - If a provider API key is already configured (env / secrets), skip
|
|
8
8
|
* onboarding entirely — write the check marker and drop straight into a
|
|
9
9
|
* working session. ZERO steps.
|
|
10
|
-
* - Otherwise ask exactly one question: start now on the
|
|
11
|
-
*
|
|
12
|
-
* always skippable.
|
|
10
|
+
* - Otherwise ask exactly one question: start now on the default model, or
|
|
11
|
+
* run the full guided setup. Dismissing (Esc) starts now — so it is
|
|
12
|
+
* always skippable. The start-now copy is GENERATED from the default
|
|
13
|
+
* model's registered auth state (buildOnboardingModelCopy), so a
|
|
14
|
+
* "no API key needed" promise can only render when the provider is
|
|
15
|
+
* genuinely keyless-ready.
|
|
13
16
|
*
|
|
14
17
|
* The full wizard remains one keystroke away (the "Full guided setup" choice,
|
|
15
18
|
* or `/onboarding` at any time). If the command context can't answer whether a
|
|
16
19
|
* provider is configured (e.g. a headless/test surface), we fall back to the
|
|
17
20
|
* classic behavior of opening the wizard, so nothing regresses.
|
|
18
21
|
*/
|
|
22
|
+
import {
|
|
23
|
+
buildOnboardingModelCopy,
|
|
24
|
+
resolveDefaultModelReadiness,
|
|
25
|
+
type ModelProviderSource,
|
|
26
|
+
type OnboardingModelCopy,
|
|
27
|
+
} from '@pellux/goodvibes-sdk/platform/providers';
|
|
19
28
|
import type { CommandContext } from '../../input/command-registry.ts';
|
|
20
29
|
import type { SelectionItem } from '../../input/selection-modal.ts';
|
|
21
30
|
import { writeOnboardingCheckMarker } from './markers.ts';
|
|
@@ -43,6 +52,27 @@ function readConfiguredProviderIds(commandContext: CommandContext): string[] | n
|
|
|
43
52
|
|
|
44
53
|
const MARKER_OPTIONS: WriteOnboardingCheckMarkerOptions = { source: 'command' };
|
|
45
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Copy for the "start now" choice, GENERATED from the default model's live
|
|
57
|
+
* registration state (never hand-written): a "no API key needed" promise can
|
|
58
|
+
* only appear when the provider's own auth state proves keyless readiness.
|
|
59
|
+
* Surfaces that cannot answer (no registry / no getForModel) resolve to the
|
|
60
|
+
* honest 'unresolvable' copy rather than a fabricated keyless claim.
|
|
61
|
+
*/
|
|
62
|
+
function resolveStartCopy(commandContext: CommandContext): OnboardingModelCopy {
|
|
63
|
+
const modelKey = String(commandContext.platform?.config?.provider?.model ?? '').trim();
|
|
64
|
+
const registry = commandContext.provider?.providerRegistry;
|
|
65
|
+
const source: ModelProviderSource =
|
|
66
|
+
registry && typeof (registry as Partial<ModelProviderSource>).getForModel === 'function'
|
|
67
|
+
? registry
|
|
68
|
+
: {
|
|
69
|
+
getForModel: () => {
|
|
70
|
+
throw new Error('provider registry unavailable on this surface');
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
return buildOnboardingModelCopy(resolveDefaultModelReadiness(source, modelKey));
|
|
74
|
+
}
|
|
75
|
+
|
|
46
76
|
/**
|
|
47
77
|
* Run the fast-path first-run flow. Returns nothing; side effects (wizard open,
|
|
48
78
|
* marker write, modal) happen through the injected deps.
|
|
@@ -68,8 +98,9 @@ export function startOnboardingFastPath(deps: OnboardingFastPathDeps): void {
|
|
|
68
98
|
return;
|
|
69
99
|
}
|
|
70
100
|
|
|
101
|
+
const startCopy = resolveStartCopy(commandContext);
|
|
71
102
|
const items: SelectionItem[] = [
|
|
72
|
-
{ id: 'start-now', label:
|
|
103
|
+
{ id: 'start-now', label: startCopy.headline, detail: startCopy.detail, primaryAction: 'select' },
|
|
73
104
|
{ id: 'full-setup', label: 'Full guided setup', detail: 'Choose provider, model, channels, and more', primaryAction: 'select' },
|
|
74
105
|
];
|
|
75
106
|
commandContext.openSelection(
|
|
@@ -82,10 +113,12 @@ export function startOnboardingFastPath(deps: OnboardingFastPathDeps): void {
|
|
|
82
113
|
return;
|
|
83
114
|
}
|
|
84
115
|
// Start now (or dismissed): mark first-run done and drop into a working
|
|
85
|
-
// session on the
|
|
116
|
+
// session on the default model. The status line reuses the generated
|
|
117
|
+
// copy so it can never promise keyless readiness the registration
|
|
118
|
+
// state does not prove.
|
|
86
119
|
writeOnboardingCheckMarker(shellPaths, MARKER_OPTIONS);
|
|
87
120
|
commandContext.print?.(
|
|
88
|
-
|
|
121
|
+
`${startCopy.detail}. Add a provider key anytime with /provider, or run /onboarding for full setup.`,
|
|
89
122
|
);
|
|
90
123
|
render();
|
|
91
124
|
},
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-agnostic key capture for onboarding.
|
|
3
|
+
*
|
|
4
|
+
* The onboarding wizard offers an API-key field for every provider the registry
|
|
5
|
+
* knows accepts a key — derived from each provider's registration-time auth
|
|
6
|
+
* state (describeAuthState), never a hardcoded vendor list. This module holds
|
|
7
|
+
* the two shared pieces that keep the wizard step and the apply step in lockstep:
|
|
8
|
+
*
|
|
9
|
+
* 1. enrichProviderAccountsSnapshot — stamps each provider-account record with
|
|
10
|
+
* the secrets-store key it reads its API key from (its declared auth env
|
|
11
|
+
* var), read live from the provider registry.
|
|
12
|
+
* 2. deriveProviderKeyCaptureTargets / providerKeyFieldId — the single source
|
|
13
|
+
* of truth for which providers get a key field and how those fields are
|
|
14
|
+
* named, used by both the field builder and the apply writer.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { LLMProvider } from '@pellux/goodvibes-sdk/platform/providers';
|
|
18
|
+
import type {
|
|
19
|
+
OnboardingProviderAccountRecord,
|
|
20
|
+
OnboardingProviderAccountsSnapshot,
|
|
21
|
+
} from './types.ts';
|
|
22
|
+
|
|
23
|
+
/** Minimal provider lookup the enrichment needs (structural for testing). */
|
|
24
|
+
export interface ProviderKeyRegistryLike {
|
|
25
|
+
tryGet(name: string): LLMProvider | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Read a provider's declared API-key env var, or undefined when it accepts none. */
|
|
29
|
+
export function readProviderApiKeyEnvVar(
|
|
30
|
+
registry: ProviderKeyRegistryLike,
|
|
31
|
+
providerId: string,
|
|
32
|
+
): string | undefined {
|
|
33
|
+
let provider: LLMProvider | undefined;
|
|
34
|
+
try {
|
|
35
|
+
provider = registry.tryGet(providerId);
|
|
36
|
+
} catch {
|
|
37
|
+
provider = undefined;
|
|
38
|
+
}
|
|
39
|
+
const envVars = provider?.describeAuthState?.().authEnvVars;
|
|
40
|
+
return envVars && envVars.length > 0 ? envVars[0] : undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Return an equivalent snapshot with each provider record stamped with the
|
|
45
|
+
* secrets-store key it accepts an API key under (from live registration truth).
|
|
46
|
+
* Records for keyless / subscription-only providers keep apiKeyEnvVar absent.
|
|
47
|
+
*/
|
|
48
|
+
export function enrichProviderAccountsSnapshot(
|
|
49
|
+
snapshot: OnboardingProviderAccountsSnapshot,
|
|
50
|
+
registry: ProviderKeyRegistryLike,
|
|
51
|
+
): OnboardingProviderAccountsSnapshot {
|
|
52
|
+
return {
|
|
53
|
+
...snapshot,
|
|
54
|
+
providers: snapshot.providers.map((record) => {
|
|
55
|
+
const apiKeyEnvVar = readProviderApiKeyEnvVar(registry, record.providerId);
|
|
56
|
+
return apiKeyEnvVar ? { ...record, apiKeyEnvVar } : record;
|
|
57
|
+
}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A provider that onboarding can offer an API-key field for. */
|
|
62
|
+
export interface ProviderKeyCaptureTarget {
|
|
63
|
+
readonly providerId: string;
|
|
64
|
+
readonly apiKeyEnvVar: string;
|
|
65
|
+
readonly configured: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The providers onboarding offers an API-key field for: every registered
|
|
70
|
+
* provider that declares an api-key route AND carries a resolved auth env var.
|
|
71
|
+
* Ordered by provider id so the wizard renders deterministically.
|
|
72
|
+
*/
|
|
73
|
+
export function deriveProviderKeyCaptureTargets(
|
|
74
|
+
providers: readonly OnboardingProviderAccountRecord[] | undefined,
|
|
75
|
+
): ProviderKeyCaptureTarget[] {
|
|
76
|
+
if (!providers) return [];
|
|
77
|
+
return providers
|
|
78
|
+
.filter((record) => record.apiKeyEnvVar && record.availableRoutes.includes('api-key'))
|
|
79
|
+
.map((record) => ({
|
|
80
|
+
providerId: record.providerId,
|
|
81
|
+
apiKeyEnvVar: record.apiKeyEnvVar as string,
|
|
82
|
+
configured: record.configured,
|
|
83
|
+
}))
|
|
84
|
+
.sort((a, b) => a.providerId.localeCompare(b.providerId));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The wizard field id that captures a given provider's API key. */
|
|
88
|
+
export function providerKeyFieldId(providerId: string): string {
|
|
89
|
+
return `providers.api-key.${providerId}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** A subscription-capable provider and its current subscription posture. */
|
|
93
|
+
export interface SubscriptionStatusTarget {
|
|
94
|
+
readonly providerId: string;
|
|
95
|
+
readonly active: boolean;
|
|
96
|
+
readonly pending: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The providers onboarding shows a subscription-status row for: every provider
|
|
101
|
+
* that declares a subscription route (registration truth), plus any provider
|
|
102
|
+
* with an active or pending subscription session — so a live session is never
|
|
103
|
+
* hidden just because its account record was absent. Ordered by provider id.
|
|
104
|
+
*/
|
|
105
|
+
export function deriveSubscriptionStatusTargets(
|
|
106
|
+
providers: readonly OnboardingProviderAccountRecord[] | undefined,
|
|
107
|
+
activeProviderIds: readonly string[],
|
|
108
|
+
pendingProviderIds: readonly string[],
|
|
109
|
+
): SubscriptionStatusTarget[] {
|
|
110
|
+
const active = new Set(activeProviderIds);
|
|
111
|
+
const pending = new Set(pendingProviderIds);
|
|
112
|
+
const ids = new Set<string>();
|
|
113
|
+
for (const record of providers ?? []) {
|
|
114
|
+
if (record.availableRoutes.includes('subscription')) ids.add(record.providerId);
|
|
115
|
+
}
|
|
116
|
+
for (const id of active) ids.add(id);
|
|
117
|
+
for (const id of pending) ids.add(id);
|
|
118
|
+
return [...ids]
|
|
119
|
+
.sort((a, b) => a.localeCompare(b))
|
|
120
|
+
.map((providerId) => ({ providerId, active: active.has(providerId), pending: pending.has(providerId) }));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The wizard field id of a provider's subscription-status row. */
|
|
124
|
+
export function subscriptionStatusFieldId(providerId: string): string {
|
|
125
|
+
return `providers.subscription.${providerId}`;
|
|
126
|
+
}
|
|
@@ -43,7 +43,6 @@ function buildConfigSnapshot(
|
|
|
43
43
|
network: config.getCategory('network'),
|
|
44
44
|
surfaces: config.getCategory('surfaces'),
|
|
45
45
|
service: config.getCategory('service'),
|
|
46
|
-
featureFlags: config.getCategory('featureFlags'),
|
|
47
46
|
batch: config.getCategory('batch'),
|
|
48
47
|
cloudflare: config.getCategory('cloudflare'),
|
|
49
48
|
};
|