@pellux/goodvibes-tui 0.27.0 → 0.29.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 +124 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation.ts +4 -6
- package/src/core/session-recovery.ts +2 -0
- package/src/core/turn-event-wiring.ts +1 -0
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-feed-routes.ts +60 -26
- package/src/input/handler-feed.ts +17 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler-ui-state.ts +4 -0
- package/src/input/handler.ts +43 -6
- package/src/input/keybindings.ts +53 -8
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +117 -1
- package/src/main.ts +5 -2
- package/src/panels/agent-inspector-panel.ts +139 -45
- package/src/panels/agent-inspector-shared.ts +147 -3
- package/src/panels/approval-panel.ts +204 -102
- package/src/panels/automation-control-panel.ts +370 -103
- package/src/panels/base-panel.ts +26 -2
- package/src/panels/builtin/agent.ts +28 -37
- package/src/panels/builtin/development.ts +40 -32
- package/src/panels/builtin/knowledge.ts +15 -6
- package/src/panels/builtin/operations.ts +178 -117
- package/src/panels/builtin/session.ts +35 -9
- package/src/panels/builtin/shared.ts +98 -6
- package/src/panels/cockpit-panel.ts +253 -75
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +159 -56
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +354 -95
- package/src/panels/cost-tracker-panel.ts +243 -57
- package/src/panels/debug-panel.ts +347 -159
- package/src/panels/diff-panel.ts +257 -82
- package/src/panels/docs-panel.ts +166 -64
- package/src/panels/eval-panel.ts +336 -109
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +238 -159
- package/src/panels/file-preview-panel.ts +141 -59
- package/src/panels/git-panel.ts +360 -191
- package/src/panels/hooks-panel.ts +181 -19
- package/src/panels/incident-review-panel.ts +252 -43
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +310 -103
- package/src/panels/knowledge-graph-panel.ts +480 -58
- package/src/panels/local-auth-panel.ts +251 -33
- package/src/panels/marketplace-panel.ts +219 -33
- package/src/panels/memory-panel.ts +83 -81
- package/src/panels/ops-control-panel.ts +211 -32
- package/src/panels/ops-strategy-panel.ts +131 -45
- package/src/panels/orchestration-panel.ts +259 -67
- package/src/panels/panel-list-panel.ts +184 -136
- package/src/panels/panel-manager.ts +120 -13
- package/src/panels/plan-dashboard-panel.ts +507 -74
- package/src/panels/plugins-panel.ts +227 -34
- package/src/panels/policy-panel.ts +264 -55
- package/src/panels/polish-core.ts +162 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +67 -149
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +84 -113
- package/src/panels/provider-health-panel.ts +438 -480
- package/src/panels/provider-health-routes.ts +203 -0
- package/src/panels/provider-health-tracker.ts +194 -6
- package/src/panels/provider-health-views.ts +560 -0
- package/src/panels/qr-panel.ts +136 -38
- package/src/panels/remote-panel.ts +123 -38
- package/src/panels/routes-panel.ts +87 -24
- package/src/panels/sandbox-panel.ts +232 -65
- package/src/panels/scrollable-list-panel.ts +143 -145
- package/src/panels/security-panel.ts +204 -52
- package/src/panels/services-panel.ts +174 -76
- package/src/panels/session-browser-panel.ts +101 -6
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +346 -68
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +105 -34
- package/src/panels/symbol-outline-panel.ts +287 -154
- package/src/panels/system-messages-panel.ts +172 -38
- package/src/panels/tasks-panel.ts +348 -134
- package/src/panels/thinking-panel.ts +66 -32
- package/src/panels/token-budget-panel.ts +233 -46
- package/src/panels/tool-inspector-panel.ts +170 -54
- package/src/panels/types.ts +65 -4
- package/src/panels/work-plan-panel.ts +393 -39
- package/src/panels/worktree-panel.ts +239 -61
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +146 -150
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +22 -16
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +30 -11
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/overlay-box.ts +16 -10
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/process-indicator.ts +3 -1
- package/src/renderer/progress.ts +8 -6
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +41 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/ui-factory.ts +45 -36
- package/src/renderer/ui-primitives.ts +23 -2
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/utils/splash-lines.ts +44 -3
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -666
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -214
- package/src/panels/forensics-panel.ts +0 -364
- package/src/panels/panel-picker.ts +0 -106
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -218
- package/src/panels/provider-stats-panel.ts +0 -366
- package/src/panels/schedule-panel.ts +0 -342
- package/src/panels/watchers-panel.ts +0 -193
- package/src/renderer/panel-picker-overlay.ts +0 -202
- package/src/renderer/panel-tab-bar.ts +0 -69
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -1,16 +1,40 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
|
-
import type { ExecutionPlan, PlanItem, PlanItemStatus } from '@pellux/goodvibes-sdk/platform/core';
|
|
2
|
+
import type { ExecutionPlan, ExecutionPlanManager, PlanItem, PlanItemStatus } from '@pellux/goodvibes-sdk/platform/core';
|
|
3
|
+
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
3
4
|
import { BasePanel } from './base-panel.ts';
|
|
4
|
-
import
|
|
5
|
+
import { AgentInspectorPanel } from './agent-inspector-panel.ts';
|
|
6
|
+
import type { PanelIntegrationContext } from './types.ts';
|
|
7
|
+
import type { WorkflowEvent } from '@/runtime/index.ts';
|
|
8
|
+
import type { UiEventFeed } from '../runtime/ui-events.ts';
|
|
5
9
|
import {
|
|
6
10
|
buildEmptyState,
|
|
11
|
+
buildKeyboardHints,
|
|
12
|
+
buildKeyValueLine,
|
|
13
|
+
buildMeterLine,
|
|
7
14
|
buildPanelLine,
|
|
8
15
|
buildPanelWorkspace,
|
|
16
|
+
buildSelectablePanelLine,
|
|
9
17
|
resolveScrollablePanelSection,
|
|
10
18
|
DEFAULT_PANEL_PALETTE,
|
|
19
|
+
extendPalette,
|
|
11
20
|
type PanelWorkspaceSection,
|
|
12
21
|
} from './polish.ts';
|
|
13
22
|
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Palette (no inline hex in render bodies)
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
28
|
+
complete: '#22c55e',
|
|
29
|
+
active: '#00ffff',
|
|
30
|
+
pending: '#94a3b8',
|
|
31
|
+
failed: '#ef4444',
|
|
32
|
+
skipped: '#64748b',
|
|
33
|
+
blocked: '#f97316',
|
|
34
|
+
selectBg: '#1e293b',
|
|
35
|
+
phase: '#cbd5e1',
|
|
36
|
+
});
|
|
37
|
+
|
|
14
38
|
// ---------------------------------------------------------------------------
|
|
15
39
|
// Status display maps
|
|
16
40
|
// ---------------------------------------------------------------------------
|
|
@@ -24,13 +48,28 @@ const STATUS_ICON: Record<PlanItemStatus, string> = {
|
|
|
24
48
|
};
|
|
25
49
|
|
|
26
50
|
const STATUS_FG: Record<PlanItemStatus, string> = {
|
|
27
|
-
complete:
|
|
28
|
-
in_progress:
|
|
29
|
-
pending:
|
|
30
|
-
failed:
|
|
31
|
-
skipped:
|
|
51
|
+
complete: C.complete,
|
|
52
|
+
in_progress: C.active,
|
|
53
|
+
pending: C.pending,
|
|
54
|
+
failed: C.failed,
|
|
55
|
+
skipped: C.skipped,
|
|
32
56
|
};
|
|
33
57
|
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Deps
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The subset of ExecutionPlanManager the dashboard needs: the live active
|
|
64
|
+
* plan (getActive) plus history/switching support (list/getSummary/
|
|
65
|
+
* toMarkdown — the same trio /plan list|show consume, planning-runtime.ts:
|
|
66
|
+
* 140-165) so the panel can browse and view any plan on disk, not just the
|
|
67
|
+
* currently active one.
|
|
68
|
+
*/
|
|
69
|
+
export interface PlanDashboardPanelDeps {
|
|
70
|
+
readonly planManager: Pick<ExecutionPlanManager, 'getActive' | 'list' | 'getSummary' | 'toMarkdown'>;
|
|
71
|
+
}
|
|
72
|
+
|
|
34
73
|
// ---------------------------------------------------------------------------
|
|
35
74
|
// PlanDashboardPanel
|
|
36
75
|
// ---------------------------------------------------------------------------
|
|
@@ -41,11 +80,40 @@ export class PlanDashboardPanel extends BasePanel {
|
|
|
41
80
|
|
|
42
81
|
// Flat list of navigable row indices (set during render)
|
|
43
82
|
private totalRows = 0;
|
|
44
|
-
|
|
83
|
+
// Item flat-list parallel to navigable rows (set during render) for detail.
|
|
84
|
+
private navItems: PlanItem[] = [];
|
|
85
|
+
private blockedItemIds = new Set<string>();
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The plan item under the cursor. This panel owns its own selection state
|
|
89
|
+
* (`selectedIndex` navigates the `this.navItems` flat list directly), so
|
|
90
|
+
* every selected-row read routes through this one accessor — indexing
|
|
91
|
+
* `this.navItems` by the cursor directly is banned by the
|
|
92
|
+
* no-raw-selectedindex-read architecture rule.
|
|
93
|
+
*/
|
|
94
|
+
private selectedNavItem(): PlanItem | undefined {
|
|
95
|
+
return this.navItems.at(this.selectedIndex);
|
|
96
|
+
}
|
|
45
97
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
98
|
+
// Plan history / switching (planManager.list()/getSummary()/toMarkdown()).
|
|
99
|
+
// `viewingPlanId` is null while following the live active plan; once set,
|
|
100
|
+
// the dashboard renders that specific plan from disk instead.
|
|
101
|
+
private viewingPlanId: string | null = null;
|
|
102
|
+
private historyMode = false;
|
|
103
|
+
private historyPlans: ExecutionPlan[] = [];
|
|
104
|
+
private historySelectedIndex = 0;
|
|
105
|
+
private historyScrollOffset = 0;
|
|
106
|
+
|
|
107
|
+
// Live-refresh subscriptions (WrfcPanel pattern: subscribe ui.events.workflows
|
|
108
|
+
// so the dashboard updates during a run without requiring a keypress).
|
|
109
|
+
private unsubscribers: Array<() => void> = [];
|
|
110
|
+
|
|
111
|
+
constructor(
|
|
112
|
+
private readonly workflowEvents: UiEventFeed<WorkflowEvent>,
|
|
113
|
+
private readonly deps: PlanDashboardPanelDeps,
|
|
114
|
+
) {
|
|
115
|
+
super('plan', 'Plan', '▤', 'agent');
|
|
116
|
+
this.subscribeToEvents();
|
|
49
117
|
}
|
|
50
118
|
|
|
51
119
|
override onActivate(): void {
|
|
@@ -54,11 +122,52 @@ export class PlanDashboardPanel extends BasePanel {
|
|
|
54
122
|
this.markDirty();
|
|
55
123
|
}
|
|
56
124
|
|
|
125
|
+
override onDestroy(): void {
|
|
126
|
+
for (const unsub of this.unsubscribers) unsub();
|
|
127
|
+
this.unsubscribers = [];
|
|
128
|
+
super.onDestroy();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// --------------------------------------------------------------------------
|
|
132
|
+
// Live refresh — mirrors WrfcPanel: any workflow event may have changed the
|
|
133
|
+
// active plan's item statuses (WRFC updates plan items via
|
|
134
|
+
// wrfcController.setPlanManager, main.ts:120), so re-render without
|
|
135
|
+
// requiring a keypress.
|
|
136
|
+
// --------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
private subscribeToEvents(): void {
|
|
139
|
+
const refresh = () => this.markDirty();
|
|
140
|
+
this.unsubscribers.push(
|
|
141
|
+
this.workflowEvents.on('WORKFLOW_CHAIN_CREATED', refresh),
|
|
142
|
+
this.workflowEvents.on('WORKFLOW_STATE_CHANGED', refresh),
|
|
143
|
+
this.workflowEvents.on('WORKFLOW_REVIEW_COMPLETED', refresh),
|
|
144
|
+
this.workflowEvents.on('WORKFLOW_FIX_ATTEMPTED', refresh),
|
|
145
|
+
this.workflowEvents.on('WORKFLOW_GATE_RESULT', refresh),
|
|
146
|
+
this.workflowEvents.on('WORKFLOW_CHAIN_PASSED', refresh),
|
|
147
|
+
this.workflowEvents.on('WORKFLOW_CHAIN_FAILED', refresh),
|
|
148
|
+
this.workflowEvents.on('WORKFLOW_AUTO_COMMITTED', refresh),
|
|
149
|
+
this.workflowEvents.on('WORKFLOW_CASCADE_ABORTED', refresh),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
57
153
|
// --------------------------------------------------------------------------
|
|
58
154
|
// Input
|
|
59
155
|
// --------------------------------------------------------------------------
|
|
60
156
|
|
|
61
157
|
handleInput(key: string): boolean {
|
|
158
|
+
if (this.historyMode) {
|
|
159
|
+
return this.handleHistoryInput(key);
|
|
160
|
+
}
|
|
161
|
+
if (key === 'h') {
|
|
162
|
+
this.enterHistoryMode();
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
if (key === 'a' && this.viewingPlanId !== null) {
|
|
166
|
+
this.viewingPlanId = null;
|
|
167
|
+
this.selectedIndex = 0;
|
|
168
|
+
this.markDirty();
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
62
171
|
if (key === 'up' || key === 'k') {
|
|
63
172
|
if (this.selectedIndex > 0) {
|
|
64
173
|
this.selectedIndex--;
|
|
@@ -71,17 +180,107 @@ export class PlanDashboardPanel extends BasePanel {
|
|
|
71
180
|
this.markDirty();
|
|
72
181
|
return true;
|
|
73
182
|
}
|
|
183
|
+
} else if (key === 'home' || key === 'g') {
|
|
184
|
+
if (this.selectedIndex !== 0) {
|
|
185
|
+
this.selectedIndex = 0;
|
|
186
|
+
this.markDirty();
|
|
187
|
+
}
|
|
188
|
+
return true;
|
|
189
|
+
} else if (key === 'end' || key === 'G') {
|
|
190
|
+
const last = Math.max(0, this.totalRows - 1);
|
|
191
|
+
if (this.selectedIndex !== last) {
|
|
192
|
+
this.selectedIndex = last;
|
|
193
|
+
this.markDirty();
|
|
194
|
+
}
|
|
195
|
+
return true;
|
|
196
|
+
} else if (key === 'enter' || key === 'return') {
|
|
197
|
+
// Consumed here (so the integration router fires next) only when the
|
|
198
|
+
// selected item actually has an agent to jump to; see
|
|
199
|
+
// handlePanelIntegrationAction below.
|
|
200
|
+
const item = this.selectedNavItem();
|
|
201
|
+
if (item?.agentId) return true;
|
|
74
202
|
}
|
|
75
203
|
return false;
|
|
76
204
|
}
|
|
77
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Cross-panel jump: Enter on a plan item with an assigned agent opens the
|
|
208
|
+
* Inspector focused on that agent (inspectAgent, WO-110). The inspector
|
|
209
|
+
* already renders a WRFC badge for the inspected agent, so this also
|
|
210
|
+
* cross-links the owning WRFC chain without the dashboard needing to know
|
|
211
|
+
* about WrfcController directly.
|
|
212
|
+
*/
|
|
213
|
+
handlePanelIntegrationAction(key: string, ctx: PanelIntegrationContext): boolean {
|
|
214
|
+
if (this.historyMode) return false;
|
|
215
|
+
if (key !== 'enter' && key !== 'return') return false;
|
|
216
|
+
const item = this.selectedNavItem();
|
|
217
|
+
if (!item?.agentId) return false;
|
|
218
|
+
const inspector = ctx.panelManager.open('inspector');
|
|
219
|
+
if (inspector instanceof AgentInspectorPanel) {
|
|
220
|
+
inspector.inspectAgent(item.agentId);
|
|
221
|
+
}
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// --------------------------------------------------------------------------
|
|
226
|
+
// Plan history / switching
|
|
227
|
+
// --------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
private enterHistoryMode(): void {
|
|
230
|
+
this.historyPlans = [...this.deps.planManager.list()].sort(
|
|
231
|
+
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
|
|
232
|
+
);
|
|
233
|
+
this.historySelectedIndex = 0;
|
|
234
|
+
this.historyScrollOffset = 0;
|
|
235
|
+
this.historyMode = true;
|
|
236
|
+
this.markDirty();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private handleHistoryInput(key: string): boolean {
|
|
240
|
+
if (key === 'up' || key === 'k') {
|
|
241
|
+
if (this.historySelectedIndex > 0) {
|
|
242
|
+
this.historySelectedIndex--;
|
|
243
|
+
this.markDirty();
|
|
244
|
+
}
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
if (key === 'down' || key === 'j') {
|
|
248
|
+
if (this.historySelectedIndex < this.historyPlans.length - 1) {
|
|
249
|
+
this.historySelectedIndex++;
|
|
250
|
+
this.markDirty();
|
|
251
|
+
}
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
if (key === 'enter' || key === 'return') {
|
|
255
|
+
const plan = this.historyPlans[this.historySelectedIndex];
|
|
256
|
+
if (plan) {
|
|
257
|
+
this.viewingPlanId = plan.id;
|
|
258
|
+
this.selectedIndex = 0;
|
|
259
|
+
}
|
|
260
|
+
this.historyMode = false;
|
|
261
|
+
this.markDirty();
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
if (key === 'escape' || key === 'h') {
|
|
265
|
+
this.historyMode = false;
|
|
266
|
+
this.markDirty();
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
// Modal browsing: absorb everything else (mirrors ConfirmState's "others
|
|
270
|
+
// absorbed" contract) so stray keys cannot leak into item navigation.
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
|
|
78
274
|
// --------------------------------------------------------------------------
|
|
79
275
|
// Render
|
|
80
276
|
// --------------------------------------------------------------------------
|
|
81
277
|
|
|
82
278
|
render(width: number, height: number): Line[] {
|
|
83
279
|
return this.trackedRender(() => {
|
|
84
|
-
|
|
280
|
+
if (this.historyMode) {
|
|
281
|
+
return this.renderHistory(width, height);
|
|
282
|
+
}
|
|
283
|
+
const plan = this.resolveViewedPlan();
|
|
85
284
|
if (!plan) {
|
|
86
285
|
return buildPanelWorkspace(width, height, {
|
|
87
286
|
title: ' Plan Dashboard',
|
|
@@ -91,24 +290,166 @@ export class PlanDashboardPanel extends BasePanel {
|
|
|
91
290
|
lines: buildEmptyState(
|
|
92
291
|
width,
|
|
93
292
|
' No active execution plan',
|
|
94
|
-
'
|
|
95
|
-
[
|
|
96
|
-
|
|
293
|
+
'Execution plans are produced by WRFC-reviewed tasks (gather-plan-apply execution). Start one from Teamwork and its phases, progress, blocked steps, and next action will surface here.',
|
|
294
|
+
[
|
|
295
|
+
{ command: '/teamwork create-mode local-engineer <title>', summary: 'start a WRFC-reviewed engineer task — the only producer of a real execution plan' },
|
|
296
|
+
{ command: '/teamwork modes', summary: 'see every reviewMode: wrfc mode that can seed a plan' },
|
|
297
|
+
],
|
|
298
|
+
C,
|
|
97
299
|
),
|
|
98
300
|
},
|
|
99
301
|
],
|
|
100
|
-
|
|
302
|
+
footerLines: this.historyFooterHintLines(width),
|
|
303
|
+
palette: C,
|
|
101
304
|
});
|
|
102
305
|
}
|
|
103
306
|
return this.renderPlan(plan, width, height);
|
|
104
307
|
});
|
|
105
308
|
}
|
|
106
309
|
|
|
310
|
+
/** Resolve which plan to render: the pinned history selection, or the live active plan. */
|
|
311
|
+
private resolveViewedPlan(): ExecutionPlan | null {
|
|
312
|
+
if (this.viewingPlanId !== null) {
|
|
313
|
+
const found = this.deps.planManager.list().find((p) => p.id === this.viewingPlanId);
|
|
314
|
+
if (found) return found;
|
|
315
|
+
// The pinned plan is gone (e.g. deleted from disk) — fall back to live.
|
|
316
|
+
this.viewingPlanId = null;
|
|
317
|
+
}
|
|
318
|
+
return this.deps.planManager.getActive();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Keyboard hint shown even on the empty state so 'h' (history) stays discoverable. */
|
|
322
|
+
private historyFooterHintLines(width: number): Line[] {
|
|
323
|
+
return [
|
|
324
|
+
buildKeyboardHints(width, [
|
|
325
|
+
{ keys: 'h', label: 'plan history' },
|
|
326
|
+
], C),
|
|
327
|
+
];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// --------------------------------------------------------------------------
|
|
331
|
+
// Plan history browser
|
|
332
|
+
// --------------------------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
private renderHistory(width: number, height: number): Line[] {
|
|
335
|
+
if (this.historyPlans.length === 0) {
|
|
336
|
+
return buildPanelWorkspace(width, height, {
|
|
337
|
+
title: ' Plan History',
|
|
338
|
+
intro: 'Every execution plan ever created for this project.',
|
|
339
|
+
sections: [
|
|
340
|
+
{
|
|
341
|
+
lines: buildEmptyState(
|
|
342
|
+
width,
|
|
343
|
+
' No plans on disk',
|
|
344
|
+
'Plans appear here once a WRFC-reviewed task (gather-plan-apply execution) creates one.',
|
|
345
|
+
[],
|
|
346
|
+
C,
|
|
347
|
+
),
|
|
348
|
+
},
|
|
349
|
+
],
|
|
350
|
+
footerLines: [buildKeyboardHints(width, [{ keys: 'Esc / h', label: 'back' }], C)],
|
|
351
|
+
palette: C,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const activePlan = this.deps.planManager.getActive();
|
|
356
|
+
const rows: Line[] = [];
|
|
357
|
+
let selectedLineIndex = 0;
|
|
358
|
+
for (let i = 0; i < this.historyPlans.length; i++) {
|
|
359
|
+
const plan = this.historyPlans[i]!;
|
|
360
|
+
const isSelected = i === this.historySelectedIndex;
|
|
361
|
+
if (isSelected) selectedLineIndex = rows.length;
|
|
362
|
+
rows.push(this.renderHistoryRow(plan, activePlan?.id === plan.id, isSelected, width));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const highlighted = this.historyPlans[this.historySelectedIndex];
|
|
366
|
+
const detailSection: PanelWorkspaceSection | null = highlighted ? {
|
|
367
|
+
title: 'Plan Detail',
|
|
368
|
+
lines: this.buildHistoryDetailLines(highlighted, width),
|
|
369
|
+
} : null;
|
|
370
|
+
|
|
371
|
+
const footerLines: Line[] = [
|
|
372
|
+
buildKeyboardHints(width, [
|
|
373
|
+
{ keys: '↑/↓', label: 'browse' },
|
|
374
|
+
{ keys: 'Enter', label: 'view plan' },
|
|
375
|
+
{ keys: 'Esc / h', label: 'back' },
|
|
376
|
+
], C),
|
|
377
|
+
];
|
|
378
|
+
|
|
379
|
+
const intro = `${this.historyPlans.length} plan${this.historyPlans.length === 1 ? '' : 's'} on disk`;
|
|
380
|
+
const historySection = resolveScrollablePanelSection(width, height, {
|
|
381
|
+
intro,
|
|
382
|
+
footerLines,
|
|
383
|
+
palette: C,
|
|
384
|
+
afterSections: detailSection ? [detailSection] : [],
|
|
385
|
+
section: {
|
|
386
|
+
title: 'Plan History',
|
|
387
|
+
scrollableLines: rows,
|
|
388
|
+
selectedIndex: selectedLineIndex,
|
|
389
|
+
scrollOffset: this.historyScrollOffset,
|
|
390
|
+
minRows: 6,
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
this.historyScrollOffset = historySection.scrollOffset;
|
|
394
|
+
|
|
395
|
+
return buildPanelWorkspace(width, height, {
|
|
396
|
+
title: ' Plan History',
|
|
397
|
+
intro,
|
|
398
|
+
sections: [
|
|
399
|
+
historySection.section,
|
|
400
|
+
...(detailSection ? [detailSection] : []),
|
|
401
|
+
],
|
|
402
|
+
footerLines,
|
|
403
|
+
palette: C,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private renderHistoryRow(plan: ExecutionPlan, isActive: boolean, isSelected: boolean, width: number): Line {
|
|
408
|
+
const marker = isActive ? '▶' : ' ';
|
|
409
|
+
const idShort = plan.id.slice(0, 8);
|
|
410
|
+
const statusTag = `[${plan.status.padEnd(8)}]`;
|
|
411
|
+
const prefix = ` ${marker} ${idShort} ${statusTag} `;
|
|
412
|
+
const label = truncateDisplay(plan.title, Math.max(6, width - prefix.length - 2));
|
|
413
|
+
return buildSelectablePanelLine(width, [
|
|
414
|
+
{ text: ` ${marker} `, fg: isActive ? C.good : C.dim, bold: isActive },
|
|
415
|
+
{ text: `${idShort} `, fg: C.dim },
|
|
416
|
+
{ text: `${statusTag} `, fg: this.statusColor(plan.status) },
|
|
417
|
+
{ text: label, fg: C.value },
|
|
418
|
+
], {
|
|
419
|
+
selected: isSelected,
|
|
420
|
+
selectedBg: C.selectBg,
|
|
421
|
+
leadingMarker: '▸',
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private buildHistoryDetailLines(plan: ExecutionPlan, width: number): Line[] {
|
|
426
|
+
const lines: Line[] = [
|
|
427
|
+
buildPanelLine(width, [
|
|
428
|
+
[' Summary ', C.label],
|
|
429
|
+
[truncateDisplay(this.deps.planManager.getSummary(plan), Math.max(8, width - 12)), C.value],
|
|
430
|
+
]),
|
|
431
|
+
];
|
|
432
|
+
const markdown = this.deps.planManager.toMarkdown(plan)
|
|
433
|
+
.split('\n')
|
|
434
|
+
.filter((line) => line.trim().length > 0)
|
|
435
|
+
.slice(0, 4);
|
|
436
|
+
for (const line of markdown) {
|
|
437
|
+
lines.push(buildPanelLine(width, [
|
|
438
|
+
[' ', C.label],
|
|
439
|
+
[truncateDisplay(line, Math.max(8, width - 3)), C.dim],
|
|
440
|
+
]));
|
|
441
|
+
}
|
|
442
|
+
return lines;
|
|
443
|
+
}
|
|
444
|
+
|
|
107
445
|
// --------------------------------------------------------------------------
|
|
108
|
-
//
|
|
446
|
+
// Plan render
|
|
109
447
|
// --------------------------------------------------------------------------
|
|
110
448
|
|
|
111
449
|
private renderPlan(plan: ExecutionPlan, width: number, height: number): Line[] {
|
|
450
|
+
const activePlan = this.deps.planManager.getActive();
|
|
451
|
+
const isHistorical = activePlan?.id !== plan.id;
|
|
452
|
+
|
|
112
453
|
const phaseOrder: string[] = [];
|
|
113
454
|
const byPhase = new Map<string, PlanItem[]>();
|
|
114
455
|
for (const item of plan.items) {
|
|
@@ -126,9 +467,17 @@ export class PlanDashboardPanel extends BasePanel {
|
|
|
126
467
|
.map((i) => i.id),
|
|
127
468
|
);
|
|
128
469
|
|
|
470
|
+
const isBlocked = (item: PlanItem): boolean =>
|
|
471
|
+
item.status === 'pending' &&
|
|
472
|
+
item.dependencies !== undefined &&
|
|
473
|
+
item.dependencies.length > 0 &&
|
|
474
|
+
!item.dependencies.every((depId) => completeIds.has(depId));
|
|
475
|
+
|
|
129
476
|
let rowCount = 0;
|
|
130
477
|
let selectedLineIndex = 0;
|
|
131
478
|
const planLines: Line[] = [];
|
|
479
|
+
const navItems: PlanItem[] = [];
|
|
480
|
+
const blockedItemIds = new Set<string>();
|
|
132
481
|
|
|
133
482
|
for (const phase of phaseOrder) {
|
|
134
483
|
const items = byPhase.get(phase)!;
|
|
@@ -136,52 +485,96 @@ export class PlanDashboardPanel extends BasePanel {
|
|
|
136
485
|
for (const item of items) {
|
|
137
486
|
const isSelected = rowCount === this.selectedIndex;
|
|
138
487
|
if (isSelected) selectedLineIndex = planLines.length;
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
!item.dependencies.every((depId) => completeIds.has(depId));
|
|
144
|
-
|
|
145
|
-
planLines.push(this.renderItem(item, isSelected, isBlocked, width));
|
|
488
|
+
const blocked = isBlocked(item);
|
|
489
|
+
if (blocked) blockedItemIds.add(item.id);
|
|
490
|
+
navItems.push(item);
|
|
491
|
+
planLines.push(this.renderItem(item, isSelected, blocked, width));
|
|
146
492
|
rowCount++;
|
|
147
493
|
}
|
|
148
494
|
}
|
|
149
495
|
|
|
150
496
|
this.totalRows = rowCount;
|
|
497
|
+
this.navItems = navItems;
|
|
498
|
+
this.blockedItemIds = blockedItemIds;
|
|
151
499
|
if (this.selectedIndex >= this.totalRows) {
|
|
152
500
|
this.selectedIndex = Math.max(0, this.totalRows - 1);
|
|
153
501
|
}
|
|
154
502
|
|
|
155
503
|
const total = plan.items.length;
|
|
156
504
|
const done = plan.items.filter((i) => i.status === 'complete' || i.status === 'skipped').length;
|
|
505
|
+
const inProgress = plan.items.filter((i) => i.status === 'in_progress').length;
|
|
506
|
+
const failed = plan.items.filter((i) => i.status === 'failed').length;
|
|
507
|
+
const blockedCount = blockedItemIds.size;
|
|
157
508
|
const pct = total > 0 ? Math.round((done / total) * 100) : 0;
|
|
509
|
+
|
|
510
|
+
// The single most useful pointer: what should happen next. Prefer the first
|
|
511
|
+
// in-progress item, else the first actionable (unblocked) pending item.
|
|
512
|
+
const nextItem = plan.items.find((i) => i.status === 'in_progress')
|
|
513
|
+
?? plan.items.find((i) => i.status === 'pending' && !isBlocked(i));
|
|
514
|
+
|
|
515
|
+
const meterWidth = Math.max(10, Math.min(28, width - 30));
|
|
158
516
|
const summary: PanelWorkspaceSection = {
|
|
159
517
|
title: 'Summary',
|
|
160
518
|
lines: [
|
|
161
519
|
buildPanelLine(width, [
|
|
162
|
-
[' Status ',
|
|
520
|
+
[' Status ', C.label],
|
|
163
521
|
[plan.status, this.statusColor(plan.status)],
|
|
164
|
-
['
|
|
165
|
-
[`${
|
|
166
|
-
['
|
|
167
|
-
[`${
|
|
522
|
+
[' ', C.label],
|
|
523
|
+
[`${done}/${total} items`, C.value],
|
|
524
|
+
[' ', C.label],
|
|
525
|
+
[`${pct}%`, pct === 100 ? C.good : C.info],
|
|
168
526
|
]),
|
|
527
|
+
buildMeterLine(width, Math.round((pct / 100) * meterWidth), meterWidth, {
|
|
528
|
+
filled: pct === 100 ? C.good : C.info,
|
|
529
|
+
empty: C.empty,
|
|
530
|
+
label: C.label,
|
|
531
|
+
}, { prefix: ' Progress ', suffix: ` ${pct}% ` }),
|
|
532
|
+
buildKeyValueLine(width, [
|
|
533
|
+
{ label: 'in progress', value: String(inProgress), valueColor: inProgress > 0 ? C.active : C.dim },
|
|
534
|
+
{ label: 'blocked', value: String(blockedCount), valueColor: blockedCount > 0 ? C.blocked : C.dim },
|
|
535
|
+
{ label: 'failed', value: String(failed), valueColor: failed > 0 ? C.bad : C.dim },
|
|
536
|
+
], C),
|
|
537
|
+
nextItem
|
|
538
|
+
? buildPanelLine(width, [
|
|
539
|
+
[' Next ', C.label],
|
|
540
|
+
[STATUS_ICON[nextItem.status], STATUS_FG[nextItem.status]],
|
|
541
|
+
[' ', C.label],
|
|
542
|
+
[truncateDisplay(nextItem.description, Math.max(8, width - 9)), C.value],
|
|
543
|
+
])
|
|
544
|
+
: buildPanelLine(width, [
|
|
545
|
+
[' Next ', C.label],
|
|
546
|
+
[failed > 0 ? 'review failed steps' : blockedCount > 0 ? 'unblock dependencies' : 'plan complete',
|
|
547
|
+
failed > 0 ? C.bad : blockedCount > 0 ? C.blocked : C.good],
|
|
548
|
+
]),
|
|
169
549
|
],
|
|
170
550
|
};
|
|
171
551
|
|
|
552
|
+
const historyBanner: PanelWorkspaceSection | null = isHistorical ? {
|
|
553
|
+
lines: [
|
|
554
|
+
buildPanelLine(width, [
|
|
555
|
+
[' Viewing history ', C.warn],
|
|
556
|
+
[plan.status.toUpperCase(), this.statusColor(plan.status)],
|
|
557
|
+
[' — press a to return to the live plan', C.dim],
|
|
558
|
+
]),
|
|
559
|
+
],
|
|
560
|
+
} : null;
|
|
561
|
+
|
|
562
|
+
const detailSection = this.buildDetailSection(width);
|
|
563
|
+
const footerLines = this.footerLines(width);
|
|
564
|
+
|
|
172
565
|
const planSection = resolveScrollablePanelSection(width, height, {
|
|
173
566
|
intro: plan.title,
|
|
174
|
-
footerLines
|
|
175
|
-
|
|
176
|
-
],
|
|
177
|
-
|
|
178
|
-
beforeSections: [summary],
|
|
567
|
+
footerLines,
|
|
568
|
+
palette: C,
|
|
569
|
+
beforeSections: historyBanner ? [historyBanner, summary] : [summary],
|
|
570
|
+
afterSections: detailSection ? [detailSection] : [],
|
|
179
571
|
section: {
|
|
180
572
|
title: 'Execution Plan',
|
|
181
573
|
scrollableLines: planLines,
|
|
182
574
|
selectedIndex: selectedLineIndex,
|
|
183
575
|
scrollOffset: this.scrollOffset,
|
|
184
|
-
minRows:
|
|
576
|
+
minRows: 6,
|
|
577
|
+
appendWindowSummary: { dimColor: C.dim },
|
|
185
578
|
},
|
|
186
579
|
});
|
|
187
580
|
this.scrollOffset = planSection.scrollOffset;
|
|
@@ -190,18 +583,57 @@ export class PlanDashboardPanel extends BasePanel {
|
|
|
190
583
|
title: ` Plan Dashboard`,
|
|
191
584
|
intro: plan.title,
|
|
192
585
|
sections: [
|
|
586
|
+
...(historyBanner ? [historyBanner] : []),
|
|
193
587
|
summary,
|
|
194
588
|
planSection.section,
|
|
589
|
+
...(detailSection ? [detailSection] : []),
|
|
195
590
|
],
|
|
196
|
-
footerLines
|
|
197
|
-
|
|
198
|
-
],
|
|
199
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
591
|
+
footerLines,
|
|
592
|
+
palette: C,
|
|
200
593
|
});
|
|
201
594
|
}
|
|
202
595
|
|
|
596
|
+
private buildDetailSection(width: number): PanelWorkspaceSection | null {
|
|
597
|
+
const item = this.selectedNavItem();
|
|
598
|
+
if (!item) return null;
|
|
599
|
+
const blocked = this.blockedItemIds.has(item.id);
|
|
600
|
+
const lines: Line[] = [
|
|
601
|
+
buildPanelLine(width, [
|
|
602
|
+
[' ', C.label],
|
|
603
|
+
[STATUS_ICON[item.status], STATUS_FG[item.status]],
|
|
604
|
+
[' ', C.label],
|
|
605
|
+
[truncateDisplay(item.description, Math.max(8, width - 4)), C.value],
|
|
606
|
+
]),
|
|
607
|
+
buildKeyValueLine(width, [
|
|
608
|
+
{ label: 'phase', value: item.phase, valueColor: C.phase },
|
|
609
|
+
{ label: 'status', value: blocked ? 'blocked' : item.status.replace(/_/g, ' '), valueColor: blocked ? C.blocked : STATUS_FG[item.status] },
|
|
610
|
+
...(item.agentId ? [{ label: 'agent', value: item.agentId, valueColor: C.info }] : []),
|
|
611
|
+
], C),
|
|
612
|
+
];
|
|
613
|
+
if (item.dependencies && item.dependencies.length > 0) {
|
|
614
|
+
lines.push(buildPanelLine(width, [
|
|
615
|
+
[' depends on ', C.label],
|
|
616
|
+
[truncateDisplay(item.dependencies.join(', '), Math.max(8, width - 13)), blocked ? C.blocked : C.dim],
|
|
617
|
+
]));
|
|
618
|
+
}
|
|
619
|
+
return { title: 'Selected Step', lines };
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
private footerLines(width: number): Line[] {
|
|
623
|
+
const hints: Array<{ keys: string; label: string }> = [
|
|
624
|
+
{ keys: this.totalRows > 0 ? `${this.selectedIndex + 1}/${this.totalRows}` : '0/0', label: 'step' },
|
|
625
|
+
{ keys: '↑/↓', label: 'navigate' },
|
|
626
|
+
{ keys: 'Home/End', label: 'jump' },
|
|
627
|
+
{ keys: 'h', label: 'history' },
|
|
628
|
+
];
|
|
629
|
+
const item = this.selectedNavItem();
|
|
630
|
+
if (item?.agentId) hints.push({ keys: 'Enter', label: 'inspect agent' });
|
|
631
|
+
if (this.viewingPlanId !== null) hints.push({ keys: 'a', label: 'back to active' });
|
|
632
|
+
return [buildKeyboardHints(width, hints, C)];
|
|
633
|
+
}
|
|
634
|
+
|
|
203
635
|
// --------------------------------------------------------------------------
|
|
204
|
-
//
|
|
636
|
+
// Phase header — title + progress meter
|
|
205
637
|
// --------------------------------------------------------------------------
|
|
206
638
|
|
|
207
639
|
private renderPhaseHeaderLine(phase: string, items: PlanItem[], width: number): Line {
|
|
@@ -212,21 +644,20 @@ export class PlanDashboardPanel extends BasePanel {
|
|
|
212
644
|
const active = items.some((i) => i.status === 'in_progress');
|
|
213
645
|
const failed = items.some((i) => i.status === 'failed');
|
|
214
646
|
|
|
215
|
-
const phaseFg = failed ?
|
|
647
|
+
const phaseFg = failed ? C.failed : active ? C.active : done === total ? C.complete : C.phase;
|
|
216
648
|
|
|
217
649
|
const barW = 8;
|
|
218
650
|
const filledB = total > 0 ? Math.round((done / total) * barW) : 0;
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
]);
|
|
651
|
+
const progressText = ` ${done}/${total}`;
|
|
652
|
+
const label = truncateDisplay(phase, Math.max(4, width - barW - progressText.length - 6));
|
|
653
|
+
|
|
654
|
+
return buildSelectablePanelLine(width, [
|
|
655
|
+
{ text: ` ${active ? '▸' : '▪'} `, fg: phaseFg, bold: true },
|
|
656
|
+
{ text: label, fg: phaseFg, bold: true },
|
|
657
|
+
{ text: ' ', fg: C.dim },
|
|
658
|
+
{ text: '▰'.repeat(filledB) + '▱'.repeat(barW - filledB), fg: phaseFg },
|
|
659
|
+
{ text: progressText, fg: C.dim },
|
|
660
|
+
], { fillBg: C.sectionBg });
|
|
230
661
|
}
|
|
231
662
|
|
|
232
663
|
// --------------------------------------------------------------------------
|
|
@@ -240,35 +671,37 @@ export class PlanDashboardPanel extends BasePanel {
|
|
|
240
671
|
width: number,
|
|
241
672
|
): Line {
|
|
242
673
|
const icon = STATUS_ICON[item.status];
|
|
243
|
-
const fg = isBlocked ?
|
|
244
|
-
const dim = item.status === 'skipped'
|
|
674
|
+
const fg = isBlocked ? C.blocked : STATUS_FG[item.status];
|
|
675
|
+
const dim = item.status === 'skipped';
|
|
245
676
|
|
|
246
677
|
// Indent blocked items to visually signal they depend on others
|
|
247
|
-
const indent = isBlocked ? '
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
text
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
678
|
+
const indent = isBlocked ? ' ' : ' ';
|
|
679
|
+
const agentTag = item.agentId && (item.status === 'in_progress' || item.status === 'complete')
|
|
680
|
+
? ` [${item.agentId}]`
|
|
681
|
+
: '';
|
|
682
|
+
const blockedTag = isBlocked ? ' ⊘ blocked' : '';
|
|
683
|
+
const reserve = indent.length + 2 + agentTag.length + blockedTag.length;
|
|
684
|
+
const desc = truncateDisplay(item.description, Math.max(6, width - reserve - 2));
|
|
685
|
+
|
|
686
|
+
return buildSelectablePanelLine(width, [
|
|
687
|
+
{ text: `${indent}${icon} `, fg, dim, bold: item.status === 'in_progress' },
|
|
688
|
+
{ text: desc, fg, dim },
|
|
689
|
+
{ text: agentTag, fg: C.info },
|
|
690
|
+
{ text: blockedTag, fg: C.blocked },
|
|
691
|
+
], {
|
|
692
|
+
selected: isSelected,
|
|
693
|
+
selectedBg: C.selectBg,
|
|
694
|
+
leadingMarker: '▸',
|
|
695
|
+
});
|
|
263
696
|
}
|
|
264
697
|
|
|
265
698
|
private statusColor(status: ExecutionPlan['status']): string {
|
|
266
699
|
return status === 'complete'
|
|
267
|
-
?
|
|
700
|
+
? C.good
|
|
268
701
|
: status === 'failed'
|
|
269
|
-
?
|
|
702
|
+
? C.bad
|
|
270
703
|
: status === 'active'
|
|
271
|
-
?
|
|
272
|
-
:
|
|
704
|
+
? C.info
|
|
705
|
+
: C.dim;
|
|
273
706
|
}
|
|
274
707
|
}
|