@pellux/goodvibes-tui 1.9.1 → 1.10.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 +36 -5
- package/README.md +1 -1
- package/docs/foundation-artifacts/knowledge-graphql.graphql +5 -0
- package/docs/foundation-artifacts/knowledge-store.sql +33 -0
- package/docs/foundation-artifacts/operator-contract.json +4026 -1376
- package/package.json +2 -2
- package/src/audio/player.ts +91 -5
- package/src/audio/spoken-turn-wiring.ts +10 -2
- package/src/cli/management-utils.ts +4 -0
- package/src/config/credential-availability.ts +1 -1
- package/src/config/index.ts +1 -1
- 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/export/cost-utils.ts +9 -1
- package/src/input/command-registry.ts +16 -5
- package/src/input/commands/config.ts +1 -1
- package/src/input/commands/control-room-runtime.ts +12 -5
- package/src/input/commands/incident-runtime.ts +2 -2
- package/src/input/commands/memory.ts +12 -12
- package/src/input/commands/planning-runtime.ts +1 -1
- package/src/input/commands/recall-bundle.ts +8 -8
- package/src/input/commands/recall-capture.ts +3 -3
- package/src/input/commands/recall-query.ts +41 -15
- package/src/input/commands/recall-review.ts +12 -12
- 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 +56 -2
- package/src/main.ts +8 -6
- package/src/panels/base-panel.ts +1 -1
- package/src/panels/builtin/operations.ts +4 -4
- package/src/panels/builtin/shared.ts +4 -4
- package/src/panels/cost-tracker-panel.ts +2 -2
- 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/modals/memory-modal.ts +77 -16
- 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-command-context.ts +4 -0
- package/src/runtime/bootstrap-command-parts.ts +4 -1
- package/src/runtime/bootstrap-core.ts +3 -3
- package/src/runtime/bootstrap-hook-bridge.ts +2 -2
- package/src/runtime/bootstrap-shell.ts +18 -7
- package/src/runtime/bootstrap.ts +22 -22
- 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 +1 -1
- package/src/runtime/memory-spine-transport.ts +289 -0
- 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 +29 -4
- package/src/runtime/process-lifecycle.ts +19 -5
- package/src/runtime/services.ts +27 -28
- 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
- package/src/audio/spoken-turn-controller.ts +0 -243
- package/src/audio/text-chunker.ts +0 -110
|
@@ -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
|
});
|
|
@@ -3,7 +3,7 @@ import type { ServiceRegistry } from '@pellux/goodvibes-sdk/platform/config';
|
|
|
3
3
|
import type { ToolRegistry } from '@pellux/goodvibes-sdk/platform/tools';
|
|
4
4
|
import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
|
|
5
5
|
import type { Orchestrator } from '../../core/orchestrator';
|
|
6
|
-
import type {
|
|
6
|
+
import type { MemoryAccess } from '@pellux/goodvibes-sdk/platform/runtime/memory-spine';
|
|
7
7
|
import type { ApprovalBroker } from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
8
8
|
import type { SessionReadFacade } from '@pellux/goodvibes-sdk/platform/runtime/session-spine';
|
|
9
9
|
import type { AutomationManager } from '@pellux/goodvibes-sdk/platform/automation';
|
|
@@ -79,14 +79,14 @@ export interface BuiltinPanelDeps {
|
|
|
79
79
|
* copy via CommandContext.extensions.evalRegistry).
|
|
80
80
|
*/
|
|
81
81
|
evalRegistry?: import('../eval-registry.ts').EvalRegistry;
|
|
82
|
-
/**
|
|
83
|
-
memoryRegistry?:
|
|
82
|
+
/** Host-vs-client memory access for the Memory modal — the spine client, never the raw registry (routes over the wire when a daemon is adopted). */
|
|
83
|
+
memoryRegistry?: MemoryAccess;
|
|
84
84
|
/** Shared policy runtime state for governance/policy diagnostics. */
|
|
85
85
|
policyRuntimeState?: import('@/runtime/index.ts').PolicyRuntimeState;
|
|
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.
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
DEFAULT_PANEL_PALETTE,
|
|
23
23
|
type PanelWorkspaceSection,
|
|
24
24
|
} from './polish.ts';
|
|
25
|
-
import { calcSessionCost, isModelPriced, computeBudgetBreach, readBudgetAlertUsd, type BudgetAlertConfigAccess } from '../export/cost-utils.ts';
|
|
25
|
+
import { calcSessionCost, isModelPriced, computeBudgetBreach, readBudgetAlertUsd, BUDGET_ALERT_USD_CONFIG_KEY, type BudgetAlertConfigAccess } from '../export/cost-utils.ts';
|
|
26
26
|
import { abbreviateCount } from '../utils/format-number.ts';
|
|
27
27
|
import { isTextBackspace } from '../input/delete-key-policy.ts';
|
|
28
28
|
|
|
@@ -332,7 +332,7 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
332
332
|
public setBudgetThreshold(usd: number): void {
|
|
333
333
|
if (!Number.isFinite(usd) || usd < 0) return;
|
|
334
334
|
this.budgetThreshold = usd;
|
|
335
|
-
this.configAccess?.set(
|
|
335
|
+
this.configAccess?.set(BUDGET_ALERT_USD_CONFIG_KEY, usd);
|
|
336
336
|
this.markDirty();
|
|
337
337
|
}
|
|
338
338
|
|
|
@@ -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
|
|
@@ -17,6 +17,16 @@ import type {
|
|
|
17
17
|
// panel's selected-record scope/class/tags/provenance detail is folded into
|
|
18
18
|
// each row label. `memoryRegistry` absent → the retired MemoryPanel's
|
|
19
19
|
// "not configured" copy renders as an honest degraded state.
|
|
20
|
+
//
|
|
21
|
+
// Read path (memory-spine adoption): reads go through the spine client's
|
|
22
|
+
// `honestSearch` — the MemoryAccess shape — not the raw registry, so a session
|
|
23
|
+
// that has adopted an external daemon reads the SAME wire-served records a
|
|
24
|
+
// wire failure is deliberately surfaced (never a silently stale local copy).
|
|
25
|
+
// `buildView()` stays synchronous/pure per the ConfigModalSurface contract;
|
|
26
|
+
// `refresh()` is async and calls the `requestRender` callback `onOpen` hands
|
|
27
|
+
// it when the data lands. The Review Queue ranking is recomputed client-side
|
|
28
|
+
// from the same honestSearch batch (see `rankForReview` below) rather than
|
|
29
|
+
// through a second wire call — `reviewQueue` is not part of MemoryAccess.
|
|
20
30
|
// ---------------------------------------------------------------------------
|
|
21
31
|
|
|
22
32
|
/** Minimal read shape of a `MemoryRecord` this modal renders. */
|
|
@@ -33,16 +43,42 @@ interface MemoryRecordLike {
|
|
|
33
43
|
readonly reviewedAt?: number | undefined;
|
|
34
44
|
readonly reviewedBy?: string | undefined;
|
|
35
45
|
readonly createdAt: number;
|
|
46
|
+
readonly updatedAt?: number | undefined;
|
|
36
47
|
readonly provenance: readonly { readonly kind: string; readonly ref: string }[];
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
export interface MemoryModalDeps {
|
|
40
51
|
readonly memoryRegistry?: {
|
|
41
|
-
|
|
42
|
-
|
|
52
|
+
honestSearch(filter?: { limit?: number }): Promise<{
|
|
53
|
+
readonly records: readonly MemoryRecordLike[];
|
|
54
|
+
readonly indexUnavailableReason?: string | null | undefined;
|
|
55
|
+
}>;
|
|
43
56
|
};
|
|
44
57
|
}
|
|
45
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Client-side port of the SDK's MemoryStore.reviewQueue ranking
|
|
61
|
+
* (memory-store-helpers.ts reviewQueueScore/isReviewCandidate): all four
|
|
62
|
+
* review states are candidates, scored by state + inverse confidence (flagged
|
|
63
|
+
* states penalized), tie-broken by recency. Presentation ordering only — not
|
|
64
|
+
* a wire call, so exact parity with the server's own tie-breaking on ids it
|
|
65
|
+
* has never seen is not load-bearing.
|
|
66
|
+
*/
|
|
67
|
+
function rankForReview(records: readonly MemoryRecordLike[], limit: number): MemoryRecordLike[] {
|
|
68
|
+
const score = (r: MemoryRecordLike): number => {
|
|
69
|
+
let s = 0;
|
|
70
|
+
if (r.reviewState === 'fresh') s += 40;
|
|
71
|
+
if (r.reviewState === 'stale') s += 20;
|
|
72
|
+
if (r.reviewState === 'contradicted') s += 10;
|
|
73
|
+
s += Math.max(0, 100 - r.confidence);
|
|
74
|
+
if (r.reviewState === 'stale' || r.reviewState === 'contradicted') s -= 20;
|
|
75
|
+
return s;
|
|
76
|
+
};
|
|
77
|
+
return [...records]
|
|
78
|
+
.sort((a, b) => score(b) - score(a) || (b.updatedAt ?? b.createdAt) - (a.updatedAt ?? a.createdAt) || b.createdAt - a.createdAt)
|
|
79
|
+
.slice(0, limit);
|
|
80
|
+
}
|
|
81
|
+
|
|
46
82
|
// Mirrors registerKnowledgePanels's withUnconfiguredFallback copy for the
|
|
47
83
|
// retired MemoryPanel (src/panels/builtin/knowledge.ts) verbatim.
|
|
48
84
|
const NOT_CONFIGURED_TITLE = 'Memory registry not configured for this session.';
|
|
@@ -57,6 +93,9 @@ class MemoryModalSurface implements ConfigModalSurface {
|
|
|
57
93
|
readonly title = 'Memory';
|
|
58
94
|
private allRecords: MemoryRecordLike[] = [];
|
|
59
95
|
private reviewRecords: MemoryRecordLike[] = [];
|
|
96
|
+
/** Honest note on the last read: a wire failure (client mode) or the index-unavailable fallback reason — never silently dropped. */
|
|
97
|
+
private loadNote: string | null = null;
|
|
98
|
+
private requestRender: (() => void) | null = null;
|
|
60
99
|
|
|
61
100
|
constructor(private readonly deps: MemoryModalDeps) {}
|
|
62
101
|
|
|
@@ -71,12 +110,27 @@ class MemoryModalSurface implements ConfigModalSurface {
|
|
|
71
110
|
{ key: 'r', id: 'refresh', label: 'refresh' },
|
|
72
111
|
];
|
|
73
112
|
|
|
74
|
-
onOpen(): void {
|
|
113
|
+
onOpen(requestRender: () => void): void {
|
|
114
|
+
this.requestRender = requestRender;
|
|
115
|
+
void this.refresh();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
onClose(): void {
|
|
119
|
+
this.requestRender = null;
|
|
120
|
+
}
|
|
75
121
|
|
|
76
|
-
private refresh(): void {
|
|
77
|
-
if (!this.deps.memoryRegistry) { this.allRecords = []; this.reviewRecords = []; return; }
|
|
78
|
-
|
|
79
|
-
|
|
122
|
+
private async refresh(): Promise<void> {
|
|
123
|
+
if (!this.deps.memoryRegistry) { this.allRecords = []; this.reviewRecords = []; this.loadNote = null; return; }
|
|
124
|
+
try {
|
|
125
|
+
const result = await this.deps.memoryRegistry.honestSearch({ limit: 100 });
|
|
126
|
+
this.allRecords = [...result.records];
|
|
127
|
+
this.reviewRecords = rankForReview(result.records, 24);
|
|
128
|
+
this.loadNote = result.indexUnavailableReason ?? null;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
// Client-mode wire failure: surfaced plainly, never masked by the last-known list.
|
|
131
|
+
this.loadNote = `Failed to reach memory over the wire: ${error instanceof Error ? error.message : String(error)}`;
|
|
132
|
+
}
|
|
133
|
+
this.requestRender?.();
|
|
80
134
|
}
|
|
81
135
|
|
|
82
136
|
private recordFrom(id: string): MemoryRecordLike | undefined {
|
|
@@ -121,11 +175,15 @@ class MemoryModalSurface implements ConfigModalSurface {
|
|
|
121
175
|
tabs: [{ id: 'all', label: 'All Records', rows: [] }],
|
|
122
176
|
};
|
|
123
177
|
}
|
|
124
|
-
return {
|
|
178
|
+
return {
|
|
179
|
+
title: 'Memory',
|
|
180
|
+
...(this.loadNote ? { degraded: this.loadNote } : {}),
|
|
181
|
+
tabs: [this.allTab(), this.reviewTab()],
|
|
182
|
+
};
|
|
125
183
|
}
|
|
126
184
|
|
|
127
185
|
onAction(id: string, ctx: ConfigModalActionContext): void {
|
|
128
|
-
if (id === 'refresh') { this.refresh(); ctx.setStatus('
|
|
186
|
+
if (id === 'refresh') { void this.refresh(); ctx.setStatus('Reloading memory records...'); return; }
|
|
129
187
|
const record = ctx.row ? this.recordFrom(ctx.row.id) : undefined;
|
|
130
188
|
if (!record) return;
|
|
131
189
|
const review = (state: string, confidence: number, reason?: string): void => {
|
|
@@ -150,9 +208,12 @@ export function createMemoryModalSurface(deps: MemoryModalDeps): ConfigModalSurf
|
|
|
150
208
|
|
|
151
209
|
/**
|
|
152
210
|
* Deterministic golden fixture: fixed memory records with frozen createdAt
|
|
153
|
-
* timestamps — no live registry, no wall-clock, no random ids.
|
|
211
|
+
* timestamps — no live registry, no wall-clock, no random ids. Promise-backed
|
|
212
|
+
* (see ecosystem-modals-golden.test.ts): `honestSearch` is async even for this
|
|
213
|
+
* in-memory fixture (matching the real MemoryAccess shape), so the factory
|
|
214
|
+
* pre-awaits the initial `onOpen` refresh before handing back the surface.
|
|
154
215
|
*/
|
|
155
|
-
export function memoryModalGoldenSurface(): ConfigModalSurface {
|
|
216
|
+
export async function memoryModalGoldenSurface(): Promise<ConfigModalSurface> {
|
|
156
217
|
const FIXED_CREATED_AT = 1735689600000; // 2025-01-01T00:00:00.000Z
|
|
157
218
|
const records: readonly MemoryRecordLike[] = [
|
|
158
219
|
{
|
|
@@ -171,10 +232,10 @@ export function memoryModalGoldenSurface(): ConfigModalSurface {
|
|
|
171
232
|
createdAt: FIXED_CREATED_AT + 86400000, provenance: [],
|
|
172
233
|
},
|
|
173
234
|
];
|
|
174
|
-
|
|
175
|
-
memoryRegistry: {
|
|
176
|
-
search: () => records,
|
|
177
|
-
reviewQueue: () => records.filter((record) => record.reviewState !== 'reviewed'),
|
|
178
|
-
},
|
|
235
|
+
const surface = createMemoryModalSurface({
|
|
236
|
+
memoryRegistry: { honestSearch: async () => ({ records }) },
|
|
179
237
|
});
|
|
238
|
+
surface.onOpen?.(() => {});
|
|
239
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
240
|
+
return surface;
|
|
180
241
|
}
|
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
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
2
|
// fleet-tab-strip.ts
|
|
3
3
|
//
|
|
4
|
-
//
|
|
4
|
+
// Renders FleetPanel's session-tab strip by reusing
|
|
5
5
|
// renderTabStrip exactly as panel-workspace-bar.ts does for the workspace
|
|
6
6
|
// tab bar. Deliberately a SEPARATE, visually distinct strip from the
|
|
7
7
|
// workspace bar: workspace tabs switch PANELS, fleet session tabs switch
|
|
@@ -23,7 +23,7 @@ const LABEL_FG = UI_TONES.fg.secondary;
|
|
|
23
23
|
/**
|
|
24
24
|
* Render the fleet session-tab strip, or `null` when there are no attached
|
|
25
25
|
* tabs — the panel omits the strip entirely in that case (root-tab-only),
|
|
26
|
-
* which is what keeps the pre-
|
|
26
|
+
* which is what keeps the pre-session-tab fleet-panel goldens byte-identical.
|
|
27
27
|
*/
|
|
28
28
|
export function renderFleetTabStrip(
|
|
29
29
|
state: FleetTabsState,
|
|
@@ -29,7 +29,7 @@ export function isAgentActive(composerStatus: string | undefined): boolean {
|
|
|
29
29
|
// shell/ui-openers.ts). The bare noun 'panels' undersold that; naming the verb
|
|
30
30
|
// keeps the tip honest about what the chord actually does.
|
|
31
31
|
const TIP_PANELS = 'Ctrl+P toggle panels';
|
|
32
|
-
//
|
|
32
|
+
// F2 now opens the Fleet panel (the process modal was retired), so the
|
|
33
33
|
// tip names 'fleet', not 'processes'.
|
|
34
34
|
const TIP_PROCESSES = 'F2 fleet';
|
|
35
35
|
const TIP_HELP = '? help';
|
|
@@ -189,7 +189,7 @@ function detailLines(picker: ModelPickerModal, width: number): string[] {
|
|
|
189
189
|
} else {
|
|
190
190
|
lines.push(`Context cap overrides the detected local-model context window for this selection.`);
|
|
191
191
|
}
|
|
192
|
-
//
|
|
192
|
+
// A visible cursor glyph on the query itself when search is focused —
|
|
193
193
|
// the only prior affordance was a footer-hint text swap ('/ search' vs.
|
|
194
194
|
// 'Typing filters search...'), buried at the end of a long hint line and
|
|
195
195
|
// easy to miss (confirmed via live tmux repro). This puts the "you are
|