@pellux/goodvibes-tui 1.1.0 → 1.9.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 +85 -21
- package/README.md +28 -18
- package/docs/foundation-artifacts/operator-contract.json +4045 -1763
- package/package.json +2 -2
- package/src/audio/spoken-turn-controller.ts +12 -2
- package/src/audio/spoken-turn-wiring.ts +2 -1
- package/src/cli/help.ts +8 -1
- package/src/cli/service-posture.ts +2 -1
- package/src/cli/status.ts +2 -1
- package/src/cli/surface-command.ts +2 -2
- package/src/core/alert-gating.ts +67 -0
- package/src/core/approval-alert.ts +72 -0
- package/src/core/budget-breach-notifier.ts +106 -0
- package/src/core/composer-state.ts +11 -3
- package/src/core/conversation-line-cache.ts +27 -3
- package/src/core/conversation-rendering.ts +90 -14
- package/src/core/conversation.ts +18 -0
- package/src/core/focus-tracker.ts +41 -0
- package/src/core/long-task-notifier.ts +33 -6
- package/src/core/stream-event-wiring.ts +20 -1
- package/src/core/system-message-noise.ts +87 -0
- package/src/core/system-message-router.ts +104 -51
- package/src/core/turn-cancellation.ts +25 -0
- package/src/core/turn-event-wiring.ts +72 -3
- package/src/daemon/cli.ts +29 -2
- package/src/daemon/handlers/register.ts +8 -1
- package/src/daemon/service-commands.ts +329 -0
- package/src/export/cost-utils.ts +36 -0
- package/src/input/autocomplete.ts +27 -1
- package/src/input/command-registry.ts +84 -5
- package/src/input/commands/checkpoint-runtime.ts +280 -0
- package/src/input/commands/codebase-runtime.ts +232 -0
- package/src/input/commands/config.ts +43 -3
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/health-runtime.ts +10 -2
- package/src/input/commands/image-runtime.ts +112 -0
- package/src/input/commands/intelligence-runtime.ts +11 -1
- package/src/input/commands/local-auth-runtime.ts +4 -1
- package/src/input/commands/local-runtime.ts +9 -3
- package/src/input/commands/marketplace-runtime.ts +2 -2
- package/src/input/commands/memory.ts +73 -35
- package/src/input/commands/operator-panel-runtime.ts +71 -31
- package/src/input/commands/planning-runtime.ts +116 -4
- package/src/input/commands/platform-sandbox-runtime.ts +1 -6
- package/src/input/commands/plugin-runtime.ts +2 -2
- package/src/input/commands/policy-dispatch.ts +21 -0
- package/src/input/commands/provider-accounts-runtime.ts +1 -1
- package/src/input/commands/qrcode-runtime.ts +26 -6
- package/src/input/commands/recall-review.ts +35 -0
- package/src/input/commands/remote-runtime-setup.ts +5 -3
- package/src/input/commands/remote-runtime.ts +3 -3
- package/src/input/commands/runtime-services.ts +54 -13
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-content.ts +20 -9
- package/src/input/commands/session-workflow.ts +16 -1
- package/src/input/commands/settings-sync-runtime.ts +16 -4
- package/src/input/commands/shell-core.ts +25 -8
- package/src/input/commands/skills-runtime.ts +2 -8
- package/src/input/commands/subscription-runtime.ts +2 -2
- package/src/input/commands/test-runtime.ts +277 -0
- package/src/input/commands/websearch-runtime.ts +101 -0
- package/src/input/commands/work-plan-runtime.ts +36 -10
- package/src/input/commands/workstream-runtime.ts +488 -0
- package/src/input/commands.ts +12 -0
- package/src/input/config-modal-types.ts +175 -0
- package/src/input/config-modal.ts +592 -0
- package/src/input/feed-context-factory.ts +10 -8
- package/src/input/handler-command-route.ts +37 -20
- package/src/input/handler-content-actions.ts +17 -2
- package/src/input/handler-feed-routes.ts +74 -114
- package/src/input/handler-feed.ts +79 -27
- package/src/input/handler-interactions.ts +1 -3
- package/src/input/handler-modal-routes.ts +136 -6
- package/src/input/handler-modal-stack.ts +14 -7
- package/src/input/handler-modal-token-routes.ts +16 -49
- package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
- package/src/input/handler-onboarding.ts +71 -59
- package/src/input/handler-picker-routes.ts +26 -102
- package/src/input/handler-shortcuts.ts +74 -14
- package/src/input/handler-types.ts +2 -6
- package/src/input/handler-ui-state.ts +10 -22
- package/src/input/handler.ts +12 -29
- package/src/input/keybindings.ts +23 -8
- package/src/input/model-picker-types.ts +24 -6
- package/src/input/model-picker.ts +77 -2
- package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
- package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
- package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
- package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
- package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
- package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
- package/src/input/panel-integration-actions.ts +9 -170
- package/src/input/panel-mouse-geometry.ts +97 -0
- package/src/input/panel-paste-flood-guard.ts +86 -0
- package/src/input/selection-modal.ts +11 -0
- package/src/input/session-picker-modal.ts +44 -4
- package/src/input/settings-modal-data.ts +146 -2
- package/src/input/settings-modal-mutations.ts +6 -4
- package/src/input/settings-modal-types.ts +4 -2
- package/src/main.ts +49 -51
- package/src/panels/agent-inspector-shared.ts +2 -1
- package/src/panels/base-panel.ts +22 -1
- package/src/panels/builtin/agent.ts +21 -107
- package/src/panels/builtin/development.ts +15 -61
- package/src/panels/builtin/knowledge.ts +8 -32
- package/src/panels/builtin/operations.ts +84 -428
- package/src/panels/builtin/session.ts +21 -112
- package/src/panels/builtin/shared.ts +18 -46
- package/src/panels/builtin-modals.ts +218 -0
- package/src/panels/builtin-panels.ts +5 -0
- package/src/panels/cost-tracker-panel.ts +36 -10
- package/src/panels/diff-panel.ts +12 -43
- package/src/panels/eval-registry.ts +60 -0
- package/src/panels/fleet-deep-link.ts +31 -0
- package/src/panels/fleet-panel-format.ts +62 -0
- package/src/panels/fleet-panel-worktree-detail.ts +48 -0
- package/src/panels/fleet-panel.ts +758 -0
- package/src/panels/fleet-read-model.ts +520 -0
- package/src/panels/fleet-steer.ts +125 -0
- package/src/panels/fleet-stop.ts +153 -0
- package/src/panels/fleet-tabs.ts +230 -0
- package/src/panels/fleet-transcript.ts +356 -0
- package/src/panels/index.ts +7 -31
- package/src/panels/modals/hooks-modal.ts +187 -0
- package/src/panels/modals/keybindings-modal.ts +166 -0
- package/src/panels/modals/knowledge-modal.ts +158 -0
- package/src/panels/modals/local-auth-modal.ts +132 -0
- package/src/panels/modals/marketplace-modal.ts +218 -0
- package/src/panels/modals/memory-modal.ts +180 -0
- package/src/panels/modals/modal-surface-helpers.ts +54 -0
- package/src/panels/modals/modal-theme.ts +42 -0
- package/src/panels/modals/pairing-modal.ts +163 -0
- package/src/panels/modals/planning-modal.ts +304 -0
- package/src/panels/modals/plugins-modal.ts +174 -0
- package/src/panels/modals/policy-modal.ts +256 -0
- package/src/panels/modals/provider-health-modal.ts +136 -0
- package/src/panels/modals/remote-modal.ts +115 -0
- package/src/panels/modals/sandbox-modal.ts +150 -0
- package/src/panels/modals/security-modal.ts +217 -0
- package/src/panels/modals/services-modal.ts +179 -0
- package/src/panels/modals/settings-sync-modal.ts +142 -0
- package/src/panels/modals/skills-modal.ts +189 -0
- package/src/panels/modals/subscription-modal.ts +172 -0
- package/src/panels/modals/work-plan-modal.ts +177 -0
- package/src/panels/panel-confirm-overlay.ts +72 -0
- package/src/panels/panel-manager.ts +123 -9
- package/src/panels/polish-core.ts +38 -25
- package/src/panels/project-planning-answer-actions.ts +8 -11
- package/src/panels/skills-panel.ts +7 -51
- package/src/panels/types.ts +37 -0
- package/src/permissions/hunk-selection.ts +179 -0
- package/src/permissions/prompt.ts +216 -13
- package/src/renderer/autocomplete-overlay.ts +28 -2
- package/src/renderer/compaction-history-modal.ts +19 -3
- package/src/renderer/compaction-preview.ts +13 -2
- package/src/renderer/compaction-quality.ts +100 -0
- package/src/renderer/compositor.ts +2 -3
- package/src/renderer/config-modal.ts +95 -0
- package/src/renderer/conversation-overlays.ts +10 -17
- package/src/renderer/fleet-tab-strip.ts +56 -0
- package/src/renderer/footer-tips.ts +15 -3
- package/src/renderer/fullscreen-primitives.ts +32 -22
- package/src/renderer/git-status.ts +43 -1
- package/src/renderer/help-overlay.ts +2 -2
- package/src/renderer/layout.ts +0 -4
- package/src/renderer/markdown.ts +7 -3
- package/src/renderer/modal-factory.ts +25 -20
- package/src/renderer/model-workspace.ts +62 -4
- package/src/renderer/overlay-box.ts +21 -17
- package/src/renderer/panel-workspace-bar.ts +8 -2
- package/src/renderer/process-indicator.ts +14 -3
- package/src/renderer/selection-modal-overlay.ts +6 -1
- package/src/renderer/session-picker-modal.ts +196 -3
- package/src/renderer/settings-modal-helpers.ts +2 -0
- package/src/renderer/settings-modal.ts +7 -0
- package/src/renderer/shell-surface.ts +8 -1
- package/src/renderer/status-glyphs.ts +14 -15
- package/src/renderer/system-message.ts +15 -3
- package/src/renderer/terminal-bg-probe.ts +339 -0
- package/src/renderer/terminal-escapes.ts +20 -0
- package/src/renderer/theme-mode-config.ts +67 -0
- package/src/renderer/theme.ts +91 -1
- package/src/renderer/thinking.ts +11 -3
- package/src/renderer/tool-call.ts +15 -9
- package/src/renderer/tool-result-summary.ts +148 -0
- package/src/renderer/turn-injection.ts +133 -0
- package/src/renderer/ui-factory.ts +154 -85
- package/src/renderer/ui-primitives.ts +30 -129
- package/src/runtime/bootstrap-command-context.ts +27 -1
- package/src/runtime/bootstrap-command-parts.ts +41 -10
- package/src/runtime/bootstrap-core.ts +47 -15
- package/src/runtime/bootstrap-hook-bridge.ts +7 -0
- package/src/runtime/bootstrap-shell.ts +48 -17
- package/src/runtime/bootstrap.ts +129 -22
- package/src/runtime/code-index-services.ts +135 -0
- package/src/runtime/legacy-daemon-migration.ts +516 -0
- package/src/runtime/memory-fold.ts +26 -0
- package/src/runtime/onboarding/derivation.ts +7 -2
- package/src/runtime/onboarding/snapshot.ts +27 -1
- package/src/runtime/onboarding/types.ts +31 -1
- package/src/runtime/operator-token-cleanup.ts +82 -1
- package/src/runtime/orchestrator-core-services.ts +59 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/resume-notice.ts +209 -0
- package/src/runtime/services.ts +98 -46
- package/src/runtime/session-inbound-inputs.ts +252 -0
- package/src/runtime/session-spine-transport.ts +64 -0
- package/src/runtime/terminal-output-guard.ts +15 -8
- package/src/runtime/ui-services.ts +27 -3
- package/src/runtime/workstream-services.ts +327 -0
- package/src/runtime/wrfc-persistence.ts +124 -17
- package/src/shell/blocking-input.ts +68 -4
- package/src/shell/recovery-input-helpers.ts +170 -1
- package/src/shell/ui-openers.ts +171 -26
- package/src/utils/format-duration.ts +8 -18
- package/src/utils/splash-lines.ts +1 -1
- package/src/utils/terminal-width.ts +52 -0
- package/src/version.ts +1 -1
- package/src/panels/agent-inspector-panel.ts +0 -786
- package/src/panels/approval-panel.ts +0 -252
- package/src/panels/automation-control-panel.ts +0 -479
- package/src/panels/cockpit-panel.ts +0 -481
- package/src/panels/cockpit-read-model.ts +0 -238
- package/src/panels/communication-panel.ts +0 -256
- package/src/panels/control-plane-panel.ts +0 -470
- package/src/panels/debug-panel.ts +0 -615
- package/src/panels/docs-panel.ts +0 -384
- package/src/panels/eval-panel.ts +0 -627
- package/src/panels/file-explorer-panel.ts +0 -673
- package/src/panels/file-preview-panel.ts +0 -517
- package/src/panels/hooks-panel.ts +0 -401
- package/src/panels/incident-review-panel.ts +0 -406
- package/src/panels/intelligence-panel.ts +0 -383
- package/src/panels/knowledge-graph-panel.ts +0 -515
- package/src/panels/marketplace-panel.ts +0 -399
- package/src/panels/memory-panel.ts +0 -558
- package/src/panels/ops-control-panel.ts +0 -329
- package/src/panels/ops-strategy-panel.ts +0 -321
- package/src/panels/orchestration-panel.ts +0 -465
- package/src/panels/panel-list-panel.ts +0 -566
- package/src/panels/plan-dashboard-panel.ts +0 -707
- package/src/panels/policy-panel.ts +0 -517
- package/src/panels/project-planning-panel.ts +0 -731
- package/src/panels/provider-health-panel.ts +0 -678
- package/src/panels/provider-health-tracker.ts +0 -310
- package/src/panels/provider-health-views.ts +0 -567
- package/src/panels/qr-panel.ts +0 -280
- package/src/panels/remote-panel.ts +0 -534
- package/src/panels/routes-panel.ts +0 -241
- package/src/panels/sandbox-panel.ts +0 -456
- package/src/panels/security-panel.ts +0 -447
- package/src/panels/services-panel.ts +0 -329
- package/src/panels/session-browser-panel.ts +0 -496
- package/src/panels/settings-sync-panel.ts +0 -398
- package/src/panels/subscription-panel.ts +0 -342
- package/src/panels/symbol-outline-panel.ts +0 -619
- package/src/panels/system-messages-panel.ts +0 -364
- package/src/panels/tasks-panel.ts +0 -608
- package/src/panels/thinking-panel.ts +0 -333
- package/src/panels/tool-inspector-panel.ts +0 -537
- package/src/panels/work-plan-panel.ts +0 -540
- package/src/panels/worktree-panel.ts +0 -360
- package/src/panels/wrfc-panel.ts +0 -790
- package/src/renderer/agent-detail-modal.ts +0 -466
- package/src/renderer/live-tail-modal.ts +0 -156
- package/src/renderer/process-modal.ts +0 -671
- package/src/renderer/qr-renderer.ts +0 -120
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// fleet-stop.ts
|
|
3
|
+
//
|
|
4
|
+
// d1 (W6.2) — the "stopping…" write-window overlay, split out of fleet-panel.ts
|
|
5
|
+
// to keep that file under the architecture 800-line gate (same rationale as
|
|
6
|
+
// fleet-steer.ts). FleetPanel owns nothing here beyond a FleetStopTracker
|
|
7
|
+
// instance; all the state->display logic lives in these pure helpers.
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
import type { ProcessNode, ProcessState } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
|
|
11
|
+
import { fleetStateGlyph, fleetStateTone, isTerminalProcessState, type FleetStateTone, type FleetTreeRow } from './fleet-read-model.ts';
|
|
12
|
+
|
|
13
|
+
/** How long 'stopping…' lingers after a stop keypress before the true state is shown regardless (never masks a stuck kill). */
|
|
14
|
+
export const STOP_SETTLE_MS = 1500;
|
|
15
|
+
/** Display-only glyph for a node with an in-flight stop request. Verified free against STATE_GLYPHS (fleet-read-model.ts) and width-1 like its ⊘/⊟ siblings. */
|
|
16
|
+
export const STOPPING_GLYPH = '⊗';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Tracks node ids the operator just asked to stop (kill/interrupt/pause) mapped
|
|
20
|
+
* to the keypress time. While an id is present AND within STOP_SETTLE_MS its row
|
|
21
|
+
* renders a display-only 'stopping…' instead of the raw state — so the tree
|
|
22
|
+
* never claims the past-tense 'killed'/'interrupted' during the brief write
|
|
23
|
+
* window between the keypress and the state actually flipping. After the settle
|
|
24
|
+
* window the TRUE state is ALWAYS shown, so a genuinely stuck kill is never
|
|
25
|
+
* masked. 'stopping…' is NOT a ProcessState; it lives only here.
|
|
26
|
+
*/
|
|
27
|
+
export class FleetStopTracker {
|
|
28
|
+
private readonly requestedAt = new Map<string, number>();
|
|
29
|
+
|
|
30
|
+
/** Record a stop request for `id` (kill/interrupt/pause). */
|
|
31
|
+
mark(id: string, now: number = Date.now()): void {
|
|
32
|
+
this.requestedAt.set(id, now);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Drop any stop request for `id` (e.g. a resume, which is a start not a stop). */
|
|
36
|
+
clear(id: string): void {
|
|
37
|
+
this.requestedAt.delete(id);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** True while `id` has an in-flight stop request inside the settle window. Prunes expired markers so the map stays bounded and stuck kills are never masked past the window. */
|
|
41
|
+
isStopping(id: string, now: number = Date.now()): boolean {
|
|
42
|
+
const at = this.requestedAt.get(id);
|
|
43
|
+
if (at === undefined) return false;
|
|
44
|
+
if (now - at < STOP_SETTLE_MS) return true;
|
|
45
|
+
this.requestedAt.delete(id);
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Glyph + literal label + tone for a node, applying the display-only 'stopping…' override while a stop is in flight. */
|
|
51
|
+
export interface FleetStateDisplay {
|
|
52
|
+
readonly glyph: string;
|
|
53
|
+
readonly label: string;
|
|
54
|
+
readonly tone: FleetStateTone;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function fleetStateDisplay(state: ProcessState, stopping: boolean): FleetStateDisplay {
|
|
58
|
+
return stopping
|
|
59
|
+
? { glyph: STOPPING_GLYPH, label: 'stopping…', tone: 'warn' }
|
|
60
|
+
: { glyph: fleetStateGlyph(state), label: state, tone: fleetStateTone(state) };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The narrow action + side-effect surface `toggleFleetPause` needs from FleetPanel. */
|
|
64
|
+
export interface FleetPauseDeps {
|
|
65
|
+
readonly interrupt: (id: string) => boolean;
|
|
66
|
+
readonly resume: (id: string) => boolean;
|
|
67
|
+
readonly setError: (message: string) => void;
|
|
68
|
+
readonly markDirty: () => void;
|
|
69
|
+
readonly tracker: FleetStopTracker;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* d2 (W6.2): 'p' toggles pause<->resume by the node's state. A `paused`
|
|
74
|
+
* resumable node is resumed (interrupt's inverse); a live pausable node is
|
|
75
|
+
* paused via the registry's interrupt() (its disable path). Honest refusal for
|
|
76
|
+
* non-resumable/non-pausable kinds. Returns true when the key is consumed,
|
|
77
|
+
* false to fall through (terminal, non-paused node — nothing to toggle).
|
|
78
|
+
*/
|
|
79
|
+
export function toggleFleetPause(node: ProcessNode, deps: FleetPauseDeps): boolean {
|
|
80
|
+
if (node.state === 'paused') {
|
|
81
|
+
if (!node.capabilities.resumable) { deps.setError(`${node.kind} cannot be resumed.`); return true; }
|
|
82
|
+
deps.resume(node.id);
|
|
83
|
+
deps.tracker.clear(node.id); // a resume is a start, not a stop
|
|
84
|
+
deps.markDirty();
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
if (isTerminalProcessState(node.state)) return false;
|
|
88
|
+
if (!node.capabilities.pausable) { deps.setError(`${node.kind} does not support pause.`); return true; }
|
|
89
|
+
deps.interrupt(node.id);
|
|
90
|
+
deps.tracker.mark(node.id);
|
|
91
|
+
deps.markDirty();
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** A single keyboard-hint chip in the fleet tree footer. */
|
|
96
|
+
export interface FleetHint {
|
|
97
|
+
readonly keys: string;
|
|
98
|
+
readonly label: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Build the fleet tree footer hints for the current selection. Capability gates
|
|
103
|
+
* mirror the handleInput guards: interrupt/kill only on a live capable node;
|
|
104
|
+
* 'p' shows 'pause' for a live pausable node and 'resume' for a paused resumable
|
|
105
|
+
* one (never both, d2).
|
|
106
|
+
*/
|
|
107
|
+
export function buildFleetTreeHints(
|
|
108
|
+
selected: ProcessNode | undefined,
|
|
109
|
+
follow: boolean,
|
|
110
|
+
hasTabs: boolean,
|
|
111
|
+
): FleetHint[] {
|
|
112
|
+
const live = selected !== undefined && !isTerminalProcessState(selected.state);
|
|
113
|
+
const isPaused = selected !== undefined && selected.state === 'paused';
|
|
114
|
+
const hints: FleetHint[] = [
|
|
115
|
+
{ keys: 'j/k', label: 'navigate' },
|
|
116
|
+
{ keys: 'Enter', label: 'attach' },
|
|
117
|
+
];
|
|
118
|
+
if (live && selected.capabilities.steerable) hints.push({ keys: 's', label: 'steer' }); // D4: discoverable from the tree (attach-and-steer)
|
|
119
|
+
if (live && selected.capabilities.interruptible) hints.push({ keys: 'i', label: 'interrupt' });
|
|
120
|
+
if (live && selected.capabilities.killable) hints.push({ keys: 'K', label: 'kill' });
|
|
121
|
+
if (live && !isPaused && selected.capabilities.pausable) hints.push({ keys: 'p', label: 'pause' });
|
|
122
|
+
if (isPaused && selected.capabilities.resumable) hints.push({ keys: 'p', label: 'resume' });
|
|
123
|
+
hints.push({ keys: 'f', label: follow ? 'follow:on' : 'follow' });
|
|
124
|
+
if (hasTabs) hints.push({ keys: '[ ]', label: 'tabs' });
|
|
125
|
+
return hints;
|
|
126
|
+
}
|
|
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" (Wave-3 C7), undercounting a mixed subtree. */
|
|
129
|
+
export function countDescendantStats(rows: readonly FleetTreeRow[], nodeId: string): { total: number; active: number } {
|
|
130
|
+
const byParent = new Map<string, string[]>();
|
|
131
|
+
for (const row of rows) {
|
|
132
|
+
if (!row.node.parentId) continue;
|
|
133
|
+
const siblings = byParent.get(row.node.parentId) ?? [];
|
|
134
|
+
siblings.push(row.node.id);
|
|
135
|
+
byParent.set(row.node.parentId, siblings);
|
|
136
|
+
}
|
|
137
|
+
const byId = new Map(rows.map((row) => [row.node.id, row] as const));
|
|
138
|
+
let total = 0, active = 0;
|
|
139
|
+
const stack = [...(byParent.get(nodeId) ?? [])];
|
|
140
|
+
const seen = new Set<string>();
|
|
141
|
+
while (stack.length > 0) {
|
|
142
|
+
const id = stack.pop()!;
|
|
143
|
+
if (seen.has(id)) continue;
|
|
144
|
+
seen.add(id);
|
|
145
|
+
const row = byId.get(id);
|
|
146
|
+
if (row && !isTerminalProcessState(row.node.state)) {
|
|
147
|
+
total++;
|
|
148
|
+
if (row.node.capabilities.killable) active++;
|
|
149
|
+
}
|
|
150
|
+
for (const child of byParent.get(id) ?? []) stack.push(child);
|
|
151
|
+
}
|
|
152
|
+
return { total, active };
|
|
153
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// fleet-tabs.ts
|
|
3
|
+
//
|
|
4
|
+
// Wave-3 (W3.1 Part C) — pure, testable tab-state model for FleetPanel's
|
|
5
|
+
// session tabs. No BasePanel/rendering dependency, mirroring the
|
|
6
|
+
// fleet-read-model.ts convention: this module owns attach/detach/switch as
|
|
7
|
+
// pure state transitions over a small immutable FleetTabsState, so the tab
|
|
8
|
+
// lifecycle is unit-testable without a live registry or a rendered panel.
|
|
9
|
+
//
|
|
10
|
+
// Tab-index convention: index 0 is always the fleet TREE (the panel's root
|
|
11
|
+
// view, never stored in `tabs`); `activeTabIndex` 1..N addresses
|
|
12
|
+
// `tabs[0..N-1]`. This mirrors the brief's "tree is the root/home tab"
|
|
13
|
+
// framing while keeping the array itself simple (only attached tabs).
|
|
14
|
+
//
|
|
15
|
+
// Attachable kinds: only 'agent' and 'wrfc-chain' carry anything worth
|
|
16
|
+
// attaching to (a transcript for an agent; a live member summary for a
|
|
17
|
+
// chain — see fleet-transcript.ts). workflow/trigger/schedule/watcher/
|
|
18
|
+
// background-process/wrfc-subtask nodes have no transcript and are never
|
|
19
|
+
// attachable (FleetPanel shows a status message instead, matching the
|
|
20
|
+
// existing i/K "not supported" convention).
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
import type { ProcessKind, ProcessNode } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
|
|
24
|
+
import { MessageLineCache } from '../core/conversation-line-cache.ts';
|
|
25
|
+
import { fleetKindTag } from './fleet-read-model.ts';
|
|
26
|
+
|
|
27
|
+
/** Node kinds that can be attached as a session tab. */
|
|
28
|
+
export type FleetAttachableKind = 'agent' | 'wrfc-chain';
|
|
29
|
+
|
|
30
|
+
export function isAttachableFleetKind(kind: ProcessKind): kind is FleetAttachableKind {
|
|
31
|
+
return kind === 'agent' || kind === 'wrfc-chain';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Wave-3 (W3.2 steering) — lifecycle of a queued steer message, tracked
|
|
36
|
+
* per-tab. `queued` means `ProcessRegistry.steer()` accepted the message
|
|
37
|
+
* onto the target's inbox, NOT that the agent has seen it — that is the
|
|
38
|
+
* later, honest `consumed` transition (a `COMMUNICATION_CONSUMED` runtime-bus
|
|
39
|
+
* event matching this badge's `messageId`). `dropped` is a TUI-side
|
|
40
|
+
* inference: the SDK emits no "expired"/"cancelled" signal for a queued
|
|
41
|
+
* steer, so if the target node goes terminal (done/failed/killed/
|
|
42
|
+
* interrupted) while the badge is still `queued`, FleetPanel resolves it to
|
|
43
|
+
* `dropped` itself rather than leaving the badge hanging forever (see
|
|
44
|
+
* fleet-panel.ts reconcileSteerBadges — cross-WO note: the SDK engineer
|
|
45
|
+
* confirmed no dropped signal exists).
|
|
46
|
+
*/
|
|
47
|
+
export type SteerBadgeStatus = 'queued' | 'consumed' | 'dropped';
|
|
48
|
+
|
|
49
|
+
/** A tab's steer-message badge state (null on the tab = no active/recent steer). */
|
|
50
|
+
export interface SteerBadge {
|
|
51
|
+
readonly messageId: string;
|
|
52
|
+
readonly status: SteerBadgeStatus;
|
|
53
|
+
/** Present for 'dropped' — a one-line honest explanation shown in the tab. */
|
|
54
|
+
readonly note?: string;
|
|
55
|
+
/** epoch ms when status left 'queued' (consumed or dropped) — drives FleetPanel's linger-then-clear tick. */
|
|
56
|
+
readonly resolvedAt?: number;
|
|
57
|
+
/**
|
|
58
|
+
* epoch ms when status entered 'queued' (set once, at submit time). Drives
|
|
59
|
+
* fleet-steer.ts's reconcileSteerBadges TTL-expiry fallback: the SDK's
|
|
60
|
+
* MessageBus attaches its own STEER_TTL_MS to the underlying steer message
|
|
61
|
+
* (see registry.js's steer()) but never tells the TUI when that TTL lapses
|
|
62
|
+
* without delivery — the "agent stays healthy/non-terminal through one
|
|
63
|
+
* very long tool call, and the steer simply expires unseen in the bus"
|
|
64
|
+
* case. Without this, that badge would show 'queued' forever. Optional
|
|
65
|
+
* because older/hand-built badges (tests, pre-fix data) may not carry it —
|
|
66
|
+
* absence just means "no TTL-expiry inference possible for this badge",
|
|
67
|
+
* not an error.
|
|
68
|
+
*/
|
|
69
|
+
readonly queuedAt?: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Append one character of steer-composer input to a draft, normalizing a
|
|
74
|
+
* pasted multi-line block's line breaks (terminals transmit bracketed-paste
|
|
75
|
+
* newlines as literal `\r`/`\n` characters delivered one at a time through
|
|
76
|
+
* the same per-char burst pipeline as ordinary typing — see
|
|
77
|
+
* handler-feed-routes.ts's isCapturingTextBurst contract) to a single
|
|
78
|
+
* collapsed space instead of either corrupting the one-line field with a
|
|
79
|
+
* raw control character or silently dropping the content. Mirrors how a
|
|
80
|
+
* plain single-line text input normalizes pasted newlines to whitespace.
|
|
81
|
+
*/
|
|
82
|
+
export function appendSteerText(draft: string, ch: string): string {
|
|
83
|
+
if (ch === '\r' || ch === '\n') {
|
|
84
|
+
if (draft.length === 0 || draft.endsWith(' ')) return draft;
|
|
85
|
+
return `${draft} `;
|
|
86
|
+
}
|
|
87
|
+
return draft + ch;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* One attached session tab. `agentId` is the SDK attach handle for
|
|
92
|
+
* `AgentManager.getConversationSnapshot(agentId)` — populated only for
|
|
93
|
+
* 'agent' tabs (agent.ts sets ProcessNode.id = record.id, so node.id IS the
|
|
94
|
+
* agentId; see the W3.1 brief's C2 note). 'wrfc-chain' tabs render a live
|
|
95
|
+
* member-summary instead of a transcript (a chain has no single conversation
|
|
96
|
+
* of its own — see fleet-transcript.ts renderFleetChainSummary) and carry an
|
|
97
|
+
* empty agentId, unused.
|
|
98
|
+
*
|
|
99
|
+
* `lineCache` is a PER-TAB `MessageLineCache` (C5 backpressure): switching
|
|
100
|
+
* tabs must never invalidate or thrash the main session's own cache, and a
|
|
101
|
+
* background (non-focused) tab must render nothing beyond its existing
|
|
102
|
+
* tree-row, so its cache simply stays empty (size 0) until the tab is
|
|
103
|
+
* focused. Disposed (`.clear()`) on detach so a closed tab's rendered
|
|
104
|
+
* Line[] are not retained (brief risk #8).
|
|
105
|
+
*/
|
|
106
|
+
export interface FleetTab {
|
|
107
|
+
readonly nodeId: string;
|
|
108
|
+
readonly kind: FleetAttachableKind;
|
|
109
|
+
readonly agentId: string;
|
|
110
|
+
readonly label: string;
|
|
111
|
+
readonly lineCache: MessageLineCache;
|
|
112
|
+
/**
|
|
113
|
+
* Cached parsed ledger fallback for a terminal agent whose conversation
|
|
114
|
+
* snapshot has been evicted from the SDK's retention ring (C6 degraded
|
|
115
|
+
* path). `null` = not yet loaded/attempted; `[]` = loaded and empty (or
|
|
116
|
+
* the load failed) — both render the same honest "no transcript" state
|
|
117
|
+
* once loaded is true.
|
|
118
|
+
*/
|
|
119
|
+
ledgerEntries: Record<string, unknown>[] | null;
|
|
120
|
+
ledgerLoadStarted: boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Wave-3 (W3.2): the one-line steer composer's in-progress text, or `null`
|
|
123
|
+
* when not composing. Mirrors git-panel.ts's `commitMessage` mutable-slot
|
|
124
|
+
* convention (FleetPanel.isCapturingTextBurst() gates on this being
|
|
125
|
+
* non-null so a burst/paste lands here char-by-char, never as tree/tab
|
|
126
|
+
* hotkeys — see FleetPanel.handleSteerInput).
|
|
127
|
+
*/
|
|
128
|
+
steerDraft: string | null;
|
|
129
|
+
/** Wave-3 (W3.2): this tab's most recent steer message's lifecycle, or null. */
|
|
130
|
+
steerBadge: SteerBadge | null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Immutable tab-bar state: the attached tabs plus which one (or the root tree) is active. */
|
|
134
|
+
export interface FleetTabsState {
|
|
135
|
+
readonly tabs: readonly FleetTab[];
|
|
136
|
+
/** 0 = root tree; 1..tabs.length addresses tabs[activeTabIndex - 1]. */
|
|
137
|
+
readonly activeTabIndex: number;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const EMPTY_FLEET_TABS_STATE: FleetTabsState = { tabs: [], activeTabIndex: 0 };
|
|
141
|
+
|
|
142
|
+
function shortNodeId(id: string): string {
|
|
143
|
+
if (id.length <= 10) return id;
|
|
144
|
+
// Trim a leading '-' left over from slicing mid-token (e.g. 'agent-done-01'
|
|
145
|
+
// -> '-done-01' before this trim) so the label never starts with a stray
|
|
146
|
+
// separator character.
|
|
147
|
+
return id.slice(-8).replace(/^-+/, '');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function fleetTabLabel(node: ProcessNode): string {
|
|
151
|
+
return `${fleetKindTag(node.kind)} ${shortNodeId(node.id)}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function makeFleetTab(node: ProcessNode & { kind: FleetAttachableKind }): FleetTab {
|
|
155
|
+
return {
|
|
156
|
+
nodeId: node.id,
|
|
157
|
+
kind: node.kind,
|
|
158
|
+
agentId: node.kind === 'agent' ? node.id : '',
|
|
159
|
+
label: fleetTabLabel(node),
|
|
160
|
+
lineCache: new MessageLineCache(),
|
|
161
|
+
ledgerEntries: null,
|
|
162
|
+
ledgerLoadStarted: false,
|
|
163
|
+
steerDraft: null,
|
|
164
|
+
steerBadge: null,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The currently-active tab, or null when the root tree is active. */
|
|
169
|
+
export function activeFleetTab(state: FleetTabsState): FleetTab | null {
|
|
170
|
+
return state.activeTabIndex > 0 ? (state.tabs[state.activeTabIndex - 1] ?? null) : null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Attach a node as a tab (Enter on an attachable tree row). Re-focuses an
|
|
175
|
+
* already-open tab for the same node instead of creating a duplicate.
|
|
176
|
+
* Returns `state` unchanged if `node.kind` is not attachable — callers
|
|
177
|
+
* (FleetPanel) are expected to guard with `isAttachableFleetKind` first and
|
|
178
|
+
* report a status message rather than silently doing nothing; this
|
|
179
|
+
* function's own no-op fallback is defense in depth.
|
|
180
|
+
*/
|
|
181
|
+
export function attachFleetTab(state: FleetTabsState, node: ProcessNode): FleetTabsState {
|
|
182
|
+
if (!isAttachableFleetKind(node.kind)) return state;
|
|
183
|
+
const existingIndex = state.tabs.findIndex((tab) => tab.nodeId === node.id);
|
|
184
|
+
if (existingIndex >= 0) {
|
|
185
|
+
return { tabs: state.tabs, activeTabIndex: existingIndex + 1 };
|
|
186
|
+
}
|
|
187
|
+
const tab = makeFleetTab(node as ProcessNode & { kind: FleetAttachableKind });
|
|
188
|
+
const tabs = [...state.tabs, tab];
|
|
189
|
+
return { tabs, activeTabIndex: tabs.length };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Detach the tab at `tabIndex` (a plain index into `state.tabs`, NOT an
|
|
194
|
+
* `activeTabIndex`). Disposes its line cache. Re-anchors `activeTabIndex`:
|
|
195
|
+
* detaching the active tab falls back to the root tree; detaching a tab
|
|
196
|
+
* before the active one shifts the active index left to keep pointing at
|
|
197
|
+
* the same logical tab.
|
|
198
|
+
*/
|
|
199
|
+
export function detachFleetTab(state: FleetTabsState, tabIndex: number): FleetTabsState {
|
|
200
|
+
if (tabIndex < 0 || tabIndex >= state.tabs.length) return state;
|
|
201
|
+
state.tabs[tabIndex]?.lineCache.clear();
|
|
202
|
+
const tabs = state.tabs.filter((_, i) => i !== tabIndex);
|
|
203
|
+
const removedActiveSlot = tabIndex + 1;
|
|
204
|
+
let activeTabIndex = state.activeTabIndex;
|
|
205
|
+
if (activeTabIndex === removedActiveSlot) {
|
|
206
|
+
activeTabIndex = 0;
|
|
207
|
+
} else if (activeTabIndex > removedActiveSlot) {
|
|
208
|
+
activeTabIndex -= 1;
|
|
209
|
+
}
|
|
210
|
+
return { tabs, activeTabIndex };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Detach whichever tab is currently active. No-op (returns state unchanged) when the root tree is active. */
|
|
214
|
+
export function detachActiveFleetTab(state: FleetTabsState): FleetTabsState {
|
|
215
|
+
if (state.activeTabIndex === 0) return state;
|
|
216
|
+
return detachFleetTab(state, state.activeTabIndex - 1);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Switch to an absolute tab-strip index (0 = root tree). Clamped to the valid range; out-of-range requests are a no-op. */
|
|
220
|
+
export function switchFleetTab(state: FleetTabsState, activeTabIndex: number): FleetTabsState {
|
|
221
|
+
if (activeTabIndex < 0 || activeTabIndex > state.tabs.length) return state;
|
|
222
|
+
if (activeTabIndex === state.activeTabIndex) return state;
|
|
223
|
+
return { tabs: state.tabs, activeTabIndex };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Move focus to the next/previous tab in the strip (wrapping is deliberately NOT applied — clamps at the ends). */
|
|
227
|
+
export function stepFleetTab(state: FleetTabsState, direction: 1 | -1): FleetTabsState {
|
|
228
|
+
const next = state.activeTabIndex + direction;
|
|
229
|
+
return switchFleetTab(state, Math.max(0, Math.min(state.tabs.length, next)));
|
|
230
|
+
}
|