@pellux/goodvibes-tui 1.16.1 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +138 -9
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +5490 -1806
- package/package.json +7 -7
- package/src/cli/completions/generate.ts +5 -4
- package/src/cli/config-overrides.ts +51 -11
- package/src/cli/doctor.ts +4 -5
- package/src/cli/entrypoint.ts +13 -8
- package/src/cli/help.ts +7 -6
- package/src/cli/launch-auto-update.ts +214 -0
- package/src/cli/management-commands.ts +82 -15
- package/src/cli/management.ts +0 -2
- package/src/cli/parser.ts +5 -0
- package/src/cli/status.ts +13 -0
- package/src/cli/surface-command.ts +5 -5
- package/src/cli/tui-startup.ts +60 -97
- package/src/cli/types.ts +2 -0
- package/src/config/index.ts +1 -1
- package/src/config/tui-extension-settings.ts +33 -0
- package/src/core/composer-state.ts +1 -1
- package/src/core/consolidation-receipt.ts +41 -0
- package/src/core/conversation-line-cache.ts +3 -3
- package/src/core/conversation-rendering.ts +18 -9
- package/src/core/conversation.ts +2 -2
- package/src/core/long-task-notifier.ts +7 -7
- package/src/core/memory-provenance.ts +67 -0
- package/src/core/pairing-devices.ts +49 -0
- package/src/core/pairing-handoff.ts +101 -0
- package/src/core/pairing-offers.ts +83 -0
- package/src/core/pairing-origin.ts +78 -0
- package/src/core/pairing-tailscale-gateway.ts +56 -0
- package/src/core/power-chip-source.ts +91 -0
- package/src/core/power-status.ts +95 -0
- package/src/core/stable-host.ts +171 -0
- package/src/core/stream-event-wiring.ts +29 -6
- package/src/core/system-message-noise.ts +1 -1
- package/src/core/system-message-router.ts +6 -6
- package/src/core/turn-budget-outcome.ts +60 -0
- package/src/core/turn-cancellation.ts +40 -1
- package/src/core/turn-event-wiring.ts +14 -8
- package/src/daemon/cli.ts +64 -58
- package/src/daemon/lifecycle.ts +41 -0
- package/src/daemon/service-commands.ts +5 -5
- package/src/export/cost-utils.ts +128 -6
- package/src/input/autocomplete.ts +2 -2
- package/src/input/command-registry.ts +55 -9
- package/src/input/commands/acp-runtime.ts +72 -0
- package/src/input/commands/ci-runtime.ts +5 -1
- package/src/input/commands/codebase-runtime.ts +6 -6
- package/src/input/commands/cost-runtime.ts +1 -1
- package/src/input/commands/devices-runtime.ts +109 -0
- package/src/input/commands/git-runtime.ts +28 -4
- package/src/input/commands/graph-runtime.ts +45 -0
- package/src/input/commands/health-runtime.ts +2 -2
- package/src/input/commands/intelligence-runtime.ts +1 -1
- package/src/input/commands/local-auth-runtime.ts +1 -1
- package/src/input/commands/local-provider-runtime.ts +49 -10
- package/src/input/commands/local-runtime.ts +53 -5
- package/src/input/commands/memory.ts +2 -2
- package/src/input/commands/operator-panel-runtime.ts +5 -5
- package/src/input/commands/operator-rpc.ts +32 -11
- package/src/input/commands/permissions-provenance.ts +1 -1
- package/src/input/commands/permissions-runtime.ts +99 -8
- package/src/input/commands/planning-runtime.ts +3 -3
- package/src/input/commands/platform-sandbox-runtime.ts +1 -1
- package/src/input/commands/power-runtime.ts +56 -0
- package/src/input/commands/provider-accounts-runtime.ts +71 -6
- package/src/input/commands/provider.ts +7 -5
- package/src/input/commands/qrcode-runtime.ts +14 -19
- package/src/input/commands/queue-runtime.ts +70 -0
- package/src/input/commands/recall-files-git.ts +13 -51
- package/src/input/commands/recall-files.ts +8 -11
- package/src/input/commands/recall-review.ts +2 -2
- package/src/input/commands/relay-runtime.ts +1 -1
- package/src/input/commands/remote-runtime.ts +1 -1
- package/src/input/commands/runtime-services.ts +4 -4
- package/src/input/commands/schedule-runtime.ts +14 -7
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-workflow.ts +2 -2
- package/src/input/commands/settings-sync-runtime.ts +2 -2
- package/src/input/commands/share-runtime.ts +1 -1
- package/src/input/commands/shell-core.ts +46 -68
- package/src/input/commands/subscription-runtime.ts +1 -1
- package/src/input/commands/update-runtime.ts +153 -96
- package/src/input/commands/work-plan-runtime.ts +1 -1
- package/src/input/commands/workstream-attempts.ts +6 -1
- package/src/input/commands/workstream-runtime.ts +46 -27
- package/src/input/commands/worktree-runtime.ts +35 -8
- package/src/input/commands.ts +10 -2
- package/src/input/config-modal-types.ts +14 -3
- package/src/input/config-modal.ts +75 -13
- package/src/input/feature-unit-layout.ts +93 -82
- package/src/input/feed-context-factory.ts +2 -2
- package/src/input/handler-command-route.ts +2 -2
- package/src/input/handler-content-actions.ts +1 -1
- package/src/input/handler-feed-routes.ts +7 -7
- package/src/input/handler-feed.ts +4 -4
- package/src/input/handler-interactions.ts +3 -2
- package/src/input/handler-modal-routes.ts +12 -4
- package/src/input/handler-modal-token-routes.ts +1 -1
- package/src/input/handler-onboarding-daemon-adopt.ts +2 -2
- package/src/input/handler-onboarding.ts +3 -2
- package/src/input/handler-picker-routes.ts +26 -13
- package/src/input/handler-shortcuts.ts +29 -6
- package/src/input/handler.ts +2 -2
- package/src/input/keybindings.ts +21 -3
- package/src/input/onboarding/onboarding-feature-units.ts +34 -27
- package/src/input/onboarding/onboarding-provider-key-fields.ts +112 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +22 -21
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +17 -0
- package/src/input/onboarding/onboarding-wizard-network-adopt.ts +2 -2
- package/src/input/onboarding/onboarding-wizard-steps.ts +30 -42
- package/src/input/panel-integration-actions.ts +2 -2
- package/src/input/panel-mouse-geometry.ts +1 -1
- package/src/input/panel-paste-flood-guard.ts +1 -1
- package/src/input/provider-key-intake.ts +167 -0
- package/src/input/sandbox-exec-config.ts +2 -2
- package/src/input/selection-modal.ts +1 -1
- package/src/input/settings-modal-activation.ts +5 -0
- package/src/input/settings-modal-adjustment.ts +4 -31
- package/src/input/settings-modal-data.ts +52 -57
- package/src/input/settings-modal-mutations.ts +12 -68
- package/src/input/settings-modal-types.ts +27 -17
- package/src/input/settings-modal.ts +74 -55
- package/src/main.ts +90 -90
- package/src/panels/agent-inspector-shared.ts +4 -4
- package/src/panels/base-panel.ts +2 -2
- package/src/panels/builtin/agent.ts +4 -4
- package/src/panels/builtin/development.ts +4 -4
- package/src/panels/builtin/knowledge.ts +1 -1
- package/src/panels/builtin/operations.ts +83 -11
- package/src/panels/builtin/session.ts +5 -5
- package/src/panels/builtin/shared.ts +16 -4
- package/src/panels/builtin-modals.ts +59 -42
- package/src/panels/builtin-panels.ts +1 -1
- package/src/panels/cost-tracker-panel.ts +124 -7
- package/src/panels/diff-panel.ts +39 -6
- package/src/panels/eval-registry.ts +2 -2
- package/src/panels/fleet-acts.ts +446 -0
- package/src/panels/fleet-deep-link.ts +1 -1
- package/src/panels/fleet-gateway.ts +117 -0
- package/src/panels/fleet-observed-render.ts +128 -0
- package/src/panels/fleet-panel-format.ts +163 -11
- package/src/panels/fleet-panel-worktree-detail.ts +14 -0
- package/src/panels/fleet-panel.ts +73 -57
- package/src/panels/fleet-read-model.ts +84 -14
- package/src/panels/fleet-spawn.ts +266 -0
- package/src/panels/fleet-steer.ts +2 -2
- package/src/panels/fleet-stop.ts +41 -5
- package/src/panels/fleet-tabs.ts +15 -16
- package/src/panels/fleet-transcript.ts +5 -5
- package/src/panels/git-panel.ts +2 -2
- package/src/panels/index.ts +4 -1
- package/src/panels/local-auth-panel.ts +1 -1
- package/src/panels/memory-consolidation-gateway.ts +94 -0
- package/src/panels/modals/devices-modal.ts +161 -0
- package/src/panels/modals/hooks-modal.ts +1 -1
- package/src/panels/modals/keybindings-modal.ts +2 -2
- package/src/panels/modals/knowledge-modal.ts +1 -1
- package/src/panels/modals/marketplace-modal.ts +4 -4
- package/src/panels/modals/memory-modal.ts +153 -9
- package/src/panels/modals/modal-surface-helpers.ts +1 -1
- package/src/panels/modals/modal-theme.ts +1 -1
- package/src/panels/modals/pairing-modal.ts +198 -68
- package/src/panels/modals/planning-modal.ts +2 -2
- package/src/panels/modals/plugins-modal.ts +1 -1
- package/src/panels/modals/policy-modal.ts +1 -1
- package/src/panels/modals/security-modal.ts +7 -1
- package/src/panels/modals/skills-modal.ts +1 -1
- package/src/panels/modals/work-plan-modal.ts +3 -3
- package/src/panels/notifications-feed.ts +127 -0
- package/src/panels/notifications-panel.ts +160 -0
- package/src/panels/panel-manager.ts +9 -9
- package/src/panels/plugins-panel.ts +4 -4
- package/src/panels/project-planning-answer-actions.ts +1 -1
- package/src/panels/provider-health-routes.ts +2 -2
- package/src/panels/skills-panel.ts +4 -5
- package/src/panels/token-budget-panel.ts +11 -3
- package/src/panels/types.ts +5 -5
- package/src/panels/workstream-graph-render.ts +123 -0
- package/src/permissions/broker-approval-card.ts +201 -0
- package/src/permissions/hunk-selection.ts +28 -4
- package/src/permissions/prompt.ts +244 -29
- package/src/renderer/autocomplete-overlay.ts +107 -54
- package/src/renderer/code-block.ts +2 -2
- package/src/renderer/compaction-history-modal.ts +1 -1
- package/src/renderer/compaction-preview.ts +1 -1
- package/src/renderer/compaction-quality.ts +1 -1
- package/src/renderer/composer-fragments.ts +69 -0
- package/src/renderer/compositor.ts +2 -2
- package/src/renderer/config-modal.ts +1 -1
- package/src/renderer/conversation-overlays.ts +1 -1
- package/src/renderer/footer-tips.ts +3 -3
- package/src/renderer/hint-grammar.ts +1 -1
- package/src/renderer/modal-factory.ts +1 -1
- package/src/renderer/model-picker-overlay.ts +1 -1
- package/src/renderer/model-workspace.ts +7 -2
- package/src/renderer/panel-composite.ts +2 -2
- package/src/renderer/panel-workspace-bar.ts +1 -1
- package/src/renderer/process-indicator.ts +1 -1
- package/src/renderer/selection-modal-overlay.ts +1 -1
- package/src/renderer/session-picker-modal.ts +2 -2
- package/src/renderer/settings-modal-helpers.ts +23 -5
- package/src/renderer/settings-modal.ts +129 -39
- package/src/renderer/shell-surface.ts +6 -0
- package/src/renderer/terminal-bg-probe.ts +1 -1
- package/src/renderer/theme.ts +3 -3
- package/src/renderer/tool-call.ts +13 -7
- package/src/renderer/tool-result-summary.ts +1 -1
- package/src/renderer/turn-injection.ts +7 -6
- package/src/renderer/ui-factory.ts +38 -1
- package/src/runtime/bootstrap-command-context.ts +7 -2
- package/src/runtime/bootstrap-command-parts.ts +9 -5
- package/src/runtime/bootstrap-core.ts +41 -41
- package/src/runtime/bootstrap-hook-bridge.ts +1 -1
- package/src/runtime/bootstrap-shell.ts +13 -5
- package/src/runtime/bootstrap.ts +34 -32
- package/src/runtime/channel-composition.ts +46 -0
- package/src/runtime/code-index-services.ts +3 -4
- package/src/runtime/daemon-attach-notices.ts +120 -0
- package/src/runtime/durability-services.ts +90 -0
- package/src/runtime/embedded-service-factories.ts +40 -0
- package/src/runtime/feature-settings.ts +147 -0
- package/src/runtime/fleet-services.ts +41 -0
- package/src/runtime/idle-power-services.ts +85 -0
- package/src/runtime/index.ts +6 -21
- package/src/runtime/interaction-seams.ts +109 -0
- package/src/runtime/legacy-daemon-migration.ts +29 -19
- package/src/runtime/memory-fold.ts +1 -1
- package/src/runtime/notification-dispatch.ts +110 -0
- package/src/runtime/onboarding/apply.ts +3 -29
- package/src/runtime/onboarding/fast-path.ts +39 -6
- package/src/runtime/onboarding/provider-key-capture.ts +126 -0
- package/src/runtime/onboarding/snapshot.ts +0 -1
- package/src/runtime/onboarding/types.ts +9 -3
- package/src/runtime/power-keepawake-remote.ts +75 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/provider-fallback.ts +55 -0
- package/src/runtime/provider-optimizer-wiring.ts +55 -0
- package/src/runtime/release-artifacts.ts +19 -103
- package/src/runtime/resume-notice.ts +1 -1
- package/src/runtime/services.ts +113 -113
- package/src/runtime/session-continuity-hints.ts +38 -0
- package/src/runtime/surface-feature-flags.ts +40 -9
- package/src/runtime/terminal-output-guard.ts +2 -2
- package/src/runtime/trust/workspace-registration.ts +10 -2
- package/src/runtime/trust/workspace-trust.ts +58 -31
- package/src/runtime/ui-services.ts +6 -1
- package/src/runtime/update-check.ts +15 -69
- package/src/runtime/workstream-services.ts +2 -2
- package/src/shell/blocking-input.ts +108 -132
- package/src/shell/recovery-input-helpers.ts +53 -166
- package/src/shell/ui-openers.ts +38 -6
- package/src/utils/format-duration.ts +1 -1
- package/src/verification/verification-ledger.ts +21 -4
- package/src/version.ts +1 -1
- package/src/input/commands/flags-runtime.ts +0 -328
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// fleet-observed-render.ts — the fleet-pane rows for observed FOREIGN coding
|
|
3
|
+
// agents (kind 'observed-external').
|
|
4
|
+
//
|
|
5
|
+
// goodvibes did NOT spawn these — they are externally-launched Claude Code /
|
|
6
|
+
// Codex / opencode sessions, detected read-only from OS signals. The row's
|
|
7
|
+
// FIRST job is visibility ("it's in claude or codex"): goodvibes never presents
|
|
8
|
+
// itself as the foreign session's cockpit. So an observed row:
|
|
9
|
+
// - shows its honest external kind + a liveness-derived state (active/quiet),
|
|
10
|
+
// - is NEVER counted in any of the panel's own-fleet counts,
|
|
11
|
+
// - carries NO stop affordance, ever,
|
|
12
|
+
// - offers steer as a DRILL-IN only (in the detail view), and only where a
|
|
13
|
+
// real channel exists; where steer.kind is 'none' the detail states why.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
import type { ProcessNode, ProcessObserved, ObservedAgentKind } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
|
|
17
|
+
import type { Line } from '../types/grid.ts';
|
|
18
|
+
import { buildPanelLine, buildSearchInputLine, DEFAULT_PANEL_PALETTE, type PanelPalette } from './polish.ts';
|
|
19
|
+
import { truncateDisplay, wrapText } from '../utils/terminal-width.ts';
|
|
20
|
+
|
|
21
|
+
/** An observed-external node carries its foreign-agent facts on `observed`. */
|
|
22
|
+
export type ObservedNode = ProcessNode & { readonly observed: ProcessObserved };
|
|
23
|
+
|
|
24
|
+
/** True for a foreign observed-agent node (never one of our own fleet rows). */
|
|
25
|
+
export function isObservedExternalNode(node: ProcessNode): node is ObservedNode {
|
|
26
|
+
return node.kind === 'observed-external' && node.observed !== undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The honest plain-language label for an observed foreign agent's kind. */
|
|
30
|
+
export function observedKindLabel(kind: ObservedAgentKind): string {
|
|
31
|
+
switch (kind) {
|
|
32
|
+
case 'claude-code': return 'Claude Code';
|
|
33
|
+
case 'codex': return 'Codex';
|
|
34
|
+
case 'opencode': return 'opencode';
|
|
35
|
+
default: return 'unknown agent';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Liveness glyph: a filled ring while CPU is advancing, a hollow one while quiet. */
|
|
40
|
+
function livenessGlyph(state: ProcessObserved['liveness']['state']): string {
|
|
41
|
+
return state === 'active' ? '◉' : '○';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* One observed-external row. Deliberately NOT the standard fleet row: no cost/
|
|
46
|
+
* token/steer-badge columns (a foreign session has no LLM turn we account for),
|
|
47
|
+
* no stop marker — just the glyph, an "observed" tag, the session label, its
|
|
48
|
+
* external kind, and its liveness state.
|
|
49
|
+
*/
|
|
50
|
+
export function renderObservedRowLine(node: ObservedNode, width: number, palette: PanelPalette = DEFAULT_PANEL_PALETTE): Line {
|
|
51
|
+
const C = palette;
|
|
52
|
+
const observed = node.observed;
|
|
53
|
+
const glyph = livenessGlyph(observed.liveness.state);
|
|
54
|
+
const kindLabel = observedKindLabel(observed.externalKind);
|
|
55
|
+
// The label is width-budgeted so the trailing "kind · state" tell never
|
|
56
|
+
// overflows the row (the tell is the honesty signal — it must survive).
|
|
57
|
+
const tell = `${kindLabel} · ${observed.liveness.state}`;
|
|
58
|
+
const labelBudget = Math.max(4, width - tell.length - 14);
|
|
59
|
+
return buildPanelLine(width, [
|
|
60
|
+
[' ', C.dim],
|
|
61
|
+
[glyph, observed.liveness.state === 'active' ? (C.info ?? DEFAULT_PANEL_PALETTE.info) : C.dim],
|
|
62
|
+
[' observed ', C.dim],
|
|
63
|
+
[truncateDisplay(node.label, labelBudget), C.value],
|
|
64
|
+
[' ', C.dim],
|
|
65
|
+
[tell, C.dim],
|
|
66
|
+
]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The detail block for a selected observed-external row. States the foreign
|
|
71
|
+
* facts (external kind, pid, cwd, liveness meaning), then the steer affordance —
|
|
72
|
+
* a DRILL-IN: where a real channel exists it names the action; where steer.kind
|
|
73
|
+
* is 'none' it states the honest reason. It NEVER shows a stop affordance —
|
|
74
|
+
* observing a foreign session is not owning its lifecycle.
|
|
75
|
+
*/
|
|
76
|
+
/**
|
|
77
|
+
* Render the observed-row detail. When `steerDraft` is a string the drill-in
|
|
78
|
+
* steer composer is open on THIS row: an active input line replaces the passive
|
|
79
|
+
* "s: steer" hint. A channel-less row never shows an input (owner ruling) — its
|
|
80
|
+
* detail states the honest reason and stops there.
|
|
81
|
+
*/
|
|
82
|
+
export function renderObservedDetailLines(node: ObservedNode, width: number, palette: PanelPalette = DEFAULT_PANEL_PALETTE, steerDraft: string | null = null): Line[] {
|
|
83
|
+
const C = palette;
|
|
84
|
+
const observed = node.observed;
|
|
85
|
+
const lines: Line[] = [];
|
|
86
|
+
lines.push(buildPanelLine(width, [
|
|
87
|
+
[' ', C.dim],
|
|
88
|
+
[livenessGlyph(observed.liveness.state), observed.liveness.state === 'active' ? (C.info ?? DEFAULT_PANEL_PALETTE.info) : C.dim],
|
|
89
|
+
[' observed ', C.dim],
|
|
90
|
+
[observedKindLabel(observed.externalKind), C.value],
|
|
91
|
+
[' pid ', C.label],
|
|
92
|
+
[String(observed.pid), C.value],
|
|
93
|
+
[' state ', C.label],
|
|
94
|
+
[observed.liveness.state, C.value],
|
|
95
|
+
]));
|
|
96
|
+
if (observed.cwd) {
|
|
97
|
+
lines.push(buildPanelLine(width, [[' cwd ', C.label], [truncateDisplay(observed.cwd, Math.max(0, width - 6)), C.dim]]));
|
|
98
|
+
}
|
|
99
|
+
// Liveness meaning, verbatim — honest about what 'quiet' can and cannot tell.
|
|
100
|
+
for (const segment of wrapText(observed.liveness.detail, Math.max(1, width - 2))) {
|
|
101
|
+
lines.push(buildPanelLine(width, [[' ', C.dim], [segment, C.dim]]));
|
|
102
|
+
}
|
|
103
|
+
// Steer is a drill-in: a real channel is named here (a message sent from this
|
|
104
|
+
// detail travels over it — fleet.observed.steer), and where none exists the
|
|
105
|
+
// honest reason is stated instead of a dead action. Wrapped so a long reason
|
|
106
|
+
// is never clipped at a narrow width.
|
|
107
|
+
const steerText = observed.steer.kind === 'tmux'
|
|
108
|
+
? `available via tmux pane ${observed.steer.paneId} (send-keys)`
|
|
109
|
+
: `unavailable — ${observed.steer.reason}`;
|
|
110
|
+
const steerFg = observed.steer.kind === 'tmux' ? (C.info ?? DEFAULT_PANEL_PALETTE.info) : C.dim;
|
|
111
|
+
const steerSegments = wrapText(steerText, Math.max(1, width - 8));
|
|
112
|
+
steerSegments.forEach((segment, i) => {
|
|
113
|
+
lines.push(buildPanelLine(width, [[i === 0 ? ' steer ' : ' ', C.label], [segment, steerFg]]));
|
|
114
|
+
});
|
|
115
|
+
// Drill-in compose input: only where a real channel exists. An active draft
|
|
116
|
+
// renders the input line + send/cancel hint; otherwise a discoverability hint.
|
|
117
|
+
if (observed.steer.kind === 'tmux') {
|
|
118
|
+
if (steerDraft !== null) {
|
|
119
|
+
lines.push(buildSearchInputLine(width, ' send ', `${steerDraft}_`, C, { active: true, bg: C.inputBg, valueColor: C.info }));
|
|
120
|
+
lines.push(buildPanelLine(width, [[' ', C.label], ['Enter: send Esc: cancel', C.dim]]));
|
|
121
|
+
} else {
|
|
122
|
+
lines.push(buildPanelLine(width, [[' ', C.label], ['s: steer this session', C.dim]]));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Visibility, not a cockpit: there is no stop here, by design.
|
|
126
|
+
lines.push(buildPanelLine(width, [[' stop ', C.label], ['not offered — goodvibes only observes this foreign session', C.dim]]));
|
|
127
|
+
return lines;
|
|
128
|
+
}
|
|
@@ -7,20 +7,23 @@
|
|
|
7
7
|
// renderItem/renderDetail call, with no `this` dependency.
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
9
9
|
|
|
10
|
-
import type { ProcessCostState, ProcessNode, ProcessUsage } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
|
|
10
|
+
import type { ProcessCostState, ProcessNode, ProcessReviewSummary, ProcessUsage } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
|
|
11
11
|
import type { Line } from '../types/grid.ts';
|
|
12
12
|
import { formatAgentCost } from './agent-inspector-shared.ts';
|
|
13
13
|
import { buildPanelLine, DEFAULT_PANEL_PALETTE, type ColumnSpec, type PanelPalette } from './polish.ts';
|
|
14
|
-
import { fleetUsageTokens, hasFleetCost, type FleetStateTone } from './fleet-read-model.ts';
|
|
14
|
+
import { fleetAttentionText, fleetNodeAttention, fleetStallMarker, fleetUsageTokens, hasFleetCost, type FleetStateTone } from './fleet-read-model.ts';
|
|
15
15
|
import { fleetStateDisplay } from './fleet-stop.ts';
|
|
16
16
|
import { formatElapsed } from '../utils/format-elapsed.ts';
|
|
17
17
|
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
18
|
-
import { formatWorkItemIsolationDetailFromRaw } from './fleet-panel-worktree-detail.ts';
|
|
18
|
+
import { conflictFilesFromRaw, formatWorkItemIsolationDetailFromRaw } from './fleet-panel-worktree-detail.ts';
|
|
19
|
+
import { wrapText } from '../utils/terminal-width.ts';
|
|
19
20
|
import { buildAlignedRow } from './polish.ts';
|
|
20
21
|
import { fleetKindTag } from './fleet-read-model.ts';
|
|
21
22
|
import { steerBadgeGlyph, steerBadgeTone } from './fleet-steer.ts';
|
|
22
23
|
import type { SteerBadge } from './fleet-tabs.ts';
|
|
23
24
|
import type { FleetTreeRow } from './fleet-read-model.ts';
|
|
25
|
+
import { isObservedExternalNode, renderObservedDetailLines, renderObservedRowLine } from './fleet-observed-render.ts';
|
|
26
|
+
import { renderPoolSummary, type WorkstreamGraphSnapshot } from './workstream-graph-render.ts';
|
|
24
27
|
|
|
25
28
|
// Column widths for the tree row layout. `label` absorbs whatever width is
|
|
26
29
|
// left over after the fixed columns + gaps; on hostile (narrow) widths the
|
|
@@ -88,6 +91,9 @@ export function renderFleetRowLine(
|
|
|
88
91
|
): Line {
|
|
89
92
|
const C = palette;
|
|
90
93
|
const node = row.node;
|
|
94
|
+
// Observed foreign agents render as their own visibility row (no cost/steer
|
|
95
|
+
// columns, no stop marker) — see fleet-observed-render.ts.
|
|
96
|
+
if (isObservedExternalNode(node)) return renderObservedRowLine(node, width, palette);
|
|
91
97
|
const disp = fleetStateDisplay(node.state, stopping, blocked);
|
|
92
98
|
const color = toneColor(disp.tone, C);
|
|
93
99
|
// Best-of-N grouping: badge a sibling attempt so the N candidates read as one
|
|
@@ -96,16 +102,39 @@ export function renderFleetRowLine(
|
|
|
96
102
|
? ` [attempt ${node.attemptGroup.index + 1}/${node.attemptGroup.total}${node.attemptGroup.held ? ', held' : ''}]`
|
|
97
103
|
: '';
|
|
98
104
|
const label = `${row.treePrefix}${node.label}${attemptBadge}`;
|
|
99
|
-
// Activity column doubles as the badge slot
|
|
100
|
-
//
|
|
105
|
+
// Activity column doubles as the badge slot, in priority order:
|
|
106
|
+
// stopping > waiting-on-human text ('blocked on you' / 'needs your input')
|
|
107
|
+
// > the read-model HEADLINE (derived from task/phase identity only —
|
|
108
|
+
// replaced in place, never a scrolling feed) with the stall marker
|
|
109
|
+
// ('quiet Nm') appended when the node has gone observably silent.
|
|
110
|
+
// Nodes without a headline fall back to the live currentActivity text.
|
|
111
|
+
const attention = fleetNodeAttention(node);
|
|
112
|
+
const stallMarker = fleetStallMarker(node);
|
|
113
|
+
const steadyText = node.headline?.text ?? node.currentActivity?.text ?? '';
|
|
114
|
+
// The stall marker must survive the 20-char activity cell: budget the
|
|
115
|
+
// headline around it (the marker is the honesty signal; a stalled node must
|
|
116
|
+
// never render as if it were healthily mid-headline).
|
|
117
|
+
const steadyWithStall = stallMarker
|
|
118
|
+
? (steadyText
|
|
119
|
+
? `${truncateDisplay(steadyText, Math.max(1, ACTIVITY_W - stallMarker.length - 3))} · ${stallMarker}`
|
|
120
|
+
: stallMarker)
|
|
121
|
+
: steadyText;
|
|
101
122
|
const activity = stopping
|
|
102
123
|
? disp.label
|
|
103
124
|
: blocked
|
|
104
|
-
?
|
|
125
|
+
? fleetAttentionText(attention ?? { reason: 'approval' })
|
|
105
126
|
: badge
|
|
106
|
-
? `${steerBadgeGlyph(badge.status)} ${
|
|
107
|
-
:
|
|
108
|
-
const activityColor = stopping
|
|
127
|
+
? `${steerBadgeGlyph(badge.status)} ${steadyWithStall}`.trimEnd()
|
|
128
|
+
: steadyWithStall;
|
|
129
|
+
const activityColor = stopping
|
|
130
|
+
? color
|
|
131
|
+
: blocked
|
|
132
|
+
? color
|
|
133
|
+
: badge
|
|
134
|
+
? steerBadgeTone(badge.status, C)
|
|
135
|
+
: stallMarker
|
|
136
|
+
? (C.warn ?? DEFAULT_PANEL_PALETTE.warn)
|
|
137
|
+
: C.dim;
|
|
109
138
|
|
|
110
139
|
return buildAlignedRow(
|
|
111
140
|
width,
|
|
@@ -130,16 +159,103 @@ export function renderFleetRowLine(
|
|
|
130
159
|
* 'state' text never claims a past-tense outcome mid-write, and a
|
|
131
160
|
* blocked-on-user node reads 'blocked on you' here too.
|
|
132
161
|
*/
|
|
162
|
+
/**
|
|
163
|
+
* The reviewer's acceptance checklist + verdict, from the fleet node's served
|
|
164
|
+
* `review` field (ProcessReviewSummary, rides fleet.snapshot/list). Rendered
|
|
165
|
+
* only when a review has completed — never an empty shell. Each item shows
|
|
166
|
+
* whether it was verified, the evidence, and (when present) how it was
|
|
167
|
+
* exercised, matching what the webui's review detail surfaces.
|
|
168
|
+
*/
|
|
169
|
+
export function renderReviewLines(review: ProcessReviewSummary, width: number, palette: PanelPalette = DEFAULT_PANEL_PALETTE): Line[] {
|
|
170
|
+
const C = palette;
|
|
171
|
+
const passTone = C.good ?? C.info;
|
|
172
|
+
const failTone = C.bad ?? C.warn ?? DEFAULT_PANEL_PALETTE.warn;
|
|
173
|
+
const cyclesLabel = review.cycles === 1 ? '1 cycle' : `${review.cycles} cycles`;
|
|
174
|
+
const lines: Line[] = [
|
|
175
|
+
buildPanelLine(width, [
|
|
176
|
+
[' review ', C.label],
|
|
177
|
+
[review.passed ? '✓ passed' : '✗ not passed', review.passed ? passTone : failTone],
|
|
178
|
+
[' score ', C.label], [String(review.score), C.value],
|
|
179
|
+
[' ', C.dim], [cyclesLabel, C.dim],
|
|
180
|
+
]),
|
|
181
|
+
];
|
|
182
|
+
if (review.checklist.length === 0) {
|
|
183
|
+
// Empty checklist is itself a gate failure — say so honestly, don't hide it.
|
|
184
|
+
lines.push(buildPanelLine(width, [[' ', C.dim], ['(the reviewer emitted no acceptance checklist — a gate failure)', failTone]]));
|
|
185
|
+
return lines;
|
|
186
|
+
}
|
|
187
|
+
for (const it of review.checklist) {
|
|
188
|
+
const mark = it.verified ? '[verified] ' : '[unverified]';
|
|
189
|
+
const markTone = it.verified ? passTone : failTone;
|
|
190
|
+
wrapText(it.item, Math.max(1, width - 16)).forEach((seg, i) => {
|
|
191
|
+
lines.push(buildPanelLine(width, i === 0
|
|
192
|
+
? [[' ', C.dim], [`${mark} `, markTone], [seg, C.value]]
|
|
193
|
+
: [[' ', C.dim], [seg, C.value]]));
|
|
194
|
+
});
|
|
195
|
+
if (it.evidence) {
|
|
196
|
+
for (const seg of wrapText(`evidence: ${it.evidence}`, Math.max(1, width - 6))) {
|
|
197
|
+
lines.push(buildPanelLine(width, [[' ', C.dim], [seg, C.dim]]));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (it.howExercised) {
|
|
201
|
+
for (const seg of wrapText(`exercised: ${it.howExercised}`, Math.max(1, width - 6))) {
|
|
202
|
+
lines.push(buildPanelLine(width, [[' ', C.dim], [seg, C.dim]]));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return lines;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The task graph's edges + elastic-pool posture for a workstream row, rendered
|
|
211
|
+
* IN the fleet detail (the tree shows nodes; edges/pool used to be reachable
|
|
212
|
+
* only via /graph — /graph still works). Fetched lazily and cached by fleet-acts;
|
|
213
|
+
* rendered only once a snapshot is in hand. An empty graph states so honestly.
|
|
214
|
+
*/
|
|
215
|
+
export function renderGraphPostureLines(graph: WorkstreamGraphSnapshot, width: number, palette: PanelPalette = DEFAULT_PANEL_PALETTE): Line[] {
|
|
216
|
+
const C = palette;
|
|
217
|
+
const lines: Line[] = [
|
|
218
|
+
buildPanelLine(width, [[' graph ', C.label], [renderPoolSummary(graph.pool), C.value]]),
|
|
219
|
+
];
|
|
220
|
+
if (graph.edges.length === 0) {
|
|
221
|
+
lines.push(buildPanelLine(width, [[' ', C.dim], ['no dependency edges', C.dim]]));
|
|
222
|
+
return lines;
|
|
223
|
+
}
|
|
224
|
+
const titleById = new Map(graph.nodes.map((n) => [n.id, n.title]));
|
|
225
|
+
lines.push(buildPanelLine(width, [[' edges ', C.label], [`${graph.edges.length} dependency link(s)`, C.value]]));
|
|
226
|
+
for (const edge of graph.edges) {
|
|
227
|
+
const from = titleById.get(edge.from) ?? edge.from;
|
|
228
|
+
const to = titleById.get(edge.to) ?? edge.to;
|
|
229
|
+
for (const seg of wrapText(`${from} → ${to}`, Math.max(1, width - 5))) {
|
|
230
|
+
lines.push(buildPanelLine(width, [[' ', C.dim], [seg, C.dim]]));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return lines;
|
|
234
|
+
}
|
|
235
|
+
|
|
133
236
|
export function renderFleetDetailLines(
|
|
134
237
|
node: ProcessNode,
|
|
135
238
|
width: number,
|
|
136
239
|
stopping: boolean,
|
|
137
240
|
blocked: boolean,
|
|
138
241
|
palette: PanelPalette = DEFAULT_PANEL_PALETTE,
|
|
242
|
+
observedSteerDraft: string | null = null,
|
|
243
|
+
graphSnapshot: WorkstreamGraphSnapshot | null = null,
|
|
139
244
|
): Line[] {
|
|
140
245
|
const C = palette;
|
|
246
|
+
// Observed foreign agents drill into their own detail (facts + steer-or-reason,
|
|
247
|
+
// never a stop) — see fleet-observed-render.ts. The drill-in steer draft (when
|
|
248
|
+
// the composer is open on this row) renders an input line in that detail.
|
|
249
|
+
if (isObservedExternalNode(node)) return renderObservedDetailLines(node, width, palette, observedSteerDraft);
|
|
141
250
|
const disp = fleetStateDisplay(node.state, stopping, blocked);
|
|
142
251
|
const color = toneColor(disp.tone, C);
|
|
252
|
+
// A blocked node's state text names WHY in the reason's own words
|
|
253
|
+
// ('needs your pick' / 'merge conflict waiting on you' / …) so the detail
|
|
254
|
+
// block never contradicts the row's reason-specific badge; the shared ⚑
|
|
255
|
+
// glyph/tone (disp) is unchanged. 'stopping' still wins over the reason.
|
|
256
|
+
const stateLabel = blocked && !stopping
|
|
257
|
+
? fleetAttentionText(fleetNodeAttention(node) ?? { reason: 'approval' })
|
|
258
|
+
: disp.label;
|
|
143
259
|
|
|
144
260
|
const line1 = buildPanelLine(width, [
|
|
145
261
|
[' ', C.dim],
|
|
@@ -148,7 +264,7 @@ export function renderFleetDetailLines(
|
|
|
148
264
|
[' id ', C.label],
|
|
149
265
|
[node.id, C.value],
|
|
150
266
|
[' state ', C.label],
|
|
151
|
-
[
|
|
267
|
+
[stateLabel, color],
|
|
152
268
|
[' elapsed ', C.label],
|
|
153
269
|
[formatElapsed(node.elapsedMs), C.value],
|
|
154
270
|
]);
|
|
@@ -160,6 +276,19 @@ export function renderFleetDetailLines(
|
|
|
160
276
|
[' cost ', C.label],
|
|
161
277
|
[formatFleetCost(node.costUsd, node.costState), C.value],
|
|
162
278
|
]);
|
|
279
|
+
// The headline (task/phase identity, replaced in place) gets its own row
|
|
280
|
+
// when present, with the stall marker appended — the detail block mirrors
|
|
281
|
+
// exactly what the tree row's steady slot shows.
|
|
282
|
+
const stallMarker = fleetStallMarker(node);
|
|
283
|
+
const headlineText = node.headline
|
|
284
|
+
? `${node.headline.text}${stallMarker ? ` · ${stallMarker}` : ''}`
|
|
285
|
+
: stallMarker;
|
|
286
|
+
const headlineLine = headlineText
|
|
287
|
+
? [buildPanelLine(width, [
|
|
288
|
+
[' headline ', C.label],
|
|
289
|
+
[truncateDisplay(headlineText, Math.max(0, width - 11)), stallMarker ? (C.warn ?? DEFAULT_PANEL_PALETTE.warn) : C.value],
|
|
290
|
+
])]
|
|
291
|
+
: [];
|
|
163
292
|
const activityText = node.currentActivity
|
|
164
293
|
? `${node.currentActivity.kind}: ${node.currentActivity.text}`
|
|
165
294
|
: '(no recent activity)';
|
|
@@ -170,5 +299,28 @@ export function renderFleetDetailLines(
|
|
|
170
299
|
// Approval history attaches here once session tabs land.
|
|
171
300
|
const line4 = buildPanelLine(width, [[' approvals ', C.label], ['—', C.dim]]);
|
|
172
301
|
const isolationDetail = node.kind === 'work-item' ? formatWorkItemIsolationDetailFromRaw(node.raw) : null;
|
|
173
|
-
|
|
302
|
+
// A merge-conflict row shows its STRUCTURED conflicting-path list, wrapped so a
|
|
303
|
+
// long path is fully readable — never clipped (STEP 4: the conflict row acts,
|
|
304
|
+
// and the operator sees exactly which files need resolving before pressing Enter).
|
|
305
|
+
const conflictFiles = node.kind === 'work-item' ? conflictFilesFromRaw(node.raw) : null;
|
|
306
|
+
const conflictLines: Line[] = conflictFiles
|
|
307
|
+
? [
|
|
308
|
+
buildPanelLine(width, [[' conflicts ', C.label], [`${conflictFiles.length} file(s) — press Enter to resolve`, C.warn ?? DEFAULT_PANEL_PALETTE.warn]]),
|
|
309
|
+
// Prefix is 3 spaces of indent + a 2-col bullet/continuation marker, so
|
|
310
|
+
// wrap the path at width-5 to keep the composed line within `width`
|
|
311
|
+
// (a hard-wrapped long path is fully readable, never clipped).
|
|
312
|
+
...conflictFiles.flatMap((file) =>
|
|
313
|
+
wrapText(file, Math.max(1, width - 5)).map((segment, i) =>
|
|
314
|
+
buildPanelLine(width, [[' ', C.dim], [i === 0 ? `• ${segment}` : ` ${segment}`, C.value]]),
|
|
315
|
+
),
|
|
316
|
+
),
|
|
317
|
+
]
|
|
318
|
+
: [];
|
|
319
|
+
// The reviewer's acceptance checklist + verdict, when a review has completed
|
|
320
|
+
// (served on node.review; absent before any review — never an empty shell).
|
|
321
|
+
const reviewLines = node.review ? renderReviewLines(node.review, width, C) : [];
|
|
322
|
+
// The task graph's edges/pool posture for a workstream row (fetched + cached
|
|
323
|
+
// by fleet-acts); rendered in-panel under the chain, /graph still available.
|
|
324
|
+
const graphLines = graphSnapshot ? renderGraphPostureLines(graphSnapshot, width, C) : [];
|
|
325
|
+
return [line1, line2, ...headlineLine, line3, line4, ...(isolationDetail ? [buildPanelLine(width, [[' isolation ', C.label], [isolationDetail, C.dim]])] : []), ...conflictLines, ...reviewLines, ...graphLines];
|
|
174
326
|
}
|
|
@@ -46,3 +46,17 @@ export function formatWorkItemIsolationDetailFromRaw(raw: unknown): string | nul
|
|
|
46
46
|
const item = (raw as { item?: WorkItem } | undefined)?.item;
|
|
47
47
|
return item ? formatWorkItemIsolationDetail(item) : null;
|
|
48
48
|
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The STRUCTURED conflicting-path list a merge-conflict work item recorded
|
|
52
|
+
* (WorkItem.conflictFiles), for the Fleet detail block to show a human exactly
|
|
53
|
+
* which files need resolving. Null when the node is not a conflicted work item
|
|
54
|
+
* or recorded no files. The caller wraps these (never clips) so a long path is
|
|
55
|
+
* always fully readable.
|
|
56
|
+
*/
|
|
57
|
+
export function conflictFilesFromRaw(raw: unknown): readonly string[] | null {
|
|
58
|
+
const item = (raw as { item?: WorkItem } | undefined)?.item;
|
|
59
|
+
if (!item || item.mergeState !== 'conflict') return null;
|
|
60
|
+
const files = item.conflictFiles;
|
|
61
|
+
return files && files.length > 0 ? files : null;
|
|
62
|
+
}
|