@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.
Files changed (143) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/core/context-usage.ts +53 -0
  5. package/src/core/conversation.ts +4 -6
  6. package/src/core/session-recovery.ts +2 -0
  7. package/src/core/turn-event-wiring.ts +1 -0
  8. package/src/input/commands/cost-runtime.ts +49 -0
  9. package/src/input/commands/eval.ts +4 -3
  10. package/src/input/commands/operator-runtime.ts +5 -1
  11. package/src/input/commands.ts +2 -0
  12. package/src/input/handler-feed-routes.ts +60 -26
  13. package/src/input/handler-feed.ts +17 -8
  14. package/src/input/handler-picker-routes.ts +18 -13
  15. package/src/input/handler-shortcuts.ts +51 -0
  16. package/src/input/handler-ui-state.ts +4 -0
  17. package/src/input/handler.ts +43 -6
  18. package/src/input/keybindings.ts +53 -8
  19. package/src/input/model-picker.ts +5 -0
  20. package/src/input/panel-integration-actions.ts +117 -1
  21. package/src/main.ts +5 -2
  22. package/src/panels/agent-inspector-panel.ts +139 -45
  23. package/src/panels/agent-inspector-shared.ts +147 -3
  24. package/src/panels/approval-panel.ts +204 -102
  25. package/src/panels/automation-control-panel.ts +370 -103
  26. package/src/panels/base-panel.ts +26 -2
  27. package/src/panels/builtin/agent.ts +28 -37
  28. package/src/panels/builtin/development.ts +40 -32
  29. package/src/panels/builtin/knowledge.ts +15 -6
  30. package/src/panels/builtin/operations.ts +178 -117
  31. package/src/panels/builtin/session.ts +35 -9
  32. package/src/panels/builtin/shared.ts +98 -6
  33. package/src/panels/cockpit-panel.ts +253 -75
  34. package/src/panels/cockpit-read-model.ts +2 -1
  35. package/src/panels/communication-panel.ts +159 -56
  36. package/src/panels/confirm-state.ts +8 -1
  37. package/src/panels/control-plane-panel.ts +354 -95
  38. package/src/panels/cost-tracker-panel.ts +243 -57
  39. package/src/panels/debug-panel.ts +347 -159
  40. package/src/panels/diff-panel.ts +257 -82
  41. package/src/panels/docs-panel.ts +166 -64
  42. package/src/panels/eval-panel.ts +336 -109
  43. package/src/panels/expandable-list-panel.ts +189 -0
  44. package/src/panels/file-explorer-panel.ts +238 -159
  45. package/src/panels/file-preview-panel.ts +141 -59
  46. package/src/panels/git-panel.ts +360 -191
  47. package/src/panels/hooks-panel.ts +181 -19
  48. package/src/panels/incident-review-panel.ts +252 -43
  49. package/src/panels/index.ts +0 -4
  50. package/src/panels/intelligence-panel.ts +310 -103
  51. package/src/panels/knowledge-graph-panel.ts +480 -58
  52. package/src/panels/local-auth-panel.ts +251 -33
  53. package/src/panels/marketplace-panel.ts +219 -33
  54. package/src/panels/memory-panel.ts +83 -81
  55. package/src/panels/ops-control-panel.ts +211 -32
  56. package/src/panels/ops-strategy-panel.ts +131 -45
  57. package/src/panels/orchestration-panel.ts +259 -67
  58. package/src/panels/panel-list-panel.ts +184 -136
  59. package/src/panels/panel-manager.ts +120 -13
  60. package/src/panels/plan-dashboard-panel.ts +507 -74
  61. package/src/panels/plugins-panel.ts +227 -34
  62. package/src/panels/policy-panel.ts +264 -55
  63. package/src/panels/polish-core.ts +162 -0
  64. package/src/panels/polish-tables.ts +258 -0
  65. package/src/panels/polish.ts +67 -149
  66. package/src/panels/project-planning-answer-actions.ts +134 -0
  67. package/src/panels/project-planning-panel.ts +84 -113
  68. package/src/panels/provider-health-panel.ts +438 -480
  69. package/src/panels/provider-health-routes.ts +203 -0
  70. package/src/panels/provider-health-tracker.ts +194 -6
  71. package/src/panels/provider-health-views.ts +560 -0
  72. package/src/panels/qr-panel.ts +136 -38
  73. package/src/panels/remote-panel.ts +123 -38
  74. package/src/panels/routes-panel.ts +87 -24
  75. package/src/panels/sandbox-panel.ts +232 -65
  76. package/src/panels/scrollable-list-panel.ts +143 -145
  77. package/src/panels/security-panel.ts +204 -52
  78. package/src/panels/services-panel.ts +174 -76
  79. package/src/panels/session-browser-panel.ts +101 -6
  80. package/src/panels/session-maintenance.ts +4 -122
  81. package/src/panels/settings-sync-panel.ts +346 -68
  82. package/src/panels/skills-panel.ts +157 -89
  83. package/src/panels/subscription-panel.ts +105 -34
  84. package/src/panels/symbol-outline-panel.ts +287 -154
  85. package/src/panels/system-messages-panel.ts +172 -38
  86. package/src/panels/tasks-panel.ts +348 -134
  87. package/src/panels/thinking-panel.ts +66 -32
  88. package/src/panels/token-budget-panel.ts +233 -46
  89. package/src/panels/tool-inspector-panel.ts +170 -54
  90. package/src/panels/types.ts +65 -4
  91. package/src/panels/work-plan-panel.ts +393 -39
  92. package/src/panels/worktree-panel.ts +239 -61
  93. package/src/panels/wrfc-panel-format.ts +133 -0
  94. package/src/panels/wrfc-panel.ts +146 -150
  95. package/src/renderer/compaction-preview.ts +2 -1
  96. package/src/renderer/compositor.ts +46 -43
  97. package/src/renderer/conversation-overlays.ts +25 -11
  98. package/src/renderer/footer-tips.ts +41 -0
  99. package/src/renderer/fullscreen-primitives.ts +22 -16
  100. package/src/renderer/help-overlay.ts +91 -14
  101. package/src/renderer/hint-grammar.ts +52 -0
  102. package/src/renderer/layout.ts +7 -7
  103. package/src/renderer/modal-factory.ts +23 -15
  104. package/src/renderer/model-picker-overlay.ts +30 -11
  105. package/src/renderer/model-workspace.ts +2 -3
  106. package/src/renderer/overlay-box.ts +16 -10
  107. package/src/renderer/panel-composite.ts +7 -20
  108. package/src/renderer/panel-workspace-bar.ts +14 -8
  109. package/src/renderer/process-indicator.ts +3 -1
  110. package/src/renderer/progress.ts +8 -6
  111. package/src/renderer/search-overlay.ts +27 -6
  112. package/src/renderer/session-picker-modal.ts +6 -4
  113. package/src/renderer/settings-modal.ts +70 -10
  114. package/src/renderer/shell-surface.ts +41 -15
  115. package/src/renderer/status-glyphs.ts +11 -9
  116. package/src/renderer/tab-strip.ts +148 -34
  117. package/src/renderer/theme.ts +60 -3
  118. package/src/renderer/ui-factory.ts +45 -36
  119. package/src/renderer/ui-primitives.ts +23 -2
  120. package/src/runtime/bootstrap-shell.ts +35 -18
  121. package/src/runtime/diagnostics/panels/index.ts +0 -3
  122. package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
  123. package/src/runtime/ui/model-picker/types.ts +4 -0
  124. package/src/shell/ui-openers.ts +14 -22
  125. package/src/utils/format-duration.ts +55 -0
  126. package/src/utils/format-number.ts +71 -0
  127. package/src/utils/splash-lines.ts +44 -3
  128. package/src/version.ts +1 -1
  129. package/src/work-plans/work-plan-store.ts +13 -0
  130. package/src/panels/agent-logs-panel.ts +0 -666
  131. package/src/panels/agent-logs-shared.ts +0 -129
  132. package/src/panels/context-visualizer-panel.ts +0 -214
  133. package/src/panels/forensics-panel.ts +0 -364
  134. package/src/panels/panel-picker.ts +0 -106
  135. package/src/panels/provider-account-snapshot.ts +0 -259
  136. package/src/panels/provider-accounts-panel.ts +0 -218
  137. package/src/panels/provider-stats-panel.ts +0 -366
  138. package/src/panels/schedule-panel.ts +0 -342
  139. package/src/panels/watchers-panel.ts +0 -193
  140. package/src/renderer/panel-picker-overlay.ts +0 -202
  141. package/src/renderer/panel-tab-bar.ts +0 -69
  142. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  143. package/src/runtime/diagnostics/panels/policy.ts +0 -177
@@ -8,12 +8,14 @@
8
8
 
9
9
  import type { Line } from '../types/grid.ts';
10
10
  import { createEmptyLine } from '../types/grid.ts';
11
+ import { truncateDisplay } from '../utils/terminal-width.ts';
11
12
  import { ScrollableListPanel } from './scrollable-list-panel.ts';
12
13
  import type { UiOrchestrationSnapshot, UiReadModel } from '../runtime/ui-read-models.ts';
13
- import type { OrchestrationGraphRecord } from '@/runtime/index.ts';
14
+ import type { OrchestrationGraphRecord, OrchestrationNodeRecord } from '@/runtime/index.ts';
15
+ import { formatElapsed } from '../utils/format-elapsed.ts';
14
16
  import {
15
17
  buildEmptyState,
16
- buildGuidanceLine,
18
+ buildKeyboardHints,
17
19
  buildKeyValueLine,
18
20
  buildPanelLine,
19
21
  buildPanelWorkspace,
@@ -24,34 +26,69 @@ import {
24
26
  type PanelWorkspaceSection,
25
27
  } from './polish.ts';
26
28
 
29
+ // Domain accents only; base chrome (header/headerBg/info/good/warn/bad/
30
+ // selectBg) comes from DEFAULT_PANEL_PALETTE.
27
31
  const C = extendPalette(DEFAULT_PANEL_PALETTE, {
28
- header: '#94a3b8',
29
- headerBg: '#1e293b',
30
- running: '#22c55e',
31
- ready: '#38bdf8',
32
- blocked: '#f59e0b',
33
- failed: '#ef4444',
34
- completed: '#a78bfa',
35
- selectBg: '#0f172a',
32
+ completed: '#a78bfa', // completed-graph badge, distinct from running/good
36
33
  } as const);
37
34
 
38
35
  function statusColor(status: string): string {
39
36
  switch (status) {
40
- case 'ready': return C.ready;
41
- case 'running': return C.running;
42
- case 'blocked': return C.blocked;
43
- case 'failed': return C.failed;
37
+ case 'ready': return C.info;
38
+ case 'running': return C.good;
39
+ case 'blocked': return C.warn;
40
+ case 'failed': return C.bad;
44
41
  case 'completed': return C.completed;
45
42
  default: return C.dim;
46
43
  }
47
44
  }
48
45
 
46
+ /** One captured recursion-guard trip — built locally from `lastRecursionGuard` deltas (WO-131: the domain state only keeps the most recent trip per graph plus a running counter, no history list). */
47
+ interface GuardTripRecord {
48
+ readonly graphId: string;
49
+ readonly graphTitle: string;
50
+ readonly depth: number;
51
+ readonly activeAgents: number;
52
+ readonly reason: string;
53
+ readonly nodeId?: string | undefined;
54
+ readonly triggeredAt: number;
55
+ }
56
+
57
+ const MAX_GUARD_TRIP_HISTORY = 50;
58
+
59
+ /** The cross-panel jump Enter dispatches for a focused node, consumed by the panel-integration router. */
60
+ export interface OrchestrationNodeJump {
61
+ readonly kind: 'agent-jump' | 'task-jump';
62
+ readonly id: string;
63
+ }
64
+
65
+ function nodesOf(graph: OrchestrationGraphRecord): OrchestrationNodeRecord[] {
66
+ return graph.nodeOrder
67
+ .map((nodeId) => graph.nodes.get(nodeId))
68
+ .filter((node): node is OrchestrationNodeRecord => Boolean(node));
69
+ }
70
+
49
71
  export class OrchestrationPanel extends ScrollableListPanel<OrchestrationGraphRecord> {
50
72
  private readonly readModel?: UiReadModel<UiOrchestrationSnapshot>;
51
73
  private readonly unsub: (() => void) | null;
52
74
 
75
+ /** Stabilizes selection across re-sorts (new graphs prepend by createdAt) — keyed by graph id, not raw index. */
76
+ private _selectedGraphId: string | null = null;
77
+
78
+ /** Tab toggles which list up/down drives. */
79
+ private _focus: 'graph' | 'node' = 'graph';
80
+ /** Cursor within the selected graph's node list; persists across focus toggles. */
81
+ private _nodeCursor = 0;
82
+
83
+ /** Recursion-guard trip history accumulated from `lastRecursionGuard` deltas across all graphs, newest first. */
84
+ private _guardTripHistory: GuardTripRecord[] = [];
85
+ private readonly _seenGuardTripAt = new Map<string, number>();
86
+
87
+ /** Set by handleInput('enter') while node-focused; consumed by the panel-integration router immediately after. */
88
+ private _pendingNodeJump: OrchestrationNodeJump | null = null;
89
+
53
90
  public constructor(readModel?: UiReadModel<UiOrchestrationSnapshot>) {
54
- super('orchestration', 'Orchestration', 'Q', 'monitoring');
91
+ super('orchestration', 'Orchestration', '', 'runtime-ops');
55
92
  this.readModel = readModel;
56
93
  this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
57
94
  }
@@ -69,6 +106,42 @@ export class OrchestrationPanel extends ScrollableListPanel<OrchestrationGraphRe
69
106
  return [...this.readModel.getSnapshot().graphs].sort((a, b) => b.createdAt - a.createdAt);
70
107
  }
71
108
 
109
+ /** Keeps `selectedIndex` pointed at the same graph id after a re-sort (e.g. a new graph prepends). */
110
+ private _syncSelectionFromId(graphs: readonly OrchestrationGraphRecord[]): void {
111
+ if (this._selectedGraphId) {
112
+ const idx = graphs.findIndex((g) => g.id === this._selectedGraphId);
113
+ if (idx >= 0) {
114
+ this.selectedIndex = idx;
115
+ return;
116
+ }
117
+ }
118
+ this.selectedIndex = graphs.length === 0 ? 0 : Math.min(this.selectedIndex, graphs.length - 1);
119
+ this._selectedGraphId = graphs.at(this.selectedIndex)?.id ?? null;
120
+ }
121
+
122
+ /** Appends any newly-observed recursion-guard trips (across all graphs) to the local history list. */
123
+ private _captureGuardTrips(): void {
124
+ if (!this.readModel) return;
125
+ for (const graph of this.readModel.getSnapshot().graphs) {
126
+ const trip = graph.lastRecursionGuard;
127
+ if (!trip) continue;
128
+ if (this._seenGuardTripAt.get(graph.id) === trip.triggeredAt) continue;
129
+ this._seenGuardTripAt.set(graph.id, trip.triggeredAt);
130
+ this._guardTripHistory.unshift({
131
+ graphId: graph.id,
132
+ graphTitle: graph.title,
133
+ depth: trip.depth,
134
+ activeAgents: trip.activeAgents,
135
+ reason: trip.reason,
136
+ nodeId: trip.nodeId,
137
+ triggeredAt: trip.triggeredAt,
138
+ });
139
+ }
140
+ if (this._guardTripHistory.length > MAX_GUARD_TRIP_HISTORY) {
141
+ this._guardTripHistory.length = MAX_GUARD_TRIP_HISTORY;
142
+ }
143
+ }
144
+
72
145
  protected renderItem(
73
146
  graph: OrchestrationGraphRecord,
74
147
  index: number,
@@ -76,12 +149,17 @@ export class OrchestrationPanel extends ScrollableListPanel<OrchestrationGraphRe
76
149
  width: number,
77
150
  ): Line {
78
151
  const bg = selected ? C.selectBg : undefined;
152
+ const marker = selected ? '▸ ' : ' ';
153
+ const nodeCount = `${graph.nodeOrder.length}n`;
154
+ // Reserve room for marker(2) + status(10) + mode(17) + id(8) + nodes(~5) + gaps.
155
+ const titleBudget = Math.max(0, width - 47);
79
156
  return buildPanelLine(width, [
80
- [' ', C.label, bg],
157
+ [marker, selected ? C.value : C.dim, bg],
81
158
  [graph.status.padEnd(10), statusColor(graph.status), bg],
82
159
  [` ${graph.mode.padEnd(17)}`, C.value, bg],
83
160
  [` ${graph.id.slice(0, 8)} `, C.dim, bg],
84
- [graph.title.slice(0, Math.max(0, width - 39)), C.value, bg],
161
+ [`${nodeCount.padStart(4)} `, C.dim, bg],
162
+ [truncateDisplay(graph.title, titleBudget), C.value, bg],
85
163
  ]);
86
164
  }
87
165
 
@@ -94,9 +172,79 @@ export class OrchestrationPanel extends ScrollableListPanel<OrchestrationGraphRe
94
172
  }
95
173
 
96
174
  // ---------------------------------------------------------------------------
97
- // Input — base class handles all navigation (up/down/j/k/pageup/pagedown/g/G)
175
+ // Input — Tab switches graph-list/node-list focus; up/down drives whichever
176
+ // list has focus; Enter on a focused node jumps to the Inspector (agent
177
+ // nodes) or Tasks (task-backed nodes) via the panel-integration router.
98
178
  // ---------------------------------------------------------------------------
99
179
 
180
+ public override handleInput(key: string): boolean {
181
+ if (this.lastError !== null) this.clearError();
182
+
183
+ const graphs = this.getItems();
184
+ this._syncSelectionFromId(graphs);
185
+ const selectedGraph = this.getSelectedItem();
186
+ const nodes = selectedGraph ? nodesOf(selectedGraph) : [];
187
+
188
+ if (key === 'tab') {
189
+ if (nodes.length === 0) return false;
190
+ this._focus = this._focus === 'graph' ? 'node' : 'graph';
191
+ this._nodeCursor = Math.min(this._nodeCursor, Math.max(0, nodes.length - 1));
192
+ this.markDirty();
193
+ return true;
194
+ }
195
+
196
+ if (this._focus === 'node' && nodes.length > 0) {
197
+ switch (key) {
198
+ case 'up':
199
+ case 'k':
200
+ this._nodeCursor = Math.max(0, this._nodeCursor - 1);
201
+ this.markDirty();
202
+ return true;
203
+ case 'down':
204
+ case 'j':
205
+ this._nodeCursor = Math.min(nodes.length - 1, this._nodeCursor + 1);
206
+ this.markDirty();
207
+ return true;
208
+ case 'enter':
209
+ case 'return': {
210
+ const node = nodes[this._nodeCursor];
211
+ if (node?.agentId) {
212
+ this._pendingNodeJump = { kind: 'agent-jump', id: node.agentId };
213
+ return true;
214
+ }
215
+ if (node?.taskId) {
216
+ this._pendingNodeJump = { kind: 'task-jump', id: node.taskId };
217
+ return true;
218
+ }
219
+ return false;
220
+ }
221
+ default:
222
+ return false;
223
+ }
224
+ }
225
+
226
+ const consumed = super.handleInput(key);
227
+ if (consumed) {
228
+ const refreshed = this.getItems();
229
+ this._selectedGraphId = refreshed.at(this.selectedIndex)?.id ?? this._selectedGraphId;
230
+ // A different graph is selected — the node cursor no longer refers to a
231
+ // meaningful row until Tab re-engages node focus for the new graph.
232
+ this._focus = 'graph';
233
+ this._nodeCursor = 0;
234
+ }
235
+ return consumed;
236
+ }
237
+
238
+ /**
239
+ * Consumed by the panel-integration router immediately after handleInput
240
+ * returns true for the same Enter keystroke while node-focused.
241
+ */
242
+ public consumePendingNodeJump(): OrchestrationNodeJump | null {
243
+ const pending = this._pendingNodeJump;
244
+ this._pendingNodeJump = null;
245
+ return pending;
246
+ }
247
+
100
248
  // ---------------------------------------------------------------------------
101
249
  // Render — multi-section layout (posture + scrollable graphs + detail + nodes)
102
250
  // ---------------------------------------------------------------------------
@@ -114,7 +262,7 @@ export class OrchestrationPanel extends ScrollableListPanel<OrchestrationGraphRe
114
262
  width,
115
263
  ' Runtime store not wired into this panel yet.',
116
264
  'The orchestration workspace needs the live runtime store before it can show graphs, nodes, and recursion guard events.',
117
- [{ command: '/orchestration', summary: 'reopen from the shell-owned runtime once orchestration is active' }],
265
+ [],
118
266
  C,
119
267
  ),
120
268
  }],
@@ -124,17 +272,19 @@ export class OrchestrationPanel extends ScrollableListPanel<OrchestrationGraphRe
124
272
  return workspace;
125
273
  }
126
274
 
275
+ this._captureGuardTrips();
276
+
127
277
  const snapshot = this.readModel.getSnapshot();
128
278
  const graphs = this.getItems();
279
+ this._syncSelectionFromId(graphs);
129
280
  const postureLines = [
130
281
  buildKeyValueLine(width, [
131
282
  { label: 'graphs', value: String(snapshot.totalGraphs), valueColor: snapshot.totalGraphs > 0 ? C.value : C.dim },
132
- { label: 'active', value: String(snapshot.activeGraphIds.length), valueColor: snapshot.activeGraphIds.length > 0 ? C.running : C.dim },
283
+ { label: 'active', value: String(snapshot.activeGraphIds.length), valueColor: snapshot.activeGraphIds.length > 0 ? C.good : C.dim },
133
284
  { label: 'completed', value: String(snapshot.totalCompletedGraphs), valueColor: snapshot.totalCompletedGraphs > 0 ? C.completed : C.dim },
134
- { label: 'failed', value: String(snapshot.totalFailedGraphs), valueColor: snapshot.totalFailedGraphs > 0 ? C.failed : C.dim },
135
- { label: 'guards', value: String(snapshot.recursionGuardTrips), valueColor: snapshot.recursionGuardTrips > 0 ? C.blocked : C.dim },
285
+ { label: 'failed', value: String(snapshot.totalFailedGraphs), valueColor: snapshot.totalFailedGraphs > 0 ? C.bad : C.dim },
286
+ { label: 'guards', value: String(snapshot.recursionGuardTrips), valueColor: snapshot.recursionGuardTrips > 0 ? C.warn : C.dim },
136
287
  ], C),
137
- buildGuidanceLine(width, '/orchestration', 'inspect recursive execution posture, graph health, and node contract flow', C),
138
288
  ];
139
289
  if (graphs.length === 0) {
140
290
  this.needsRender = false;
@@ -164,15 +314,23 @@ export class OrchestrationPanel extends ScrollableListPanel<OrchestrationGraphRe
164
314
  }
165
315
 
166
316
  this.clampSelection();
167
- const selected = graphs[this.selectedIndex]!;
317
+ const selected = this.getSelectedItem()!;
318
+ const isActive = snapshot.activeGraphIds.includes(selected.id);
319
+ const elapsed = selected.startedAt
320
+ ? formatElapsed(Math.max(0, (selected.endedAt ?? Date.now()) - selected.startedAt))
321
+ : 'n/a';
168
322
  const detailLines: Line[] = [
169
323
  buildPanelLine(width, [
170
324
  [' Title: ', C.label],
171
- [selected.title, C.value],
325
+ [truncateDisplay(selected.title, Math.max(0, width - 11)), C.value],
326
+ ]),
327
+ buildPanelLine(width, [
172
328
  [' Status: ', C.label],
173
329
  [selected.status, statusColor(selected.status)],
174
330
  [' Mode: ', C.label],
175
331
  [selected.mode, C.value],
332
+ [' Live: ', C.label],
333
+ [isActive ? 'yes' : 'no', isActive ? C.good : C.dim],
176
334
  ]),
177
335
  buildPanelLine(width, [
178
336
  [' Nodes: ', C.label],
@@ -181,65 +339,65 @@ export class OrchestrationPanel extends ScrollableListPanel<OrchestrationGraphRe
181
339
  [selected.startedAt ? new Date(selected.startedAt).toLocaleTimeString() : 'n/a', C.dim],
182
340
  [' Ended: ', C.label],
183
341
  [selected.endedAt ? new Date(selected.endedAt).toLocaleTimeString() : 'n/a', C.dim],
342
+ [' Duration: ', C.label],
343
+ [elapsed, C.dim],
184
344
  ]),
185
345
  ];
186
- if (selected.lastRecursionGuard) {
187
- detailLines.push(buildPanelLine(width, [
188
- [' Recursion guard: ', C.label],
189
- [`depth ${selected.lastRecursionGuard.depth} active ${selected.lastRecursionGuard.activeAgents} ${selected.lastRecursionGuard.reason}`, C.blocked],
190
- ]));
191
- }
192
346
 
193
- const nodes = selected.nodeOrder
194
- .map((nodeId) => selected.nodes.get(nodeId))
195
- .filter((node): node is NonNullable<typeof node> => Boolean(node));
196
- const focusNode = nodes[0];
197
- if (focusNode?.contract) {
198
- const toolCount = focusNode.contract.allowedTools?.length ?? 0;
199
- const evidenceCount = focusNode.contract.requiredEvidence?.length ?? 0;
200
- const scopeCount = focusNode.contract.writeScope?.length ?? 0;
201
- detailLines.push(buildPanelLine(width, [
202
- [' Contract: ', C.label],
203
- [`tools ${toolCount}`, C.value],
204
- [' evidence ', C.label],
205
- [String(evidenceCount), C.value],
206
- [' write scope ', C.label],
207
- [String(scopeCount), C.value],
208
- ]));
209
- detailLines.push(buildPanelLine(width, [
210
- [' Flow: ', C.label],
211
- [focusNode.contract.executionProtocol ?? 'direct', C.value],
212
- [' Review: ', C.label],
213
- [focusNode.contract.reviewMode ?? 'none', C.value],
214
- [' Lane: ', C.label],
215
- [focusNode.contract.communicationLane ?? 'default', C.value],
216
- [' Inherits: ', C.label],
217
- [focusNode.contract.inheritsParentConstraints ? 'yes' : 'no', C.value],
218
- ]));
219
- }
347
+ const nodes = nodesOf(selected);
348
+ const nodeCursor = Math.min(this._nodeCursor, Math.max(0, nodes.length - 1));
349
+ const focusNode = nodes[nodeCursor];
350
+ detailLines.push(...this._buildNodeContractLines(width, focusNode));
220
351
 
221
352
  const nodeLines: Line[] = nodes.length === 0
222
353
  ? [buildPanelLine(width, [[' No nodes recorded yet.', C.dim]])]
223
- : nodes.map((node) => {
354
+ : nodes.map((node, idx) => {
355
+ const isNodeCursor = this._focus === 'node' && idx === nodeCursor;
356
+ const bg = isNodeCursor ? C.selectBg : undefined;
357
+ const marker = isNodeCursor ? '▸ ' : ' ';
224
358
  const depends = node.dependencyNodeIds.length > 0 ? ` deps:${node.dependencyNodeIds.length}` : '';
225
359
  return buildPanelLine(width, [
226
- [' ', C.label],
227
- [node.status.padEnd(10), statusColor(node.status)],
228
- [` ${node.role.padEnd(10)}`, C.value],
229
- [` ${node.id.slice(0, 8)} `, C.dim],
230
- [`${node.title}${depends}`.slice(0, Math.max(0, width - 34)), C.value],
360
+ [marker, isNodeCursor ? C.value : C.label, bg],
361
+ [node.status.padEnd(10), statusColor(node.status), bg],
362
+ [` ${node.role.padEnd(10)}`, C.value, bg],
363
+ [` ${node.id.slice(0, 8)} `, C.dim, bg],
364
+ [truncateDisplay(`${node.title}${depends}`, Math.max(0, width - 36)), C.value, bg],
231
365
  ]);
232
366
  });
233
367
 
368
+ const guardTripLines: Line[] = this._guardTripHistory.slice(0, 5).map((trip) => buildPanelLine(width, [
369
+ [` ${new Date(trip.triggeredAt).toLocaleTimeString()} `, C.dim],
370
+ [truncateDisplay(trip.graphTitle, Math.max(0, width - 60)), C.value],
371
+ [` depth ${trip.depth} active ${trip.activeAgents}`, C.warn],
372
+ [` ${truncateDisplay(trip.reason, Math.max(0, width - 50))}`, C.dim],
373
+ ]));
374
+
234
375
  const scrollableLines: Line[] = graphs.map((graph, index) =>
235
376
  this.renderItem(graph, index, index === this.selectedIndex, width),
236
377
  );
237
378
 
379
+ // Context-aware footer: surface position + only the keys that work in the
380
+ // currently-focused list (graph list vs node list).
381
+ const footerLines: Line[] = [
382
+ buildKeyboardHints(width, [
383
+ { keys: `${this.selectedIndex + 1}/${graphs.length}`, label: 'graph' },
384
+ ...(nodes.length > 0 ? [{ keys: 'Tab', label: this._focus === 'node' ? 'graph focus' : 'node focus' }] : []),
385
+ ...(this._focus === 'node'
386
+ ? [{ keys: '↑/↓', label: 'node' }, { keys: 'Enter', label: 'jump to node detail' }]
387
+ : [{ keys: '↑/↓', label: 'select' }, { keys: 'g/G', label: 'top/bottom' }, { keys: 'PgUp/PgDn', label: 'page' }]),
388
+ ], C),
389
+ ];
390
+
238
391
  const postureSection: PanelWorkspaceSection = { title: 'Orchestration posture', lines: postureLines };
239
392
  const selectedGraphSection: PanelWorkspaceSection = { title: 'Selected Graph', lines: detailLines };
240
393
  const nodesSection: PanelWorkspaceSection = { title: 'Nodes', lines: nodeLines };
394
+ const afterSections: PanelWorkspaceSection[] = [selectedGraphSection, nodesSection];
395
+ if (guardTripLines.length > 0) {
396
+ afterSections.push({ title: 'Guard trips (this session)', lines: guardTripLines });
397
+ }
241
398
  const graphsSection = resolveScrollablePanelSection(width, height, {
242
399
  intro,
400
+ footerLines,
243
401
  palette: C,
244
402
  beforeSections: [postureSection],
245
403
  section: {
@@ -250,24 +408,58 @@ export class OrchestrationPanel extends ScrollableListPanel<OrchestrationGraphRe
250
408
  minRows: 4,
251
409
  appendWindowSummary: { dimColor: C.dim },
252
410
  },
253
- afterSections: [selectedGraphSection, nodesSection],
411
+ afterSections,
254
412
  });
255
413
  this.scrollStart = graphsSection.scrollOffset;
256
414
 
257
415
  const sections: PanelWorkspaceSection[] = [
258
416
  postureSection,
259
417
  graphsSection.section,
260
- selectedGraphSection,
261
- nodesSection,
418
+ ...afterSections,
262
419
  ];
263
420
  this.needsRender = false;
264
421
  const lines = buildPanelWorkspace(width, height, {
265
422
  title: 'Orchestration Control Room',
266
423
  intro,
267
424
  sections,
425
+ footerLines,
268
426
  palette: C,
269
427
  });
270
428
  while (lines.length < height) lines.push(createEmptyLine(width));
271
429
  return lines.slice(0, height);
272
430
  }
431
+
432
+ /** Full node contract — allowedTools/requiredEvidence/writeScope/executionProtocol/reviewMode/communicationLane, plus inheritance. */
433
+ private _buildNodeContractLines(width: number, node: OrchestrationNodeRecord | undefined): Line[] {
434
+ if (!node) return [];
435
+ const contract = node.contract;
436
+ if (!contract) {
437
+ return [buildPanelLine(width, [[' Contract: ', C.label], ['none recorded', C.dim]])];
438
+ }
439
+ const list = (arr: string[] | undefined) => arr && arr.length > 0 ? arr.join(', ') : 'none';
440
+ return [
441
+ buildPanelLine(width, [
442
+ [' Allowed tools: ', C.label],
443
+ [truncateDisplay(list(contract.allowedTools), Math.max(0, width - 18)), C.value],
444
+ ]),
445
+ buildPanelLine(width, [
446
+ [' Required evidence: ', C.label],
447
+ [truncateDisplay(list(contract.requiredEvidence), Math.max(0, width - 22)), C.value],
448
+ ]),
449
+ buildPanelLine(width, [
450
+ [' Write scope: ', C.label],
451
+ [truncateDisplay(list(contract.writeScope), Math.max(0, width - 16)), C.value],
452
+ ]),
453
+ buildPanelLine(width, [
454
+ [' Flow: ', C.label],
455
+ [contract.executionProtocol ?? 'direct', C.value],
456
+ [' Review: ', C.label],
457
+ [contract.reviewMode ?? 'none', C.value],
458
+ [' Lane: ', C.label],
459
+ [contract.communicationLane ?? 'default', C.value],
460
+ [' Inherits: ', C.label],
461
+ [contract.inheritsParentConstraints ? 'yes' : 'no', C.value],
462
+ ]),
463
+ ];
464
+ }
273
465
  }