@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
package/src/panels/wrfc-panel.ts
DELETED
|
@@ -1,790 +0,0 @@
|
|
|
1
|
-
import type { Line } from '../types/grid.ts';
|
|
2
|
-
import type { WrfcChain, WrfcState, QualityGateResult } from '@pellux/goodvibes-sdk/platform/agents';
|
|
3
|
-
import type { WrfcController } from '@pellux/goodvibes-sdk/platform/agents';
|
|
4
|
-
import { BasePanel } from './base-panel.ts';
|
|
5
|
-
import type { WorkflowEvent } from '@/runtime/index.ts';
|
|
6
|
-
import type { UiEventFeed } from '../runtime/ui-events.ts';
|
|
7
|
-
import {
|
|
8
|
-
buildMeterLine,
|
|
9
|
-
buildPanelLine,
|
|
10
|
-
buildPanelWorkspace,
|
|
11
|
-
resolveScrollablePanelSection,
|
|
12
|
-
DEFAULT_PANEL_PALETTE,
|
|
13
|
-
type PanelWorkspaceSection,
|
|
14
|
-
buildSelectablePanelLine,
|
|
15
|
-
buildStyledPanelLine,
|
|
16
|
-
buildEmptyState,
|
|
17
|
-
} from './polish.ts';
|
|
18
|
-
import { getDisplayWidth } from '../utils/terminal-width.ts';
|
|
19
|
-
import {
|
|
20
|
-
type ConfirmState,
|
|
21
|
-
handleConfirmInput,
|
|
22
|
-
} from './confirm-state.ts';
|
|
23
|
-
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
24
|
-
import { formatDuration } from '../utils/format-duration.ts';
|
|
25
|
-
import { AgentInspectorPanel } from './agent-inspector-panel.ts';
|
|
26
|
-
import type { PanelIntegrationContext } from './types.ts';
|
|
27
|
-
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
// Constants
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
|
|
32
|
-
/** Chains in non-terminal state with no event for this long are shown as STALLED. */
|
|
33
|
-
const STALL_THRESHOLD_MS = 5 * 60 * 1000;
|
|
34
|
-
|
|
35
|
-
/** Terminal states — chains in these states cannot be resumed or cancelled. */
|
|
36
|
-
const TERMINAL_STATES: readonly WrfcState[] = ['passed', 'failed'];
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* States from which resume is permitted. These mirror the states
|
|
40
|
-
* WrfcController.resumeChain() actually recovers from: 'pending' (awaiting
|
|
41
|
-
* retry / start engineering), 'reviewing' and 'fixing' (re-spawn the
|
|
42
|
-
* interrupted child), and 'awaiting_gates' (re-run gates). resumeChain no-ops
|
|
43
|
-
* safely when a child is still running, so offering resume for these states is
|
|
44
|
-
* always safe — it is exactly the set an operator needs after a restart or a
|
|
45
|
-
* STALLED chain.
|
|
46
|
-
*/
|
|
47
|
-
const RESUMABLE_STATES: readonly WrfcState[] = ['pending', 'reviewing', 'fixing', 'awaiting_gates'];
|
|
48
|
-
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
50
|
-
// Colour palette + formatting helpers live in ./wrfc-panel-format.ts (extracted
|
|
51
|
-
// to keep this module under the architecture line-count cap). Re-exported here
|
|
52
|
-
// so ./wrfc-panel.ts remains their public import site.
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
import {
|
|
55
|
-
C,
|
|
56
|
-
sparkline,
|
|
57
|
-
stateColor,
|
|
58
|
-
stateLabel,
|
|
59
|
-
issueColor,
|
|
60
|
-
issuePrefix,
|
|
61
|
-
truncate,
|
|
62
|
-
constraintStatusMarker,
|
|
63
|
-
} from './wrfc-panel-format.ts';
|
|
64
|
-
export { sparkline, stateColor, stateLabel, truncate, constraintStatusMarker } from './wrfc-panel-format.ts';
|
|
65
|
-
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
// Panel
|
|
68
|
-
// ---------------------------------------------------------------------------
|
|
69
|
-
export interface WrfcPanelDeps {
|
|
70
|
-
readonly controller: Pick<WrfcController, 'listChains' | 'resumeChain'>;
|
|
71
|
-
/** Cancel the active agent for a chain by its ownerAgentId. Returns true if cancelled. */
|
|
72
|
-
readonly cancelChain: (agentId: string) => boolean;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export class WrfcPanel extends BasePanel {
|
|
76
|
-
private chains: WrfcChain[] = [];
|
|
77
|
-
private selectedIndex = 0;
|
|
78
|
-
private scrollOffset = 0;
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* The chain under the cursor. This panel owns its own selection state
|
|
82
|
-
* (`selectedIndex` navigates `this.chains` directly), so every selected-row
|
|
83
|
-
* read routes through this one accessor — indexing `this.chains` by the
|
|
84
|
-
* cursor directly is banned by the no-raw-selectedindex-read architecture rule.
|
|
85
|
-
*/
|
|
86
|
-
private selectedChain(): WrfcChain | undefined {
|
|
87
|
-
return this.chains.at(this.selectedIndex);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
private expandedChainIds = new Set<string>();
|
|
91
|
-
private unsubscribers: Array<() => void> = [];
|
|
92
|
-
/** Last event timestamp per chain id, for stall detection. */
|
|
93
|
-
private lastEventAt = new Map<string, number>();
|
|
94
|
-
/** Pending cancel confirmation: subject is the chain id to cancel. */
|
|
95
|
-
private confirmCancel: ConfirmState<string> | null = null;
|
|
96
|
-
/** Controller error seen after initialization (distinct from pre-init silence). */
|
|
97
|
-
private controllerError: string | null = null;
|
|
98
|
-
|
|
99
|
-
constructor(
|
|
100
|
-
private readonly workflowEvents: UiEventFeed<WorkflowEvent>,
|
|
101
|
-
private readonly deps: WrfcPanelDeps,
|
|
102
|
-
) {
|
|
103
|
-
super('wrfc', 'WRFC', 'W', 'agent');
|
|
104
|
-
this.subscribeToEvents();
|
|
105
|
-
this.syncFromController();
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// -------------------------------------------------------------------------
|
|
109
|
-
// Lifecycle
|
|
110
|
-
// -------------------------------------------------------------------------
|
|
111
|
-
override onActivate(): void {
|
|
112
|
-
super.onActivate();
|
|
113
|
-
this.syncFromController();
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
override onDestroy(): void {
|
|
117
|
-
for (const unsub of this.unsubscribers) unsub();
|
|
118
|
-
this.unsubscribers = [];
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// -------------------------------------------------------------------------
|
|
122
|
-
// Input
|
|
123
|
-
// -------------------------------------------------------------------------
|
|
124
|
-
handleInput(key: string): boolean {
|
|
125
|
-
// Confirm-cancel flow takes priority over all other keys.
|
|
126
|
-
// Enter and y both confirm; n, escape both cancel; any other key is absorbed.
|
|
127
|
-
if (this.confirmCancel) {
|
|
128
|
-
const confirmResult = handleConfirmInput(this.confirmCancel, key);
|
|
129
|
-
if (confirmResult === 'confirmed') {
|
|
130
|
-
const chain = this.chains.find(c => c.id === this.confirmCancel!.subject);
|
|
131
|
-
if (chain && !TERMINAL_STATES.includes(chain.state)) {
|
|
132
|
-
this.deps.cancelChain(chain.ownerAgentId);
|
|
133
|
-
}
|
|
134
|
-
this.confirmCancel = null;
|
|
135
|
-
this.markDirty();
|
|
136
|
-
return true;
|
|
137
|
-
}
|
|
138
|
-
if (confirmResult === 'cancelled') {
|
|
139
|
-
this.confirmCancel = null;
|
|
140
|
-
this.markDirty();
|
|
141
|
-
}
|
|
142
|
-
// absorbed: confirm stays pending, key swallowed
|
|
143
|
-
return true;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Normal key dispatch.
|
|
147
|
-
switch (key) {
|
|
148
|
-
case 'up': this.moveSelection(-1); return true;
|
|
149
|
-
case 'down': this.moveSelection(1); return true;
|
|
150
|
-
case 'return':
|
|
151
|
-
case 'enter': this.toggleExpanded(); return true;
|
|
152
|
-
case 'c': this.beginCancelConfirm(); return true;
|
|
153
|
-
case 'r': this.doResume(); return true;
|
|
154
|
-
// 'a' jumps to the selected chain's owner agent in the Inspector. The
|
|
155
|
-
// actual navigation happens in handlePanelIntegrationAction (it needs
|
|
156
|
-
// the PanelManager); consuming the key here just requires a selected
|
|
157
|
-
// chain to exist so the router below fires next.
|
|
158
|
-
case 'a': return this.selectedChain() !== undefined;
|
|
159
|
-
default: return false;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Cross-panel jump: 'a' on a selected chain opens the Inspector focused on
|
|
165
|
-
* that chain's owner agent (inspectAgent, reverse of the forward jump
|
|
166
|
-
* agent-detail-modal.ts makes from an agent record's wrfcId to its chain).
|
|
167
|
-
*/
|
|
168
|
-
handlePanelIntegrationAction(key: string, ctx: PanelIntegrationContext): boolean {
|
|
169
|
-
if (key !== 'a') return false;
|
|
170
|
-
const chain = this.selectedChain();
|
|
171
|
-
if (!chain) return false;
|
|
172
|
-
const inspector = ctx.panelManager.open('inspector');
|
|
173
|
-
if (inspector instanceof AgentInspectorPanel) {
|
|
174
|
-
inspector.inspectAgent(chain.ownerAgentId);
|
|
175
|
-
return true;
|
|
176
|
-
}
|
|
177
|
-
return false;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Mouse-wheel scroll: pan the chain list viewport without moving the
|
|
182
|
-
* selection cursor (BasePanel's default forwards wheel deltas into
|
|
183
|
-
* handleInput('up'/'down'), which would also move the selected row).
|
|
184
|
-
* scrollOffset is a hint consumed by resolveScrollablePanelSection's
|
|
185
|
-
* tracked window on the next render, which clamps it into range and keeps
|
|
186
|
-
* the selected row inside the guard band — so an over-large delta here is
|
|
187
|
-
* always safe.
|
|
188
|
-
*/
|
|
189
|
-
override handleScroll(deltaRows: number): boolean {
|
|
190
|
-
if (this.confirmCancel) return false;
|
|
191
|
-
const rows = Math.trunc(deltaRows);
|
|
192
|
-
if (rows === 0) return false;
|
|
193
|
-
const next = Math.max(0, this.scrollOffset + rows);
|
|
194
|
-
if (next === this.scrollOffset) return false;
|
|
195
|
-
this.scrollOffset = next;
|
|
196
|
-
this.markDirty();
|
|
197
|
-
return true;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Cross-panel jump target: select the chain by id and expand it. Used by
|
|
202
|
-
* WorkPlanPanel's 'w' jump key when an item links a wrfcId.
|
|
203
|
-
*/
|
|
204
|
-
selectChain(chainId: string): void {
|
|
205
|
-
const index = this.chains.findIndex((chain) => chain.id === chainId);
|
|
206
|
-
if (index < 0) return;
|
|
207
|
-
this.selectedIndex = index;
|
|
208
|
-
this.expandedChainIds.add(chainId);
|
|
209
|
-
this.markDirty();
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// -------------------------------------------------------------------------
|
|
213
|
-
// Render
|
|
214
|
-
// -------------------------------------------------------------------------
|
|
215
|
-
render(width: number, height: number): Line[] {
|
|
216
|
-
return this.trackedRender(() => {
|
|
217
|
-
const now = Date.now();
|
|
218
|
-
const activeCount = this.chains.filter(c => !TERMINAL_STATES.includes(c.state)).length;
|
|
219
|
-
const passedCount = this.chains.filter(c => c.state === 'passed').length;
|
|
220
|
-
const failedCount = this.chains.filter(c => c.state === 'failed').length;
|
|
221
|
-
|
|
222
|
-
if (this.chains.length === 0) {
|
|
223
|
-
const emptySections: PanelWorkspaceSection[] = [
|
|
224
|
-
{
|
|
225
|
-
lines: buildEmptyState(
|
|
226
|
-
width,
|
|
227
|
-
' No WRFC chains yet',
|
|
228
|
-
'WRFC chains appear here as review/fix cycles execute. Expanded rows show scores, gates, issues, and failure detail.',
|
|
229
|
-
[
|
|
230
|
-
{ command: '/teamwork create-mode local-engineer <task>', summary: 'run an engineer task through the write-review-fix-commit loop (reviewMode: wrfc)' },
|
|
231
|
-
{ command: '/teamwork modes', summary: 'list every teamwork mode and see which ones use reviewMode wrfc' },
|
|
232
|
-
],
|
|
233
|
-
DEFAULT_PANEL_PALETTE,
|
|
234
|
-
),
|
|
235
|
-
},
|
|
236
|
-
];
|
|
237
|
-
if (this.controllerError) {
|
|
238
|
-
emptySections.push({
|
|
239
|
-
lines: [
|
|
240
|
-
buildPanelLine(width, [
|
|
241
|
-
[' controller: ', DEFAULT_PANEL_PALETTE.dim],
|
|
242
|
-
[truncate(this.controllerError, width - 16), DEFAULT_PANEL_PALETTE.warn],
|
|
243
|
-
]),
|
|
244
|
-
],
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
return buildPanelWorkspace(width, height, {
|
|
248
|
-
title: ' WRFC Chain Monitor',
|
|
249
|
-
intro: 'Track WRFC engineering, review, fixing, gating, and final chain outcomes.',
|
|
250
|
-
sections: emptySections,
|
|
251
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const chainLines: Line[] = [];
|
|
256
|
-
let selectedLineIndex = 0;
|
|
257
|
-
for (let i = 0; i < this.chains.length; i++) {
|
|
258
|
-
const chain = this.chains[i];
|
|
259
|
-
const isSelected = i === this.selectedIndex;
|
|
260
|
-
const isExpanded = this.expandedChainIds.has(chain.id);
|
|
261
|
-
const rowBg = isSelected ? C.selected : '';
|
|
262
|
-
const rowFg = isSelected ? C.selectedFg : '';
|
|
263
|
-
const stalled = this.isStalled(chain, now);
|
|
264
|
-
|
|
265
|
-
if (isSelected) selectedLineIndex = chainLines.length;
|
|
266
|
-
chainLines.push(...this.renderChainRow(chain, width, isSelected, isExpanded, rowBg, rowFg, stalled, now));
|
|
267
|
-
|
|
268
|
-
if (isExpanded) {
|
|
269
|
-
chainLines.push(...this.renderChainDetail(chain, width, 12));
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const selectedChain = this.selectedChain();
|
|
274
|
-
const selectedLines: Line[] = selectedChain
|
|
275
|
-
? [
|
|
276
|
-
buildPanelLine(width, [
|
|
277
|
-
[' State ', DEFAULT_PANEL_PALETTE.label],
|
|
278
|
-
[stateLabel(selectedChain.state), stateColor(selectedChain.state)],
|
|
279
|
-
[TERMINAL_STATES.includes(selectedChain.state) ? ' Duration ' : ' Elapsed ', DEFAULT_PANEL_PALETTE.label],
|
|
280
|
-
[this.durationLabel(selectedChain, now), DEFAULT_PANEL_PALETTE.info],
|
|
281
|
-
]),
|
|
282
|
-
buildPanelLine(width, [
|
|
283
|
-
[' Task ', DEFAULT_PANEL_PALETTE.label],
|
|
284
|
-
[truncate(selectedChain.task, Math.max(8, width - 8)), DEFAULT_PANEL_PALETTE.value],
|
|
285
|
-
]),
|
|
286
|
-
buildPanelLine(width, [
|
|
287
|
-
[' Reviews ', DEFAULT_PANEL_PALETTE.label],
|
|
288
|
-
[String(selectedChain.reviewCycles), DEFAULT_PANEL_PALETTE.value],
|
|
289
|
-
[' Fixes ', DEFAULT_PANEL_PALETTE.label],
|
|
290
|
-
[String(selectedChain.fixAttempts), DEFAULT_PANEL_PALETTE.value],
|
|
291
|
-
[' Scores ', DEFAULT_PANEL_PALETTE.label],
|
|
292
|
-
[selectedChain.reviewScores.length > 0 ? selectedChain.reviewScores.map((score) => score.toFixed(0)).join(' -> ') : 'none', DEFAULT_PANEL_PALETTE.info],
|
|
293
|
-
]),
|
|
294
|
-
...(selectedChain.constraints.length > 0 ? [
|
|
295
|
-
buildPanelLine(width, (() => {
|
|
296
|
-
const total = selectedChain.constraints.length;
|
|
297
|
-
const findings = selectedChain.reviewerReport?.constraintFindings;
|
|
298
|
-
const satisfied = findings ? findings.filter(f => f.satisfied).length : 0;
|
|
299
|
-
const satFg = !findings || findings.length === 0
|
|
300
|
-
? DEFAULT_PANEL_PALETTE.dim
|
|
301
|
-
: satisfied === total ? C.good : C.bad;
|
|
302
|
-
return [
|
|
303
|
-
[' Constraints ', DEFAULT_PANEL_PALETTE.label],
|
|
304
|
-
[`${satisfied} sat / ${total} total`, satFg],
|
|
305
|
-
] as Array<[string, string]>;
|
|
306
|
-
})()),
|
|
307
|
-
] : []),
|
|
308
|
-
]
|
|
309
|
-
: [];
|
|
310
|
-
|
|
311
|
-
const totalChains = this.chains.length;
|
|
312
|
-
const stalledCount = this.chains.filter((c) => this.isStalled(c, now)).length;
|
|
313
|
-
const completedChains = passedCount + failedCount;
|
|
314
|
-
const meterWidth = Math.max(10, Math.min(24, width - 34));
|
|
315
|
-
const summarySection: PanelWorkspaceSection = {
|
|
316
|
-
title: 'Summary',
|
|
317
|
-
lines: [
|
|
318
|
-
buildPanelLine(width, [
|
|
319
|
-
[' Active ', DEFAULT_PANEL_PALETTE.label],
|
|
320
|
-
[String(activeCount), activeCount > 0 ? DEFAULT_PANEL_PALETTE.warn : DEFAULT_PANEL_PALETTE.dim],
|
|
321
|
-
[' Passed ', DEFAULT_PANEL_PALETTE.label],
|
|
322
|
-
[String(passedCount), DEFAULT_PANEL_PALETTE.good],
|
|
323
|
-
[' Failed ', DEFAULT_PANEL_PALETTE.label],
|
|
324
|
-
[String(failedCount), failedCount > 0 ? DEFAULT_PANEL_PALETTE.bad : DEFAULT_PANEL_PALETTE.dim],
|
|
325
|
-
[' Stalled ', DEFAULT_PANEL_PALETTE.label],
|
|
326
|
-
[String(stalledCount), stalledCount > 0 ? C.warn : DEFAULT_PANEL_PALETTE.dim],
|
|
327
|
-
]),
|
|
328
|
-
buildMeterLine(
|
|
329
|
-
width,
|
|
330
|
-
totalChains > 0 ? Math.round((completedChains / totalChains) * meterWidth) : 0,
|
|
331
|
-
meterWidth,
|
|
332
|
-
{ filled: failedCount > 0 ? DEFAULT_PANEL_PALETTE.bad : DEFAULT_PANEL_PALETTE.good, empty: C.border, label: DEFAULT_PANEL_PALETTE.label },
|
|
333
|
-
{ prefix: ' Resolved ', suffix: ` ${completedChains}/${totalChains} ` },
|
|
334
|
-
),
|
|
335
|
-
],
|
|
336
|
-
};
|
|
337
|
-
const selectedSection: PanelWorkspaceSection = {
|
|
338
|
-
title: 'Selected',
|
|
339
|
-
lines: selectedLines,
|
|
340
|
-
};
|
|
341
|
-
// Confirm-cancel overlay section.
|
|
342
|
-
const confirmSection: PanelWorkspaceSection | null = this.confirmCancel ? {
|
|
343
|
-
title: 'Confirm Cancel',
|
|
344
|
-
lines: [
|
|
345
|
-
buildPanelLine(width, [
|
|
346
|
-
[' Cancel chain "', DEFAULT_PANEL_PALETTE.warn],
|
|
347
|
-
[truncate(this.confirmCancel.label, Math.max(8, width - 32)), DEFAULT_PANEL_PALETTE.value],
|
|
348
|
-
['"?', DEFAULT_PANEL_PALETTE.warn],
|
|
349
|
-
]),
|
|
350
|
-
buildPanelLine(width, [
|
|
351
|
-
[' y', DEFAULT_PANEL_PALETTE.info], [' confirm', DEFAULT_PANEL_PALETTE.dim],
|
|
352
|
-
[' Enter', DEFAULT_PANEL_PALETTE.info], [' confirm', DEFAULT_PANEL_PALETTE.dim],
|
|
353
|
-
[' n / Esc', DEFAULT_PANEL_PALETTE.info], [' cancel', DEFAULT_PANEL_PALETTE.dim],
|
|
354
|
-
]),
|
|
355
|
-
],
|
|
356
|
-
} : null;
|
|
357
|
-
|
|
358
|
-
// Footer: show resume-disabled reason for the selected chain.
|
|
359
|
-
const selectedForFooter = this.selectedChain();
|
|
360
|
-
const resumeReason = selectedForFooter ? this.resumeDisabledReason(selectedForFooter) : null;
|
|
361
|
-
const footerLines: Line[] = [
|
|
362
|
-
buildPanelLine(width, [
|
|
363
|
-
[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim],
|
|
364
|
-
[' Enter', DEFAULT_PANEL_PALETTE.info], [' expand', DEFAULT_PANEL_PALETTE.dim],
|
|
365
|
-
[' c', DEFAULT_PANEL_PALETTE.info], [' cancel', DEFAULT_PANEL_PALETTE.dim],
|
|
366
|
-
[' r', resumeReason ? DEFAULT_PANEL_PALETTE.dim : DEFAULT_PANEL_PALETTE.info],
|
|
367
|
-
[resumeReason ? ` resume (${resumeReason})` : ' resume', DEFAULT_PANEL_PALETTE.dim],
|
|
368
|
-
[' a', DEFAULT_PANEL_PALETTE.info], [' jump to agent', DEFAULT_PANEL_PALETTE.dim],
|
|
369
|
-
]),
|
|
370
|
-
];
|
|
371
|
-
|
|
372
|
-
const chainsSection = resolveScrollablePanelSection(width, height, {
|
|
373
|
-
intro: 'Track WRFC engineering, review, fixing, gating, and final chain outcomes.',
|
|
374
|
-
footerLines,
|
|
375
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
376
|
-
beforeSections: [summarySection],
|
|
377
|
-
section: {
|
|
378
|
-
title: 'Chains',
|
|
379
|
-
scrollableLines: chainLines,
|
|
380
|
-
selectedIndex: selectedLineIndex,
|
|
381
|
-
scrollOffset: this.scrollOffset,
|
|
382
|
-
minRows: 8,
|
|
383
|
-
},
|
|
384
|
-
afterSections: confirmSection
|
|
385
|
-
? [selectedSection, confirmSection]
|
|
386
|
-
: [selectedSection],
|
|
387
|
-
});
|
|
388
|
-
this.scrollOffset = chainsSection.scrollOffset;
|
|
389
|
-
const sections: PanelWorkspaceSection[] = [
|
|
390
|
-
summarySection,
|
|
391
|
-
chainsSection.section,
|
|
392
|
-
selectedSection,
|
|
393
|
-
...(confirmSection ? [confirmSection] : []),
|
|
394
|
-
];
|
|
395
|
-
|
|
396
|
-
// Controller error section (post-init only).
|
|
397
|
-
if (this.controllerError) {
|
|
398
|
-
sections.push({
|
|
399
|
-
lines: [
|
|
400
|
-
buildPanelLine(width, [
|
|
401
|
-
[' controller: ', DEFAULT_PANEL_PALETTE.dim],
|
|
402
|
-
[truncate(this.controllerError, width - 16), DEFAULT_PANEL_PALETTE.warn],
|
|
403
|
-
]),
|
|
404
|
-
],
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
return buildPanelWorkspace(width, height, {
|
|
409
|
-
title: ' WRFC Chain Monitor',
|
|
410
|
-
intro: 'Track WRFC engineering, review, fixing, gating, and final chain outcomes.',
|
|
411
|
-
sections,
|
|
412
|
-
footerLines,
|
|
413
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
414
|
-
});
|
|
415
|
-
});
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
// -------------------------------------------------------------------------
|
|
420
|
-
// Rendering helpers
|
|
421
|
-
// -------------------------------------------------------------------------
|
|
422
|
-
private renderChainRow(
|
|
423
|
-
chain: WrfcChain,
|
|
424
|
-
width: number,
|
|
425
|
-
isSelected: boolean,
|
|
426
|
-
isExpanded: boolean,
|
|
427
|
-
bg: string,
|
|
428
|
-
fg: string,
|
|
429
|
-
stalled = false,
|
|
430
|
-
now: number = Date.now(),
|
|
431
|
-
): Line[] {
|
|
432
|
-
const stateCol = stalled ? C.warn : stateColor(chain.state);
|
|
433
|
-
const stateTag = stalled
|
|
434
|
-
? ` STALLED`
|
|
435
|
-
: ` ${stateLabel(chain.state).padEnd(6)}`;
|
|
436
|
-
const arrow = isExpanded ? '▾' : '▸';
|
|
437
|
-
const chainIdShort = chain.id.slice(-6);
|
|
438
|
-
const prefix = ` ${arrow} [${chainIdShort}] `;
|
|
439
|
-
const fixes = chain.fixAttempts > 0 ? ` fix:${chain.fixAttempts}` : '';
|
|
440
|
-
const cycles = chain.reviewCycles > 0 ? ` rev:${chain.reviewCycles}` : '';
|
|
441
|
-
const latestScore = chain.reviewScores.length > 0
|
|
442
|
-
? ` ${chain.reviewScores[chain.reviewScores.length - 1].toFixed(1)}/10`
|
|
443
|
-
: '';
|
|
444
|
-
const stalledBadge = stalled ? ' [STALLED]' : '';
|
|
445
|
-
const durationTag = ` ${this.durationLabel(chain, now)}`;
|
|
446
|
-
// Constraint badge: c:sat/total — only when constraints exist
|
|
447
|
-
let constraintBadge = '';
|
|
448
|
-
if (chain.constraints.length > 0) {
|
|
449
|
-
const total = chain.constraints.length;
|
|
450
|
-
const findings = chain.reviewerReport?.constraintFindings;
|
|
451
|
-
const satisfied = findings ? findings.filter(f => f.satisfied).length : 0;
|
|
452
|
-
constraintBadge = ` c:${satisfied}/${total}`;
|
|
453
|
-
}
|
|
454
|
-
const rightInfo = `${stalledBadge}${durationTag}${latestScore}${fixes}${cycles}${constraintBadge} `;
|
|
455
|
-
|
|
456
|
-
// Compute how much space the task text can use, then check if rightInfo fits.
|
|
457
|
-
// If the terminal is narrow and rightInfo would overflow, omit it entirely
|
|
458
|
-
// rather than producing corrupted layout.
|
|
459
|
-
const usedWithoutTask = getDisplayWidth(prefix) + getDisplayWidth(stateTag) + 1; // prefix + stateTag + space
|
|
460
|
-
const taskMax = width - usedWithoutTask - getDisplayWidth(rightInfo);
|
|
461
|
-
const taskText = truncate(chain.task, Math.max(8, taskMax));
|
|
462
|
-
const usedWidth = usedWithoutTask + getDisplayWidth(taskText);
|
|
463
|
-
const remaining = width - usedWidth;
|
|
464
|
-
|
|
465
|
-
const segments = [
|
|
466
|
-
{ text: prefix, fg: isSelected ? fg : C.label },
|
|
467
|
-
{ text: stateTag, fg: stateCol, bold: true },
|
|
468
|
-
{ text: ' ', fg: '' },
|
|
469
|
-
{ text: taskText, fg: isSelected ? fg : C.value },
|
|
470
|
-
];
|
|
471
|
-
if (remaining >= rightInfo.length + 1) {
|
|
472
|
-
// Right-align rightInfo in the remaining space
|
|
473
|
-
// Colour the constraint badge separately when present
|
|
474
|
-
if (chain.constraints.length > 0 && !isSelected) {
|
|
475
|
-
const total = chain.constraints.length;
|
|
476
|
-
const findings = chain.reviewerReport?.constraintFindings;
|
|
477
|
-
const satisfied = findings ? findings.filter(f => f.satisfied).length : 0;
|
|
478
|
-
// Determine badge colour
|
|
479
|
-
let badgeFg: string;
|
|
480
|
-
if (!findings || findings.length === 0) {
|
|
481
|
-
badgeFg = C.dim;
|
|
482
|
-
} else if (satisfied === total) {
|
|
483
|
-
badgeFg = C.good;
|
|
484
|
-
} else if (findings.some(f => !f.satisfied)) {
|
|
485
|
-
badgeFg = C.bad;
|
|
486
|
-
} else {
|
|
487
|
-
badgeFg = C.reviewing; // some unverified but none failed
|
|
488
|
-
}
|
|
489
|
-
// Split: everything before the badge, then the badge
|
|
490
|
-
const badgeText = ` c:${satisfied}/${total}`;
|
|
491
|
-
const beforeBadge = rightInfo.slice(0, rightInfo.length - badgeText.length - 1);
|
|
492
|
-
const padding = remaining - rightInfo.length;
|
|
493
|
-
segments.push({ text: beforeBadge.padStart(padding + beforeBadge.length), fg: isSelected ? fg : C.label });
|
|
494
|
-
segments.push({ text: badgeText, fg: badgeFg });
|
|
495
|
-
segments.push({ text: ' ', fg: '' });
|
|
496
|
-
} else {
|
|
497
|
-
segments.push({ text: rightInfo.padStart(remaining), fg: isSelected ? fg : C.label });
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
// else: no room — makeSegmentedLine will pad with spaces to fill width
|
|
501
|
-
|
|
502
|
-
const row = buildSelectablePanelLine(width, segments, {
|
|
503
|
-
selected: isSelected,
|
|
504
|
-
selectedBg: bg,
|
|
505
|
-
fillFg: isSelected ? fg : '',
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
return [row];
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
private renderChainDetail(chain: WrfcChain, width: number, maxLines: number): Line[] {
|
|
512
|
-
// Build the FULL detail content first (no per-section maxLines guards
|
|
513
|
-
// below), then apply one cap at the end so a truncation always ends with
|
|
514
|
-
// a visible "+N more" indicator instead of silently dropping content —
|
|
515
|
-
// the constraint sub-list keeps its own independent (+N more) cap since
|
|
516
|
-
// that is documented, unrelated badge/marker behavior (PRESERVE).
|
|
517
|
-
const lines: Line[] = [];
|
|
518
|
-
const indent = ' ';
|
|
519
|
-
|
|
520
|
-
// Score sparkline
|
|
521
|
-
if (chain.reviewScores.length > 0) {
|
|
522
|
-
const spark = sparkline(chain.reviewScores);
|
|
523
|
-
const latestScore = chain.reviewScores[chain.reviewScores.length - 1];
|
|
524
|
-
const sparkColor = latestScore >= 8 ? C.good : latestScore >= 5 ? C.info : C.bad;
|
|
525
|
-
lines.push(buildStyledPanelLine(width, [
|
|
526
|
-
{ text: `${indent}Scores `, fg: C.label },
|
|
527
|
-
{ text: spark, fg: sparkColor },
|
|
528
|
-
{ text: ` (${chain.reviewScores.map(s => s.toFixed(0)).join(' -> ')})`, fg: C.dim },
|
|
529
|
-
]));
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// Fix attempts + review cycles
|
|
533
|
-
if (chain.fixAttempts > 0 || chain.reviewCycles > 0) {
|
|
534
|
-
lines.push(buildStyledPanelLine(width, [
|
|
535
|
-
{ text: `${indent}Cycles `, fg: C.label },
|
|
536
|
-
{ text: `${chain.reviewCycles} review`, fg: C.value },
|
|
537
|
-
{ text: ' ', fg: '' },
|
|
538
|
-
{ text: `${chain.fixAttempts} fix`, fg: chain.fixAttempts > 0 ? C.fixing : C.value },
|
|
539
|
-
]));
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// Constraints section (between Cycles and Gates)
|
|
543
|
-
if (chain.constraints.length > 0) {
|
|
544
|
-
lines.push(buildStyledPanelLine(width, [{ text: `${indent}Constraints`, fg: C.label }]));
|
|
545
|
-
const MAX_CONSTRAINTS = 10;
|
|
546
|
-
const findings = chain.reviewerReport?.constraintFindings;
|
|
547
|
-
const displayed = chain.constraints.slice(0, MAX_CONSTRAINTS);
|
|
548
|
-
for (const constraint of displayed) {
|
|
549
|
-
const marker = constraintStatusMarker(constraint, findings);
|
|
550
|
-
const statusTag = marker.tag;
|
|
551
|
-
const rowPrefix = `${indent} ${statusTag} `;
|
|
552
|
-
const textMax = Math.max(8, width - rowPrefix.length);
|
|
553
|
-
const constraintText = truncate(constraint.text, textMax);
|
|
554
|
-
lines.push(buildStyledPanelLine(width, [
|
|
555
|
-
{ text: `${indent} `, fg: C.dim },
|
|
556
|
-
{ text: statusTag, fg: marker.fg, dim: marker.dim, bold: !marker.dim },
|
|
557
|
-
{ text: ' ', fg: '' },
|
|
558
|
-
{ text: constraintText, fg: C.value },
|
|
559
|
-
]));
|
|
560
|
-
}
|
|
561
|
-
const remaining = chain.constraints.length - MAX_CONSTRAINTS;
|
|
562
|
-
if (remaining > 0) {
|
|
563
|
-
lines.push(buildStyledPanelLine(width, [
|
|
564
|
-
{ text: `${indent} (+${remaining} more)`, fg: C.dim, dim: true },
|
|
565
|
-
]));
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
// Quality gate results
|
|
570
|
-
if (chain.gateResults && chain.gateResults.length > 0) {
|
|
571
|
-
lines.push(buildStyledPanelLine(width, [{ text: `${indent}Gates`, fg: C.label }]));
|
|
572
|
-
for (const gate of chain.gateResults) {
|
|
573
|
-
lines.push(this.renderGateResult(gate, width, indent + ' '));
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// Gate status (no results yet but state is gating)
|
|
578
|
-
if (chain.state === 'gating' || chain.state === 'awaiting_gates') {
|
|
579
|
-
if (!chain.gateResults || chain.gateResults.length === 0) {
|
|
580
|
-
lines.push(buildStyledPanelLine(width, [{ text: `${indent}Gates awaiting...`, fg: C.gating, dim: true }]));
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// Synthetic issues injected by controller (continuity violations)
|
|
585
|
-
if (chain.syntheticIssues && chain.syntheticIssues.length > 0) {
|
|
586
|
-
lines.push(buildStyledPanelLine(width, [{ text: `${indent}Controller flags`, fg: C.issueCrit, bold: true }]));
|
|
587
|
-
for (const synthetic of chain.syntheticIssues) {
|
|
588
|
-
const prefix = `${indent} [CRIT] `;
|
|
589
|
-
const descMax = Math.max(8, width - prefix.length);
|
|
590
|
-
const desc = truncate(synthetic.description, descMax);
|
|
591
|
-
lines.push(buildStyledPanelLine(width, [
|
|
592
|
-
{ text: prefix, fg: C.issueCrit, bold: true },
|
|
593
|
-
{ text: desc, fg: C.value },
|
|
594
|
-
]));
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
// Issues from reviewer
|
|
599
|
-
const issues = chain.reviewerReport?.issues ?? [];
|
|
600
|
-
if (issues.length > 0) {
|
|
601
|
-
lines.push(buildStyledPanelLine(width, [{ text: `${indent}Issues`, fg: C.label }]));
|
|
602
|
-
for (const issue of issues) {
|
|
603
|
-
const prefix = `${indent} ${issuePrefix(issue.severity)}`;
|
|
604
|
-
const descMax = width - prefix.length;
|
|
605
|
-
const desc = truncate(issue.description, Math.max(8, descMax));
|
|
606
|
-
lines.push(buildStyledPanelLine(width, [
|
|
607
|
-
{ text: prefix, fg: issueColor(issue.severity), bold: issue.severity === 'critical' },
|
|
608
|
-
{ text: desc, fg: C.value },
|
|
609
|
-
]));
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// Error
|
|
614
|
-
if (chain.error) {
|
|
615
|
-
const errPrefix = `${indent}Error `;
|
|
616
|
-
lines.push(buildStyledPanelLine(width, [
|
|
617
|
-
{ text: errPrefix, fg: C.failed, bold: true },
|
|
618
|
-
{ text: truncate(chain.error, width - errPrefix.length), fg: C.value },
|
|
619
|
-
]));
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// Divider after expanded section
|
|
623
|
-
lines.push(buildStyledPanelLine(width, [{ text: ' ' + '-'.repeat(Math.max(0, width - 4)) + ' ', fg: C.border, dim: true }]));
|
|
624
|
-
|
|
625
|
-
if (lines.length <= maxLines) return lines;
|
|
626
|
-
const visible = lines.slice(0, Math.max(0, maxLines - 1));
|
|
627
|
-
const hidden = lines.length - visible.length;
|
|
628
|
-
visible.push(buildStyledPanelLine(width, [
|
|
629
|
-
{ text: `${indent}+${hidden} more (press a to inspect the owner agent for full detail)`, fg: C.dim, dim: true },
|
|
630
|
-
]));
|
|
631
|
-
return visible;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
private renderGateResult(gate: QualityGateResult, width: number, indent: string): Line {
|
|
635
|
-
const icon = gate.passed ? '✓' : '✕';
|
|
636
|
-
const iconFg = gate.passed ? C.good : C.bad;
|
|
637
|
-
const dur = `${gate.durationMs}ms`;
|
|
638
|
-
const nameMax = width - indent.length - 2 - dur.length - 4;
|
|
639
|
-
const name = truncate(gate.gate, Math.max(8, nameMax));
|
|
640
|
-
|
|
641
|
-
return buildStyledPanelLine(width, [
|
|
642
|
-
{ text: indent, fg: C.dim },
|
|
643
|
-
{ text: `${icon} `, fg: iconFg, bold: true },
|
|
644
|
-
{ text: name, fg: gate.passed ? C.value : C.failed },
|
|
645
|
-
{ text: ` (${dur})`, fg: C.dim },
|
|
646
|
-
]);
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
// -------------------------------------------------------------------------
|
|
650
|
-
// Navigation
|
|
651
|
-
// -------------------------------------------------------------------------
|
|
652
|
-
private moveSelection(delta: number): void {
|
|
653
|
-
if (this.chains.length === 0) return;
|
|
654
|
-
this.selectedIndex = Math.max(0, Math.min(this.chains.length - 1, this.selectedIndex + delta));
|
|
655
|
-
this.markDirty();
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
private toggleExpanded(): void {
|
|
659
|
-
const chain = this.selectedChain();
|
|
660
|
-
if (!chain) return;
|
|
661
|
-
if (this.expandedChainIds.has(chain.id)) {
|
|
662
|
-
this.expandedChainIds.delete(chain.id);
|
|
663
|
-
} else {
|
|
664
|
-
this.expandedChainIds.add(chain.id);
|
|
665
|
-
}
|
|
666
|
-
this.markDirty();
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
// -------------------------------------------------------------------------
|
|
670
|
-
// Event subscriptions
|
|
671
|
-
// -------------------------------------------------------------------------
|
|
672
|
-
private subscribeToEvents(): void {
|
|
673
|
-
const refreshWithTimestamp = (chainId?: string) => {
|
|
674
|
-
if (chainId) this.lastEventAt.set(chainId, Date.now());
|
|
675
|
-
this.syncFromController();
|
|
676
|
-
this.markDirty();
|
|
677
|
-
};
|
|
678
|
-
|
|
679
|
-
// Each workflow event carries the chainId in its payload — record it for
|
|
680
|
-
// stall tracking. The handler signature is (event) => void; we extract
|
|
681
|
-
// chainId where present using a narrow type guard so we never guess.
|
|
682
|
-
const timestamped = (e: WorkflowEvent) => {
|
|
683
|
-
const chainId = 'chainId' in e && typeof e.chainId === 'string' ? e.chainId : undefined;
|
|
684
|
-
refreshWithTimestamp(chainId);
|
|
685
|
-
};
|
|
686
|
-
|
|
687
|
-
this.unsubscribers.push(
|
|
688
|
-
this.workflowEvents.on('WORKFLOW_CHAIN_CREATED', timestamped),
|
|
689
|
-
this.workflowEvents.on('WORKFLOW_STATE_CHANGED', timestamped),
|
|
690
|
-
this.workflowEvents.on('WORKFLOW_REVIEW_COMPLETED', timestamped),
|
|
691
|
-
this.workflowEvents.on('WORKFLOW_FIX_ATTEMPTED', timestamped),
|
|
692
|
-
this.workflowEvents.on('WORKFLOW_GATE_RESULT', timestamped),
|
|
693
|
-
this.workflowEvents.on('WORKFLOW_CHAIN_PASSED', timestamped),
|
|
694
|
-
this.workflowEvents.on('WORKFLOW_CHAIN_FAILED', timestamped),
|
|
695
|
-
this.workflowEvents.on('WORKFLOW_AUTO_COMMITTED', timestamped),
|
|
696
|
-
this.workflowEvents.on('WORKFLOW_CASCADE_ABORTED', timestamped),
|
|
697
|
-
this.workflowEvents.on('WORKFLOW_CONSTRAINTS_ENUMERATED', timestamped),
|
|
698
|
-
);
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
private syncFromController(): void {
|
|
702
|
-
// Distinguish two failure modes:
|
|
703
|
-
// 1. Controller not yet initialized — chains is empty Map, listChains
|
|
704
|
-
// returns [] with no error. This is the normal pre-ready path.
|
|
705
|
-
// 2. Controller throws after initialization — an actual error we surface.
|
|
706
|
-
// We detect (2) by checking whether we previously had chains: if chains
|
|
707
|
-
// were present and listChains now throws, that is a post-init error.
|
|
708
|
-
const hadChains = this.chains.length > 0;
|
|
709
|
-
try {
|
|
710
|
-
const all = this.deps.controller.listChains();
|
|
711
|
-
this.controllerError = null;
|
|
712
|
-
|
|
713
|
-
// Sort: active first (by createdAt desc), then completed
|
|
714
|
-
const active = all.filter(c => !TERMINAL_STATES.includes(c.state));
|
|
715
|
-
const done = all.filter(c => TERMINAL_STATES.includes(c.state));
|
|
716
|
-
active.sort((a, b) => b.createdAt - a.createdAt);
|
|
717
|
-
done.sort( (a, b) => (b.completedAt ?? 0) - (a.completedAt ?? 0));
|
|
718
|
-
this.chains = [...active, ...done];
|
|
719
|
-
|
|
720
|
-
// Clamp selection
|
|
721
|
-
if (this.chains.length > 0) {
|
|
722
|
-
this.selectedIndex = Math.min(this.selectedIndex, this.chains.length - 1);
|
|
723
|
-
}
|
|
724
|
-
} catch (err) {
|
|
725
|
-
if (hadChains) {
|
|
726
|
-
// Post-init error: the controller was working and now throws. Surface
|
|
727
|
-
// a faint diagnostic rather than silently preserving stale state.
|
|
728
|
-
const msg = summarizeError(err);
|
|
729
|
-
this.controllerError = msg;
|
|
730
|
-
console.debug('[WrfcPanel] controller.listChains() error post-init:', msg);
|
|
731
|
-
}
|
|
732
|
-
// Pre-init: controller not ready yet, leave chain list empty (no error).
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
// -------------------------------------------------------------------------
|
|
737
|
-
// Cancel / resume actions
|
|
738
|
-
// -------------------------------------------------------------------------
|
|
739
|
-
|
|
740
|
-
/** Initiate cancel-confirm flow for the selected chain (noop if terminal). */
|
|
741
|
-
private beginCancelConfirm(): void {
|
|
742
|
-
const chain = this.selectedChain();
|
|
743
|
-
if (!chain || TERMINAL_STATES.includes(chain.state)) return;
|
|
744
|
-
this.confirmCancel = {
|
|
745
|
-
subject: chain.id,
|
|
746
|
-
label: truncate(chain.task, 40),
|
|
747
|
-
};
|
|
748
|
-
this.markDirty();
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
/**
|
|
752
|
-
* Resume the selected chain via the controller.
|
|
753
|
-
* Only permitted when the chain state is in RESUMABLE_STATES.
|
|
754
|
-
* Emits a visible noop reason when the chain is not resumable.
|
|
755
|
-
*/
|
|
756
|
-
private doResume(): void {
|
|
757
|
-
const chain = this.selectedChain();
|
|
758
|
-
if (!chain) return;
|
|
759
|
-
if (!RESUMABLE_STATES.includes(chain.state)) return;
|
|
760
|
-
this.deps.controller.resumeChain(chain.id);
|
|
761
|
-
// Re-sync so the rows/footer reflect the state resumeChain transitioned to.
|
|
762
|
-
this.syncFromController();
|
|
763
|
-
this.markDirty();
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
/** Returns a human-readable reason why resume is disabled for a chain, or null if it is allowed. */
|
|
767
|
-
private resumeDisabledReason(chain: WrfcChain): string | null {
|
|
768
|
-
if (TERMINAL_STATES.includes(chain.state)) return 'chain is complete';
|
|
769
|
-
if (!RESUMABLE_STATES.includes(chain.state)) return `active (${stateLabel(chain.state)})`;
|
|
770
|
-
return null;
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
/** Returns whether a chain is considered stalled (non-terminal, no recent event). */
|
|
774
|
-
private isStalled(chain: WrfcChain, now: number): boolean {
|
|
775
|
-
if (TERMINAL_STATES.includes(chain.state)) return false;
|
|
776
|
-
const last = this.lastEventAt.get(chain.id) ?? chain.createdAt;
|
|
777
|
-
return (now - last) >= STALL_THRESHOLD_MS;
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
/**
|
|
781
|
-
* Elapsed time since createdAt for an active chain, or total duration from
|
|
782
|
-
* createdAt to completedAt for a terminal one. createdAt/completedAt are
|
|
783
|
-
* already consumed for chain sort order (syncFromController) — this just
|
|
784
|
-
* surfaces the same fields as a human-readable label.
|
|
785
|
-
*/
|
|
786
|
-
private durationLabel(chain: WrfcChain, now: number): string {
|
|
787
|
-
const end = chain.completedAt ?? now;
|
|
788
|
-
return formatDuration(Math.max(0, end - chain.createdAt));
|
|
789
|
-
}
|
|
790
|
-
}
|