@pellux/goodvibes-tui 1.9.0 → 1.9.2
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 +24 -2
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/audio/player.ts +91 -5
- package/src/audio/spoken-turn-controller.ts +30 -2
- package/src/audio/spoken-turn-wiring.ts +3 -0
- package/src/config/credential-availability.ts +122 -0
- package/src/config/index.ts +15 -0
- package/src/core/turn-cancellation.ts +1 -1
- package/src/daemon/cli.ts +4 -4
- package/src/daemon/service-commands.ts +8 -8
- package/src/input/command-registry.ts +5 -5
- package/src/input/commands/config.ts +1 -1
- package/src/input/commands/memory.ts +1 -1
- package/src/input/commands/planning-runtime.ts +1 -1
- package/src/input/commands/recall-review.ts +2 -2
- package/src/input/commands/session-content.ts +1 -1
- package/src/input/feed-context-factory.ts +1 -1
- package/src/input/handler-content-actions.ts +2 -2
- package/src/input/handler-feed-routes.ts +2 -2
- package/src/input/handler-feed.ts +1 -1
- package/src/input/handler-onboarding-daemon-adopt.ts +3 -3
- package/src/input/handler-picker-routes.ts +2 -2
- package/src/input/handler-shortcuts.ts +1 -1
- package/src/input/model-picker.ts +2 -2
- package/src/input/onboarding/onboarding-wizard-apply.ts +3 -3
- package/src/input/onboarding/onboarding-wizard-network-adopt.ts +3 -3
- package/src/input/onboarding/onboarding-wizard-steps.ts +1 -1
- package/src/input/session-picker-modal.ts +1 -1
- package/src/input/settings-modal-data.ts +2 -2
- package/src/main.ts +5 -4
- package/src/panels/base-panel.ts +1 -1
- package/src/panels/builtin/operations.ts +4 -4
- package/src/panels/builtin/shared.ts +1 -1
- package/src/panels/fleet-panel-format.ts +1 -1
- package/src/panels/fleet-panel.ts +15 -15
- package/src/panels/fleet-read-model.ts +11 -11
- package/src/panels/fleet-steer.ts +2 -2
- package/src/panels/fleet-stop.ts +2 -2
- package/src/panels/fleet-tabs.ts +4 -4
- package/src/panels/fleet-transcript.ts +5 -5
- package/src/panels/types.ts +1 -1
- package/src/renderer/compaction-quality.ts +1 -1
- package/src/renderer/fleet-tab-strip.ts +2 -2
- package/src/renderer/footer-tips.ts +1 -1
- package/src/renderer/model-workspace.ts +1 -1
- package/src/renderer/session-picker-modal.ts +4 -4
- package/src/renderer/shell-surface.ts +1 -1
- package/src/renderer/status-glyphs.ts +3 -3
- package/src/renderer/terminal-bg-probe.ts +1 -1
- package/src/renderer/theme.ts +2 -2
- package/src/renderer/turn-injection.ts +3 -3
- package/src/renderer/ui-factory.ts +7 -7
- package/src/renderer/ui-primitives.ts +3 -3
- package/src/runtime/bootstrap-core.ts +15 -15
- package/src/runtime/bootstrap-hook-bridge.ts +2 -2
- package/src/runtime/bootstrap-shell.ts +3 -3
- package/src/runtime/bootstrap.ts +5 -5
- package/src/runtime/code-index-services.ts +2 -2
- package/src/runtime/index.ts +11 -3
- package/src/runtime/legacy-daemon-migration.ts +9 -9
- package/src/runtime/memory-fold.ts +30 -1
- package/src/runtime/onboarding/snapshot.ts +3 -3
- package/src/runtime/onboarding/types.ts +2 -2
- package/src/runtime/operator-token-cleanup.ts +2 -2
- package/src/runtime/orchestrator-core-services.ts +2 -2
- package/src/runtime/process-lifecycle.ts +19 -5
- package/src/runtime/services.ts +14 -14
- package/src/runtime/session-inbound-inputs.ts +1 -1
- package/src/runtime/session-spine-transport.ts +2 -2
- package/src/runtime/ui-services.ts +1 -1
- package/src/runtime/workstream-services.ts +1 -1
- package/src/runtime/wrfc-persistence.ts +2 -2
- package/src/shell/ui-openers.ts +2 -2
- package/src/version.ts +1 -1
|
@@ -447,8 +447,8 @@ export function handleCtrlC(
|
|
|
447
447
|
} catch {
|
|
448
448
|
// Non-fatal to the exit chord; the quit-window bookkeeping still runs.
|
|
449
449
|
}
|
|
450
|
-
// A press that silenced LIVE speech is consumed by that job (
|
|
451
|
-
//
|
|
450
|
+
// A press that silenced LIVE speech is consumed by that job (an earlier replay
|
|
451
|
+
// fix) — symmetric with the prompt-clearing press above. The quit chord
|
|
452
452
|
// ("Ctrl+C x2") starts from a quiet state; turn-aborts still count toward
|
|
453
453
|
// the double-press.
|
|
454
454
|
if (stoppedSpeech) return;
|
|
@@ -189,7 +189,7 @@ export function handlePanelFocusToken(state: PanelFocusRouteState, token: InputT
|
|
|
189
189
|
export type IndicatorFocusRouteState = {
|
|
190
190
|
indicatorFocused: boolean;
|
|
191
191
|
modalOpened: (name: string) => void;
|
|
192
|
-
/**
|
|
192
|
+
/** The footer process indicator's [Enter] opens the Fleet panel (F2 also opens it; the process modal was retired). */
|
|
193
193
|
openFleetPanel: () => void;
|
|
194
194
|
requestRender: () => void;
|
|
195
195
|
};
|
|
@@ -337,7 +337,7 @@ export type KeyRouteState = {
|
|
|
337
337
|
commandRegistry?: CommandRegistry | null;
|
|
338
338
|
autocomplete: AutocompleteEngine | null;
|
|
339
339
|
blockActionsMenu: { open: (block: BlockMeta) => void };
|
|
340
|
-
/**
|
|
340
|
+
/** F2 opens+focuses the Fleet panel (which subsumes the retired process modal). */
|
|
341
341
|
openFleetPanel: () => void;
|
|
342
342
|
modalOpened: (name: string) => void;
|
|
343
343
|
saveUndoState: () => void;
|
|
@@ -186,7 +186,7 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
|
|
|
186
186
|
// focus to it. panelManager.open() only makes the panel active — focus is a
|
|
187
187
|
// separate axis (see PanelManager.focusPanels()/getFocusTarget()); without the
|
|
188
188
|
// focusPanels() call, j/k/i/K land silently in the composer until Tab. Used by
|
|
189
|
-
// the footer indicator's [Enter] and by F2 (
|
|
189
|
+
// the footer indicator's [Enter] and by F2 (F2 and the footer
|
|
190
190
|
// indicator both subsume the retired process modal). Mirrors the Ctrl+P
|
|
191
191
|
// panel-picker launcher (ui-openers.ts openPanelPicker).
|
|
192
192
|
const openFleetPanel = (): void => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Friction fix: the onboarding wizard's "Connect to
|
|
3
3
|
* this daemon now" action, wired from the Network step's `network.daemon-source`
|
|
4
4
|
* = 'adopt' fields. Extracted from handler-onboarding.ts to keep that file
|
|
5
5
|
* under the architecture line cap.
|
|
@@ -88,8 +88,8 @@ export async function handleConnectExistingDaemonForHandler(handler: InputHandle
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
|
-
*
|
|
92
|
-
* `goodvibes-daemon.service` unit (
|
|
91
|
+
* The guided UX entry point for migrating a legacy
|
|
92
|
+
* `goodvibes-daemon.service` unit (an earlier release shipped detect+disclose only; this
|
|
93
93
|
* closes that inheritance). This is a thin wrapper over
|
|
94
94
|
* `detectLegacyUnit` + `runLegacyDaemonMigration`
|
|
95
95
|
* (`../runtime/legacy-daemon-migration.ts`) — the actual migration mechanics
|
|
@@ -22,7 +22,7 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
|
|
|
22
22
|
|
|
23
23
|
if (token.type === 'key') {
|
|
24
24
|
if (token.logicalName === 'escape') {
|
|
25
|
-
//
|
|
25
|
+
// Search now starts focused by default (see
|
|
26
26
|
// ModelPickerModal.openAllModels() doc comment). Escape with an empty
|
|
27
27
|
// query used to only blurSearch() here, leaving the picker open and
|
|
28
28
|
// requiring a SECOND Escape to actually close it — confusing on a
|
|
@@ -133,7 +133,7 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
|
|
|
133
133
|
state.modelPicker.focusSearch();
|
|
134
134
|
} else if (state.modelPicker.searchFocused) {
|
|
135
135
|
// Symmetric with 'down' (below): search now starts focused by default
|
|
136
|
-
//
|
|
136
|
+
// so 'up' must have a way out of it too, or it would be a
|
|
137
137
|
// silent no-op the very first time a user reaches for the list.
|
|
138
138
|
state.modelPicker.blurSearch();
|
|
139
139
|
} else {
|
|
@@ -121,7 +121,7 @@ export function handleGlobalShortcutToken(
|
|
|
121
121
|
case 'panel-close': {
|
|
122
122
|
const pm = state.panelManager;
|
|
123
123
|
const active = pm.getActivePanel();
|
|
124
|
-
//
|
|
124
|
+
// Give the active panel a chance to consume Ctrl+X for an
|
|
125
125
|
// in-panel action (FleetPanel session-tab detach) before it closes the
|
|
126
126
|
// panel outright — see Panel.interceptPanelClose's doc comment.
|
|
127
127
|
if (active?.interceptPanelClose?.()) {
|
|
@@ -264,7 +264,7 @@ export class ModelPickerModal {
|
|
|
264
264
|
/**
|
|
265
265
|
* Open showing all models — entry point for /model.
|
|
266
266
|
*
|
|
267
|
-
*
|
|
267
|
+
* Search starts FOCUSED. A live tmux repro showed the actual
|
|
268
268
|
* friction wasn't the filter itself (it already works — fuzzy multi-word
|
|
269
269
|
* substring over id/displayName/provider) but that with search unfocused by
|
|
270
270
|
* default, typing a search term went character-by-character into single-key
|
|
@@ -295,7 +295,7 @@ export class ModelPickerModal {
|
|
|
295
295
|
this.scrollOffset = 0;
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
-
/** Open showing providers first — entry point for /provider.
|
|
298
|
+
/** Open showing providers first — entry point for /provider. Search starts focused — see openAllModels(). */
|
|
299
299
|
openProviders(providers: string[], currentProvider: string): void {
|
|
300
300
|
this.previousMode = null;
|
|
301
301
|
this.providers = providers;
|
|
@@ -86,13 +86,13 @@ export function buildOnboardingApplyRequest(controller: OnboardingWizardControll
|
|
|
86
86
|
setConfig('service.autostart', hasServers);
|
|
87
87
|
setConfig('service.restartOnFailure', true);
|
|
88
88
|
// One-Platform daemon-by-default (SDK docs/decisions/2026-07-05-daemon-by-default.md):
|
|
89
|
-
// the loopback-only cross-surface session daemon is safe-by-design
|
|
90
|
-
// loopback bind, auth-gated, rate-limited) and runs regardless of whether
|
|
89
|
+
// the loopback-only cross-surface session daemon is safe-by-design
|
|
90
|
+
// (loopback bind, auth-gated, rate-limited) and runs regardless of whether
|
|
91
91
|
// browser/LAN/webhook/external-app capabilities are selected here — it is no
|
|
92
92
|
// longer bundled with those network-exposing surfaces, so onboarding leaves
|
|
93
93
|
// daemon.enabled untouched and lets the SDK's own default-true (or an existing
|
|
94
94
|
// explicit user override) govern. (The deprecated danger.daemon alias this
|
|
95
|
-
// comment used to also name was removed
|
|
95
|
+
// comment used to also name was later removed.)
|
|
96
96
|
setConfig('controlPlane.enabled', hasServers);
|
|
97
97
|
setConfig('danger.httpListener', httpListener);
|
|
98
98
|
setConfig('web.enabled', browserAccess);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Friction fix: the Network step's daemon-source choice
|
|
3
3
|
* — start a new daemon owned by this TUI (default) vs connect to one that is
|
|
4
4
|
* already running elsewhere with a known token. Extracted from
|
|
5
5
|
* onboarding-wizard-steps.ts to keep that file under the architecture line cap.
|
|
@@ -78,7 +78,7 @@ export function pushDaemonAdoptionFields(
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
|
-
*
|
|
81
|
+
* The guided, visible entry point for migrating a detected legacy
|
|
82
82
|
* `goodvibes-daemon.service` unit — closes the fast-follow-up flagged when
|
|
83
83
|
* `handleMigrateLegacyDaemonServiceForHandler` (handler-onboarding-daemon-adopt.ts)
|
|
84
84
|
* shipped fully built but not wired to any onboarding control. Independent of
|
|
@@ -107,7 +107,7 @@ export function pushLegacyDaemonMigrationFields(
|
|
|
107
107
|
kind: 'status',
|
|
108
108
|
id: 'network.migrate-legacy-daemon-detected',
|
|
109
109
|
label: 'Legacy daemon service detected',
|
|
110
|
-
//
|
|
110
|
+
// Follow-up: name the unit this host actually resolves (carried on the
|
|
111
111
|
// snapshot from `resolveConfiguredServiceName` at collection time) — the
|
|
112
112
|
// MANAGED_SERVICE_NAME fallback only covers snapshots built without it.
|
|
113
113
|
hint: legacyUnitNote(legacy, legacy.trackedServiceName ?? MANAGED_SERVICE_NAME),
|
|
@@ -533,7 +533,7 @@ export function buildNetworkStep(controller: OnboardingWizardControllerLike): On
|
|
|
533
533
|
},
|
|
534
534
|
];
|
|
535
535
|
|
|
536
|
-
//
|
|
536
|
+
// Recognize an adoptable already-running daemon (see onboarding-wizard-network-adopt.ts).
|
|
537
537
|
const daemonSource = getDaemonSource(controller);
|
|
538
538
|
pushDaemonAdoptionFields(fields, controller, daemonSource, bindSettings);
|
|
539
539
|
pushLegacyDaemonMigrationFields(fields, controller);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* control-plane session, see below), tracks selected index, and handles
|
|
7
7
|
* load/delete actions.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* Union-sessions surface: additionally surfaces the cross-surface
|
|
10
10
|
* session union from `sessionBroker` (a SessionReadFacade — normally
|
|
11
11
|
* `uiServices.sessions.sessionBroker`, the SessionUnionCache) so a user can
|
|
12
12
|
* SEE what sessions are live/closed across every surface sharing this
|
|
@@ -156,7 +156,7 @@ export function buildSettingGroups(
|
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
//
|
|
159
|
+
// Inject the storage.codeIndexEnabled toggle into the
|
|
160
160
|
// storage category. TUI-local synthetic setting (not in the SDK ConfigKey
|
|
161
161
|
// union — see code-index-services.ts), same rationale as
|
|
162
162
|
// notifyAfterSeconds above: opt-in, default off, states its own bounds.
|
|
@@ -342,7 +342,7 @@ export function buildNotifyAlertSyntheticEntries(configManager: Pick<ConfigManag
|
|
|
342
342
|
}
|
|
343
343
|
|
|
344
344
|
// ---------------------------------------------------------------------------
|
|
345
|
-
//
|
|
345
|
+
// storage.codeIndexEnabled synthetic setting
|
|
346
346
|
// ---------------------------------------------------------------------------
|
|
347
347
|
|
|
348
348
|
/**
|
package/src/main.ts
CHANGED
|
@@ -202,7 +202,7 @@ async function main() {
|
|
|
202
202
|
|
|
203
203
|
const unsubs: Array<() => void> = [];
|
|
204
204
|
let recoveryInterval: ReturnType<typeof setInterval> | null = null;
|
|
205
|
-
let stopSpokenOutputForExit: (() => void) | null = null;
|
|
205
|
+
let stopSpokenOutputForExit: (() => Promise<void>) | null = null;
|
|
206
206
|
let recoveryPending = false;
|
|
207
207
|
// Which file the current recovery prompt should load/delete from (see recovery-input-helpers.ts).
|
|
208
208
|
let recoverySource: 'live' | 'preserved' = 'live';
|
|
@@ -240,7 +240,8 @@ async function main() {
|
|
|
240
240
|
events: uiServices.events,
|
|
241
241
|
notify: (message) => { systemMessageRouter.high(message); render(); },
|
|
242
242
|
});
|
|
243
|
-
|
|
243
|
+
// Exit-path stop: bounded drain of the audio already playing (see stopForExit).
|
|
244
|
+
stopSpokenOutputForExit = () => spokenTurns.stopForExit();
|
|
244
245
|
unsubs.push(...spokenTurns.unsubs);
|
|
245
246
|
unsubs.push(attachSpokenTurnModelRouting({
|
|
246
247
|
orchestrator,
|
|
@@ -497,7 +498,7 @@ async function main() {
|
|
|
497
498
|
lastInputTokens: orchestrator.lastInputTokens,
|
|
498
499
|
commandArgsHint,
|
|
499
500
|
hitlMode: modeManager.getHITLMode(),
|
|
500
|
-
//
|
|
501
|
+
// Cross-surface spine posture segment (adopted-daemon mode only).
|
|
501
502
|
sessionSpineStatus: (() => { const s = uiServices.platform.externalServices?.inspect(); return s?.sessionSpineActive && s.sessionSpineStatus && s.sessionSpineStatus !== 'unknown' ? s.sessionSpineStatus : undefined; })(), runningAgentCount, runningProcessCount,
|
|
502
503
|
// Composer must not read as focused while the panel/process indicator owns keyboard focus.
|
|
503
504
|
promptFocused: !input.panelFocused && !input.indicatorFocused,
|
|
@@ -646,7 +647,7 @@ async function main() {
|
|
|
646
647
|
const terminalOutputGuard = installTuiTerminalOutputGuard({ stdout, stderr: process.stderr, onCapture: (total) => { commandContext.session.runtime.terminalWritesIntercepted = total; render(); } });
|
|
647
648
|
|
|
648
649
|
setRenderRequest(renderNow); // bootstrap's 16ms coalescer calls the composite directly
|
|
649
|
-
setPanelFrameRequester(render); // live panels repaint when idle (
|
|
650
|
+
setPanelFrameRequester(render); // live panels repaint when idle (a replay finding: fleet sat stale until keypress)
|
|
650
651
|
orchestratorRefs.requestRender = render;
|
|
651
652
|
commandContext.renderRequest = render;
|
|
652
653
|
wireShellUiOpeners({
|
package/src/panels/base-panel.ts
CHANGED
|
@@ -14,7 +14,7 @@ const ERROR_FG = '#ef4444';
|
|
|
14
14
|
* markDirty() only sets a flag the compositor reads when a frame is ALREADY
|
|
15
15
|
* being composed for another reason (input, streaming) — so a live panel
|
|
16
16
|
* (fleet ticks, registry subscriptions) sat visibly stale while the app was
|
|
17
|
-
* idle until the next keypress (
|
|
17
|
+
* idle until the next keypress (a replay finding). The scheduler
|
|
18
18
|
* coalesces same-tick requests, so this stays cheap.
|
|
19
19
|
*/
|
|
20
20
|
let panelFrameRequester: (() => void) | null = null;
|
|
@@ -38,7 +38,7 @@ export function registerOperationsPanels(manager: PanelManager, deps: ResolvedBu
|
|
|
38
38
|
// process registry constructed once in runtime/services.ts (shared with
|
|
39
39
|
// every other consumer rather than duplicated here; the registry owns its
|
|
40
40
|
// own coalesced tick, so no manual lifecycle-event wiring is needed).
|
|
41
|
-
//
|
|
41
|
+
// runtimeBus is also passed so the read model can subscribe
|
|
42
42
|
// to the honest COMMUNICATION_CONSUMED steer-ack signal (fleet-read-model.ts).
|
|
43
43
|
const fleetReadModel = createFleetReadModel(ui.runtime.processRegistry, ui.runtime.runtimeBus);
|
|
44
44
|
|
|
@@ -51,16 +51,16 @@ export function registerOperationsPanels(manager: PanelManager, deps: ResolvedBu
|
|
|
51
51
|
description: 'Live unified process tree: agents, WRFC chains, workflows, watchers, and background processes, with interrupt/kill/steer controls',
|
|
52
52
|
factory: () => new FleetPanel(fleetReadModel, {
|
|
53
53
|
interrupt: (id: string) => fleetReadModel.interrupt(id),
|
|
54
|
-
//
|
|
54
|
+
// Re-arm a paused trigger/schedule/automation job.
|
|
55
55
|
resume: (id: string) => fleetReadModel.resume(id),
|
|
56
56
|
kill: (id: string, opts: { cascade: boolean }) => fleetReadModel.kill(id, opts),
|
|
57
|
-
//
|
|
57
|
+
// Full-fidelity transcript source for an attached agent
|
|
58
58
|
// tab — live while the agent runs, frozen briefly after it completes
|
|
59
59
|
// (AgentManager's bounded retention ring), empty once evicted (the
|
|
60
60
|
// panel degrades to the on-disk ledger fallback in that case).
|
|
61
61
|
getConversationSnapshot: (agentId: string) => ui.agents.agentManager.getConversationSnapshot(agentId),
|
|
62
62
|
resolveSessionLogPath: (agentId: string) => ui.environment.shellPaths.resolveProjectPath('tui', 'sessions', `${agentId}.jsonl`),
|
|
63
|
-
//
|
|
63
|
+
// Queue a message for a live in-process agent/wrfc-subtask member.
|
|
64
64
|
steer: (id: string, text: string) => fleetReadModel.steer(id, text),
|
|
65
65
|
}, deps.configManager),
|
|
66
66
|
});
|
|
@@ -86,7 +86,7 @@ export interface BuiltinPanelDeps {
|
|
|
86
86
|
/** Approval broker for control-plane/operator panels. */
|
|
87
87
|
approvalBroker?: ApprovalBroker;
|
|
88
88
|
/**
|
|
89
|
-
*
|
|
89
|
+
* Cross-surface session READ facade for control-plane/operator panels.
|
|
90
90
|
* Sync listSessions()/getSession() shape preserved; in adopted-daemon mode it
|
|
91
91
|
* serves the daemon-hosted union (with an honest offline note when the wire is
|
|
92
92
|
* down) instead of only this process's local broker. See session-union-cache.ts.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// fleet-panel-format.ts
|
|
3
3
|
//
|
|
4
4
|
// Pure column-layout + cell-formatting helpers for FleetPanel's tree rows.
|
|
5
|
-
// Extracted from fleet-panel.ts (
|
|
5
|
+
// Extracted from fleet-panel.ts (file-size hygiene) to hold that file under the
|
|
6
6
|
// 800-line architecture cap; these are stateless functions the panel's
|
|
7
7
|
// renderItem/renderDetail call, with no `this` dependency.
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
@@ -58,21 +58,21 @@ const C = DEFAULT_PANEL_PALETTE;
|
|
|
58
58
|
export interface FleetActionCallbacks {
|
|
59
59
|
/** Graceful interruption (AgentManager.cancel / trigger-schedule disable, via the registry). */
|
|
60
60
|
readonly interrupt: (id: string) => boolean;
|
|
61
|
-
/**
|
|
61
|
+
/** Re-arm a `paused` node (disabled trigger/schedule or automation job) — interrupt()'s inverse. Honest false when not paused / non-resumable. */
|
|
62
62
|
readonly resume: (id: string) => boolean;
|
|
63
63
|
/** Hard stop, optionally cascading to killable descendants. Returns the node ids acted on. */
|
|
64
64
|
readonly kill: (id: string, opts: { readonly cascade: boolean }) => readonly string[];
|
|
65
65
|
/**
|
|
66
|
-
*
|
|
66
|
+
* Fetch an agent's conversation history — live (running) or
|
|
67
67
|
* frozen (just-completed, still in the SDK's retention ring). Empty array
|
|
68
68
|
* means "unavailable" (evicted, or never registered) — FleetPanel degrades
|
|
69
69
|
* to the on-disk ledger fallback in that case, never fabricating content.
|
|
70
70
|
*/
|
|
71
71
|
readonly getConversationSnapshot: (agentId: string) => readonly ConversationMessageSnapshot[];
|
|
72
|
-
/**
|
|
72
|
+
/** Resolve the on-disk `<agentId>.jsonl` event-ledger path for the degraded fallback view. */
|
|
73
73
|
readonly resolveSessionLogPath: (agentId: string) => string;
|
|
74
74
|
/**
|
|
75
|
-
*
|
|
75
|
+
* Queue a human message for a live in-process agent (or a
|
|
76
76
|
* wrfc-subtask's current live member agent), delivered at the target's
|
|
77
77
|
* next turn boundary — never mid-token. Honest refusal
|
|
78
78
|
* (`{queued:false,reason}`) for anything that cannot take mid-run input
|
|
@@ -128,7 +128,7 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
|
|
|
128
128
|
this.reconcileSteerBadges();
|
|
129
129
|
this.markDirty();
|
|
130
130
|
});
|
|
131
|
-
//
|
|
131
|
+
// The honest "the agent actually consumed this steer at
|
|
132
132
|
// its turn boundary" signal — see fleet-tabs.ts's SteerBadgeStatus doc.
|
|
133
133
|
// A read-model without a runtimeBus dep never invokes this (graceful
|
|
134
134
|
// no-op), same degrade as steer()/steerable without a messageBus dep.
|
|
@@ -179,13 +179,13 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
|
|
|
179
179
|
for (const tab of this.tabsState.tabs) tab.lineCache.clear();
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
/** True while a session tab (not the root tree) is focused. Replaces the old
|
|
182
|
+
/** True while a session tab (not the root tree) is focused. Replaces the old isDetailFocused() seam. */
|
|
183
183
|
public isTabActive(): boolean {
|
|
184
184
|
return this.tabsState.activeTabIndex > 0;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
/**
|
|
188
|
-
*
|
|
188
|
+
* True while the steer composer on the active tab is open —
|
|
189
189
|
* every character of a burst (paste, or several fast-typed chars in one
|
|
190
190
|
* stdin chunk) must land in the draft one at a time rather than being
|
|
191
191
|
* routed away as panel hotkeys or exploded to the main composer. See
|
|
@@ -207,7 +207,7 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
/**
|
|
210
|
-
* Ctrl+X (panel-close) collision resolution
|
|
210
|
+
* Ctrl+X (panel-close) collision resolution: the global
|
|
211
211
|
* shortcut route (handler-shortcuts.ts) calls this BEFORE closing the
|
|
212
212
|
* panel. Detaching the active tab consumes the key; on the root tree tab
|
|
213
213
|
* there is nothing to detach, so this returns false and Ctrl+X falls
|
|
@@ -317,7 +317,7 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
|
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
/**
|
|
320
|
-
*
|
|
320
|
+
* The "dropped inference" reconciliation: called on every read-model
|
|
321
321
|
* snapshot update and on the panel's 1s tick. See fleet-steer.ts's
|
|
322
322
|
* reconcileSteerBadges doc for why this is needed (the SDK emits no
|
|
323
323
|
* cancelled/expired signal for a queued steer).
|
|
@@ -350,7 +350,7 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
|
|
|
350
350
|
// Confirm overlay owns input first (K-armed kill confirm).
|
|
351
351
|
if (this.confirmOverlay.handleInput(key)) return true;
|
|
352
352
|
|
|
353
|
-
//
|
|
353
|
+
// Once the steer composer is open on the active tab, it
|
|
354
354
|
// owns EVERY key (same full-priority-while-composing contract as
|
|
355
355
|
// git-panel.ts's commitMessage entry) until Enter (submit) or Esc
|
|
356
356
|
// (cancel) — a burst/paste and single hotkeys like 'j'/'s'/'[' all land
|
|
@@ -458,7 +458,7 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
|
|
|
458
458
|
}
|
|
459
459
|
|
|
460
460
|
if (key === 's') {
|
|
461
|
-
//
|
|
461
|
+
// An earlier replay fix: 's' from the TREE was silently dead — steering
|
|
462
462
|
// required an undiscoverable Enter-attach first. Attach-and-steer in
|
|
463
463
|
// one press for a steerable node; honest refusal otherwise.
|
|
464
464
|
if (!selected) return false;
|
|
@@ -485,7 +485,7 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
|
|
|
485
485
|
}
|
|
486
486
|
|
|
487
487
|
/**
|
|
488
|
-
*
|
|
488
|
+
* The `s` gate — mirrors the i/K capability-gating template
|
|
489
489
|
* exactly (guard on the live node existing, non-terminal, and the specific
|
|
490
490
|
* capability; consume the key and say so rather than silently no-op-ing).
|
|
491
491
|
*/
|
|
@@ -649,13 +649,13 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
|
|
|
649
649
|
|
|
650
650
|
const snapshotRows = this.readModel.getSnapshot().rows;
|
|
651
651
|
const liveNode = snapshotRows.find((row) => row.node.id === tab.nodeId)?.node ?? null;
|
|
652
|
-
//
|
|
652
|
+
// The steer composer only ever applies to 'agent' tabs
|
|
653
653
|
// (wrfc-chain has no conversation loop of its own — see
|
|
654
654
|
// fleet-tabs.ts's FleetAttachableKind doc); the live-node lookup is
|
|
655
655
|
// shared with the isTerminal computation below either way.
|
|
656
656
|
const canSteer = liveNode !== null && !isTerminalProcessState(liveNode.state) && liveNode.capabilities.steerable;
|
|
657
657
|
|
|
658
|
-
//
|
|
658
|
+
// While composing, Enter/Esc replace the tab-switch/detach
|
|
659
659
|
// hints (mirrors git-panel.ts's renderCommitCompose footer) so the
|
|
660
660
|
// footer never advertises a key the composer itself has absorbed.
|
|
661
661
|
const hints: Array<{ keys: string; label: string }> = tab.steerDraft !== null
|
|
@@ -739,7 +739,7 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
|
|
|
739
739
|
const hints = buildFleetTreeHints(selected?.node, this.follow, this.tabsState.tabs.length > 0);
|
|
740
740
|
|
|
741
741
|
// Tab strip renders only when tabs exist — omitting it entirely with no
|
|
742
|
-
// tabs attached keeps the pre-
|
|
742
|
+
// tabs attached keeps the pre-session-tab root-tree rendering byte-identical.
|
|
743
743
|
const stripLine = renderFleetTabStrip(this.tabsState, width);
|
|
744
744
|
const header = stripLine ? [stripLine] : undefined;
|
|
745
745
|
|
|
@@ -71,7 +71,7 @@ export interface FleetSnapshot {
|
|
|
71
71
|
|
|
72
72
|
export type FleetStateTone = 'active' | 'success' | 'failure' | 'warn' | 'muted';
|
|
73
73
|
|
|
74
|
-
//
|
|
74
|
+
// Design ruling: this is a DIFFERENT table from the SDK presentation
|
|
75
75
|
// contract's STATE_GLYPHS (@pellux/goodvibes-sdk/platform/presentation) — that
|
|
76
76
|
// one is a 4-state semantic alias (good/warn/bad/info) shared by the TUI and
|
|
77
77
|
// agent renderers; this one is a 12-state ProcessNode taxonomy specific to the
|
|
@@ -90,16 +90,16 @@ const STATE_GLYPHS: Record<ProcessState, string> = {
|
|
|
90
90
|
done: '✓',
|
|
91
91
|
failed: '✗',
|
|
92
92
|
killed: '⊘',
|
|
93
|
-
//
|
|
93
|
+
// A distinct TERMINAL outcome from 'killed' —
|
|
94
94
|
// both come from AgentManager.cancel(), but a graceful interrupt request
|
|
95
95
|
// ('the operator asked nicely') is display-distinguishable from a hard
|
|
96
96
|
// kill. '◌' verified free against every other glyph in this table.
|
|
97
97
|
interrupted: '◌',
|
|
98
98
|
idle: '·',
|
|
99
99
|
queued: '…',
|
|
100
|
-
//
|
|
100
|
+
// SDK behavior: schedules/triggers/automation jobs report 'paused' when
|
|
101
101
|
// disabled (previously mislabeled 'killed'). NOT terminal — resumable via
|
|
102
|
-
// ProcessRegistry.resume() (
|
|
102
|
+
// ProcessRegistry.resume() (the full pause/resume UI lives in
|
|
103
103
|
// fleet-stop.ts). '❚' verified free against every other glyph in this table.
|
|
104
104
|
paused: '❚',
|
|
105
105
|
};
|
|
@@ -140,11 +140,11 @@ const KIND_TAGS: Record<ProcessKind, string> = {
|
|
|
140
140
|
schedule: 'sched',
|
|
141
141
|
watcher: 'watch',
|
|
142
142
|
'background-process': 'exec',
|
|
143
|
-
//
|
|
143
|
+
// Orchestration-engine kinds (adapters/orchestration.ts).
|
|
144
144
|
workstream: 'stream',
|
|
145
145
|
phase: 'phase',
|
|
146
146
|
'work-item': 'item',
|
|
147
|
-
//
|
|
147
|
+
// The repo source-tree code index (adapters/code-index.ts).
|
|
148
148
|
// A single leaf node — never a rollup of other flat-list nodes (see
|
|
149
149
|
// ROLLUP_KINDS below) — so no other list in this module needs updating.
|
|
150
150
|
'code-index': 'index',
|
|
@@ -288,7 +288,7 @@ export function buildFleetRows(nodes: readonly ProcessNode[]): FleetTreeRow[] {
|
|
|
288
288
|
* nonzero cost/token reading (defense in depth against a future adapter
|
|
289
289
|
* change, not load-bearing).
|
|
290
290
|
*
|
|
291
|
-
*
|
|
291
|
+
* Orchestration-engine additions — verified against adapters/orchestration.ts:
|
|
292
292
|
* - 'workstream': adaptWorkstream SUMS every work-item's usage/costUsd
|
|
293
293
|
* exactly once (sumWorkItemUsage/aggregateWorkItemCost), the same
|
|
294
294
|
* "rollup of nodes that also appear individually" shape as adaptChain —
|
|
@@ -308,7 +308,7 @@ export function buildFleetRows(nodes: readonly ProcessNode[]): FleetTreeRow[] {
|
|
|
308
308
|
* 'wrfc-subtask'-for-capabilities analogue but the 'agent'-for-usage
|
|
309
309
|
* analogue. Excluding it would silently zero out real cost/token totals.
|
|
310
310
|
*
|
|
311
|
-
*
|
|
311
|
+
* 'code-index' is deliberately NOT in this set either —
|
|
312
312
|
* adaptCodeIndex yields exactly one standalone ProcessNode per registry
|
|
313
313
|
* (never a parent whose children ALSO appear individually in the flat
|
|
314
314
|
* list), so it is a leaf like 'agent'/'work-item', not a grouping construct.
|
|
@@ -435,7 +435,7 @@ export interface FleetReadModel {
|
|
|
435
435
|
/** Graceful interruption where the source supports one. Returns true when accepted. */
|
|
436
436
|
interrupt(id: string): boolean;
|
|
437
437
|
/**
|
|
438
|
-
*
|
|
438
|
+
* Re-arm a `paused` node (a disabled trigger/schedule,
|
|
439
439
|
* or an automation job). The inverse of interrupt()'s pause. Returns true when
|
|
440
440
|
* accepted; false for a node that is not currently `paused` or whose kind has
|
|
441
441
|
* no resume path (honest refusal).
|
|
@@ -444,14 +444,14 @@ export interface FleetReadModel {
|
|
|
444
444
|
/** Hard stop, optionally cascading to descendants. Returns the node ids acted on. */
|
|
445
445
|
kill(id: string, opts?: { readonly cascade?: boolean }): readonly string[];
|
|
446
446
|
/**
|
|
447
|
-
*
|
|
447
|
+
* Queue a human message for a live in-process agent (or a
|
|
448
448
|
* wrfc-subtask's current live member), delivered at its next turn
|
|
449
449
|
* boundary. Honest refusal (`{queued:false,reason}`) for anything that
|
|
450
450
|
* cannot take mid-run input — see ProcessRegistry.steer's doc comment.
|
|
451
451
|
*/
|
|
452
452
|
steer(id: string, text: string): SteerResult;
|
|
453
453
|
/**
|
|
454
|
-
*
|
|
454
|
+
* Subscribe to the honest "the agent actually consumed this
|
|
455
455
|
* steer at its turn boundary" signal (COMMUNICATION_CONSUMED on the
|
|
456
456
|
* runtime bus's 'communication' domain). Returns an unsubscribe function.
|
|
457
457
|
* A read-model constructed without a runtimeBus (e.g. the static factory,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
2
|
// fleet-steer.ts
|
|
3
3
|
//
|
|
4
|
-
//
|
|
4
|
+
// Steer-badge rendering helpers and the "dropped inference"
|
|
5
5
|
// reconciliation pass, split out of fleet-panel.ts to keep that file under
|
|
6
6
|
// the architecture line cap (see check-architecture.ts's 800-line gate).
|
|
7
7
|
// Pure functions only; FleetPanel still owns the mutable FleetTab.steerBadge
|
|
@@ -67,7 +67,7 @@ export function renderSteerBadgeLine(badge: SteerBadge, width: number, palette:
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
|
-
*
|
|
70
|
+
* The "dropped inference" risk: the SDK emits no
|
|
71
71
|
* cancelled/expired signal for a queued steer, so a badge left `queued`
|
|
72
72
|
* after its target node goes terminal (done/failed/killed/interrupted)
|
|
73
73
|
* would hang forever with no honest resolution. Resolves any such badge to
|
package/src/panels/fleet-stop.ts
CHANGED
|
@@ -115,7 +115,7 @@ export function buildFleetTreeHints(
|
|
|
115
115
|
{ keys: 'j/k', label: 'navigate' },
|
|
116
116
|
{ keys: 'Enter', label: 'attach' },
|
|
117
117
|
];
|
|
118
|
-
if (live && selected.capabilities.steerable) hints.push({ keys: 's', label: 'steer' }); //
|
|
118
|
+
if (live && selected.capabilities.steerable) hints.push({ keys: 's', label: 'steer' }); // discoverable from the tree (attach-and-steer)
|
|
119
119
|
if (live && selected.capabilities.interruptible) hints.push({ keys: 'i', label: 'interrupt' });
|
|
120
120
|
if (live && selected.capabilities.killable) hints.push({ keys: 'K', label: 'kill' });
|
|
121
121
|
if (live && !isPaused && selected.capabilities.pausable) hints.push({ keys: 'p', label: 'pause' });
|
|
@@ -125,7 +125,7 @@ export function buildFleetTreeHints(
|
|
|
125
125
|
return hints;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
/** K-confirm descendant stats (UX-C item 6): total = every non-terminal descendant (what a cascade kill takes down); active = the individually-killable subset. Was "active only"
|
|
128
|
+
/** K-confirm descendant stats (UX-C item 6): total = every non-terminal descendant (what a cascade kill takes down); active = the individually-killable subset. Was "active only" in an earlier version, undercounting a mixed subtree. */
|
|
129
129
|
export function countDescendantStats(rows: readonly FleetTreeRow[], nodeId: string): { total: number; active: number } {
|
|
130
130
|
const byParent = new Map<string, string[]>();
|
|
131
131
|
for (const row of rows) {
|
package/src/panels/fleet-tabs.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
2
|
// fleet-tabs.ts
|
|
3
3
|
//
|
|
4
|
-
//
|
|
4
|
+
// Pure, testable tab-state model for FleetPanel's
|
|
5
5
|
// session tabs. No BasePanel/rendering dependency, mirroring the
|
|
6
6
|
// fleet-read-model.ts convention: this module owns attach/detach/switch as
|
|
7
7
|
// pure state transitions over a small immutable FleetTabsState, so the tab
|
|
@@ -32,7 +32,7 @@ export function isAttachableFleetKind(kind: ProcessKind): kind is FleetAttachabl
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
*
|
|
35
|
+
* Lifecycle of a queued steer message, tracked
|
|
36
36
|
* per-tab. `queued` means `ProcessRegistry.steer()` accepted the message
|
|
37
37
|
* onto the target's inbox, NOT that the agent has seen it — that is the
|
|
38
38
|
* later, honest `consumed` transition (a `COMMUNICATION_CONSUMED` runtime-bus
|
|
@@ -119,14 +119,14 @@ export interface FleetTab {
|
|
|
119
119
|
ledgerEntries: Record<string, unknown>[] | null;
|
|
120
120
|
ledgerLoadStarted: boolean;
|
|
121
121
|
/**
|
|
122
|
-
*
|
|
122
|
+
* The one-line steer composer's in-progress text, or `null`
|
|
123
123
|
* when not composing. Mirrors git-panel.ts's `commitMessage` mutable-slot
|
|
124
124
|
* convention (FleetPanel.isCapturingTextBurst() gates on this being
|
|
125
125
|
* non-null so a burst/paste lands here char-by-char, never as tree/tab
|
|
126
126
|
* hotkeys — see FleetPanel.handleSteerInput).
|
|
127
127
|
*/
|
|
128
128
|
steerDraft: string | null;
|
|
129
|
-
/**
|
|
129
|
+
/** This tab's most recent steer message's lifecycle, or null. */
|
|
130
130
|
steerBadge: SteerBadge | null;
|
|
131
131
|
}
|
|
132
132
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
2
|
// fleet-transcript.ts
|
|
3
3
|
//
|
|
4
|
-
//
|
|
4
|
+
// Renders the content of an attached FleetPanel
|
|
5
5
|
// session tab. Three sources, chosen by what the SDK can actually provide
|
|
6
|
-
// (see the
|
|
6
|
+
// (see the design brief's "central reality check" — a full-fidelity live
|
|
7
7
|
// transcript needs a ConversationMessageSnapshot[] history source, which
|
|
8
8
|
// only exists for a RUNNING agent or a just-completed one still inside the
|
|
9
9
|
// SDK's bounded retention ring):
|
|
@@ -163,8 +163,8 @@ export function renderFleetAgentTranscript(
|
|
|
163
163
|
/**
|
|
164
164
|
* Render a live one-line-per-member summary for an attached wrfc-chain tab.
|
|
165
165
|
*
|
|
166
|
-
* `chainDoneOrAbsent` disambiguates an empty member list
|
|
167
|
-
* chain prunes its wrapper node (
|
|
166
|
+
* `chainDoneOrAbsent` disambiguates an empty member list: a completed
|
|
167
|
+
* chain prunes its wrapper node (zombie reap), so zero members can mean the
|
|
168
168
|
* chain FINISHED, not that it has not started. When the chain node is absent
|
|
169
169
|
* (pruned) or terminal, say so honestly instead of the "(no member agents yet)"
|
|
170
170
|
* not-started wording.
|
|
@@ -284,7 +284,7 @@ function renderLedgerEntry(width: number, entry: Record<string, unknown>, palett
|
|
|
284
284
|
{ text: `${status}${formatLedgerDuration(entry['durationMs'])}`, fg: tone },
|
|
285
285
|
]);
|
|
286
286
|
}
|
|
287
|
-
//
|
|
287
|
+
// Per-turn passive knowledge injection
|
|
288
288
|
// records, appended to this same JSONL ledger as
|
|
289
289
|
// `{type:'knowledge_injection', turn, ...record}` (orchestrator-runner.ts).
|
|
290
290
|
// Without this case these fell through to the generic 'event' default
|
package/src/panels/types.ts
CHANGED
|
@@ -142,7 +142,7 @@ export interface Panel {
|
|
|
142
142
|
/**
|
|
143
143
|
* Optional: called by the global Ctrl+X (`panel-close`) shortcut BEFORE it
|
|
144
144
|
* closes this panel (handler-shortcuts.ts). Return `true` to consume
|
|
145
|
-
* Ctrl+X for an in-panel action instead — e.g. FleetPanel (
|
|
145
|
+
* Ctrl+X for an in-panel action instead — e.g. FleetPanel (session
|
|
146
146
|
* tabs) detaches its active tab and leaves the panel open. Return `false`
|
|
147
147
|
* (or omit this hook) to fall through to the ordinary close behavior.
|
|
148
148
|
* Ctrl+X never reaches a panel's own `handleInput` (ctrl/meta combos are
|