@pellux/goodvibes-tui 0.28.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 (118) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/input/commands/cost-runtime.ts +49 -0
  5. package/src/input/commands/operator-runtime.ts +5 -1
  6. package/src/input/commands.ts +2 -0
  7. package/src/input/handler-feed-routes.ts +12 -35
  8. package/src/input/handler-feed.ts +4 -8
  9. package/src/input/handler-shortcuts.ts +51 -0
  10. package/src/input/handler.ts +43 -6
  11. package/src/input/keybindings.ts +48 -8
  12. package/src/input/panel-integration-actions.ts +107 -1
  13. package/src/main.ts +5 -1
  14. package/src/panels/agent-inspector-panel.ts +125 -36
  15. package/src/panels/agent-inspector-shared.ts +144 -0
  16. package/src/panels/approval-panel.ts +67 -16
  17. package/src/panels/automation-control-panel.ts +368 -124
  18. package/src/panels/builtin/agent.ts +28 -37
  19. package/src/panels/builtin/development.ts +40 -32
  20. package/src/panels/builtin/knowledge.ts +15 -6
  21. package/src/panels/builtin/operations.ts +178 -117
  22. package/src/panels/builtin/session.ts +35 -9
  23. package/src/panels/builtin/shared.ts +98 -6
  24. package/src/panels/cockpit-panel.ts +232 -73
  25. package/src/panels/communication-panel.ts +58 -20
  26. package/src/panels/confirm-state.ts +8 -1
  27. package/src/panels/control-plane-panel.ts +351 -118
  28. package/src/panels/cost-tracker-panel.ts +165 -7
  29. package/src/panels/debug-panel.ts +312 -159
  30. package/src/panels/diff-panel.ts +203 -57
  31. package/src/panels/docs-panel.ts +152 -66
  32. package/src/panels/eval-panel.ts +220 -42
  33. package/src/panels/file-explorer-panel.ts +202 -122
  34. package/src/panels/file-preview-panel.ts +132 -57
  35. package/src/panels/git-panel.ts +309 -128
  36. package/src/panels/hooks-panel.ts +150 -22
  37. package/src/panels/incident-review-panel.ts +223 -35
  38. package/src/panels/index.ts +0 -4
  39. package/src/panels/intelligence-panel.ts +212 -86
  40. package/src/panels/knowledge-graph-panel.ts +461 -101
  41. package/src/panels/local-auth-panel.ts +240 -28
  42. package/src/panels/marketplace-panel.ts +193 -26
  43. package/src/panels/memory-panel.ts +78 -77
  44. package/src/panels/ops-control-panel.ts +146 -29
  45. package/src/panels/ops-strategy-panel.ts +72 -4
  46. package/src/panels/orchestration-panel.ts +231 -69
  47. package/src/panels/panel-list-panel.ts +154 -131
  48. package/src/panels/panel-manager.ts +97 -9
  49. package/src/panels/plan-dashboard-panel.ts +333 -17
  50. package/src/panels/plugins-panel.ts +184 -29
  51. package/src/panels/policy-panel.ts +210 -38
  52. package/src/panels/polish-core.ts +7 -2
  53. package/src/panels/polish.ts +23 -4
  54. package/src/panels/project-planning-answer-actions.ts +134 -0
  55. package/src/panels/project-planning-panel.ts +62 -113
  56. package/src/panels/provider-health-panel.ts +434 -518
  57. package/src/panels/provider-health-routes.ts +203 -0
  58. package/src/panels/provider-health-tracker.ts +194 -6
  59. package/src/panels/provider-health-views.ts +560 -0
  60. package/src/panels/qr-panel.ts +116 -30
  61. package/src/panels/remote-panel.ts +114 -36
  62. package/src/panels/routes-panel.ts +63 -22
  63. package/src/panels/sandbox-panel.ts +174 -42
  64. package/src/panels/scrollable-list-panel.ts +19 -135
  65. package/src/panels/security-panel.ts +133 -33
  66. package/src/panels/services-panel.ts +116 -64
  67. package/src/panels/session-browser-panel.ts +73 -2
  68. package/src/panels/session-maintenance.ts +4 -122
  69. package/src/panels/settings-sync-panel.ts +335 -72
  70. package/src/panels/skills-panel.ts +157 -89
  71. package/src/panels/subscription-panel.ts +86 -33
  72. package/src/panels/symbol-outline-panel.ts +248 -108
  73. package/src/panels/system-messages-panel.ts +114 -13
  74. package/src/panels/tasks-panel.ts +326 -139
  75. package/src/panels/thinking-panel.ts +43 -10
  76. package/src/panels/token-budget-panel.ts +194 -18
  77. package/src/panels/tool-inspector-panel.ts +144 -34
  78. package/src/panels/types.ts +40 -4
  79. package/src/panels/work-plan-panel.ts +280 -17
  80. package/src/panels/worktree-panel.ts +175 -42
  81. package/src/panels/wrfc-panel.ts +116 -24
  82. package/src/renderer/conversation-overlays.ts +25 -11
  83. package/src/renderer/footer-tips.ts +41 -0
  84. package/src/renderer/fullscreen-primitives.ts +22 -16
  85. package/src/renderer/help-overlay.ts +91 -14
  86. package/src/renderer/hint-grammar.ts +52 -0
  87. package/src/renderer/layout.ts +7 -7
  88. package/src/renderer/modal-factory.ts +23 -15
  89. package/src/renderer/model-picker-overlay.ts +21 -7
  90. package/src/renderer/overlay-box.ts +16 -10
  91. package/src/renderer/process-indicator.ts +3 -1
  92. package/src/renderer/progress.ts +5 -4
  93. package/src/renderer/search-overlay.ts +27 -6
  94. package/src/renderer/session-picker-modal.ts +6 -4
  95. package/src/renderer/settings-modal.ts +70 -10
  96. package/src/renderer/shell-surface.ts +41 -15
  97. package/src/renderer/status-glyphs.ts +11 -9
  98. package/src/renderer/theme.ts +60 -3
  99. package/src/renderer/ui-factory.ts +41 -30
  100. package/src/renderer/ui-primitives.ts +23 -2
  101. package/src/runtime/bootstrap-shell.ts +35 -18
  102. package/src/runtime/diagnostics/panels/index.ts +0 -3
  103. package/src/shell/ui-openers.ts +14 -22
  104. package/src/utils/format-duration.ts +2 -2
  105. package/src/utils/splash-lines.ts +44 -3
  106. package/src/version.ts +1 -1
  107. package/src/work-plans/work-plan-store.ts +13 -0
  108. package/src/panels/agent-logs-panel.ts +0 -635
  109. package/src/panels/agent-logs-shared.ts +0 -129
  110. package/src/panels/context-visualizer-panel.ts +0 -238
  111. package/src/panels/forensics-panel.ts +0 -378
  112. package/src/panels/provider-account-snapshot.ts +0 -259
  113. package/src/panels/provider-accounts-panel.ts +0 -255
  114. package/src/panels/provider-stats-panel.ts +0 -391
  115. package/src/panels/schedule-panel.ts +0 -365
  116. package/src/panels/watchers-panel.ts +0 -213
  117. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  118. package/src/runtime/diagnostics/panels/policy.ts +0 -177
@@ -11,11 +11,10 @@ import { createEmptyLine } from '../types/grid.ts';
11
11
  import { truncateDisplay } from '../utils/terminal-width.ts';
12
12
  import { ScrollableListPanel } from './scrollable-list-panel.ts';
13
13
  import type { UiOrchestrationSnapshot, UiReadModel } from '../runtime/ui-read-models.ts';
14
- import type { OrchestrationGraphRecord } from '@/runtime/index.ts';
14
+ import type { OrchestrationGraphRecord, OrchestrationNodeRecord } from '@/runtime/index.ts';
15
15
  import { formatElapsed } from '../utils/format-elapsed.ts';
16
16
  import {
17
17
  buildEmptyState,
18
- buildGuidanceLine,
19
18
  buildKeyboardHints,
20
19
  buildKeyValueLine,
21
20
  buildPanelLine,
@@ -27,34 +26,69 @@ import {
27
26
  type PanelWorkspaceSection,
28
27
  } from './polish.ts';
29
28
 
29
+ // Domain accents only; base chrome (header/headerBg/info/good/warn/bad/
30
+ // selectBg) comes from DEFAULT_PANEL_PALETTE.
30
31
  const C = extendPalette(DEFAULT_PANEL_PALETTE, {
31
- header: '#94a3b8',
32
- headerBg: '#1e293b',
33
- running: '#22c55e',
34
- ready: '#38bdf8',
35
- blocked: '#f59e0b',
36
- failed: '#ef4444',
37
- completed: '#a78bfa',
38
- selectBg: '#0f172a',
32
+ completed: '#a78bfa', // completed-graph badge, distinct from running/good
39
33
  } as const);
40
34
 
41
35
  function statusColor(status: string): string {
42
36
  switch (status) {
43
- case 'ready': return C.ready;
44
- case 'running': return C.running;
45
- case 'blocked': return C.blocked;
46
- 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;
47
41
  case 'completed': return C.completed;
48
42
  default: return C.dim;
49
43
  }
50
44
  }
51
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
+
52
71
  export class OrchestrationPanel extends ScrollableListPanel<OrchestrationGraphRecord> {
53
72
  private readonly readModel?: UiReadModel<UiOrchestrationSnapshot>;
54
73
  private readonly unsub: (() => void) | null;
55
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
+
56
90
  public constructor(readModel?: UiReadModel<UiOrchestrationSnapshot>) {
57
- super('orchestration', 'Orchestration', 'Q', 'monitoring');
91
+ super('orchestration', 'Orchestration', '', 'runtime-ops');
58
92
  this.readModel = readModel;
59
93
  this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
60
94
  }
@@ -72,6 +106,42 @@ export class OrchestrationPanel extends ScrollableListPanel<OrchestrationGraphRe
72
106
  return [...this.readModel.getSnapshot().graphs].sort((a, b) => b.createdAt - a.createdAt);
73
107
  }
74
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
+
75
145
  protected renderItem(
76
146
  graph: OrchestrationGraphRecord,
77
147
  index: number,
@@ -102,9 +172,79 @@ export class OrchestrationPanel extends ScrollableListPanel<OrchestrationGraphRe
102
172
  }
103
173
 
104
174
  // ---------------------------------------------------------------------------
105
- // 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.
106
178
  // ---------------------------------------------------------------------------
107
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
+
108
248
  // ---------------------------------------------------------------------------
109
249
  // Render — multi-section layout (posture + scrollable graphs + detail + nodes)
110
250
  // ---------------------------------------------------------------------------
@@ -122,7 +262,7 @@ export class OrchestrationPanel extends ScrollableListPanel<OrchestrationGraphRe
122
262
  width,
123
263
  ' Runtime store not wired into this panel yet.',
124
264
  'The orchestration workspace needs the live runtime store before it can show graphs, nodes, and recursion guard events.',
125
- [{ command: '/orchestration', summary: 'reopen from the shell-owned runtime once orchestration is active' }],
265
+ [],
126
266
  C,
127
267
  ),
128
268
  }],
@@ -132,17 +272,19 @@ export class OrchestrationPanel extends ScrollableListPanel<OrchestrationGraphRe
132
272
  return workspace;
133
273
  }
134
274
 
275
+ this._captureGuardTrips();
276
+
135
277
  const snapshot = this.readModel.getSnapshot();
136
278
  const graphs = this.getItems();
279
+ this._syncSelectionFromId(graphs);
137
280
  const postureLines = [
138
281
  buildKeyValueLine(width, [
139
282
  { label: 'graphs', value: String(snapshot.totalGraphs), valueColor: snapshot.totalGraphs > 0 ? C.value : C.dim },
140
- { 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 },
141
284
  { label: 'completed', value: String(snapshot.totalCompletedGraphs), valueColor: snapshot.totalCompletedGraphs > 0 ? C.completed : C.dim },
142
- { label: 'failed', value: String(snapshot.totalFailedGraphs), valueColor: snapshot.totalFailedGraphs > 0 ? C.failed : C.dim },
143
- { 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 },
144
287
  ], C),
145
- buildGuidanceLine(width, '/orchestration', 'inspect recursive execution posture, graph health, and node contract flow', C),
146
288
  ];
147
289
  if (graphs.length === 0) {
148
290
  this.needsRender = false;
@@ -172,7 +314,7 @@ export class OrchestrationPanel extends ScrollableListPanel<OrchestrationGraphRe
172
314
  }
173
315
 
174
316
  this.clampSelection();
175
- const selected = graphs[this.selectedIndex]!;
317
+ const selected = this.getSelectedItem()!;
176
318
  const isActive = snapshot.activeGraphIds.includes(selected.id);
177
319
  const elapsed = selected.startedAt
178
320
  ? formatElapsed(Math.max(0, (selected.endedAt ?? Date.now()) - selected.startedAt))
@@ -188,7 +330,7 @@ export class OrchestrationPanel extends ScrollableListPanel<OrchestrationGraphRe
188
330
  [' Mode: ', C.label],
189
331
  [selected.mode, C.value],
190
332
  [' Live: ', C.label],
191
- [isActive ? 'yes' : 'no', isActive ? C.running : C.dim],
333
+ [isActive ? 'yes' : 'no', isActive ? C.good : C.dim],
192
334
  ]),
193
335
  buildPanelLine(width, [
194
336
  [' Nodes: ', C.label],
@@ -201,71 +343,58 @@ export class OrchestrationPanel extends ScrollableListPanel<OrchestrationGraphRe
201
343
  [elapsed, C.dim],
202
344
  ]),
203
345
  ];
204
- if (selected.lastRecursionGuard) {
205
- detailLines.push(buildPanelLine(width, [
206
- [' Recursion guard: ', C.label],
207
- [`depth ${selected.lastRecursionGuard.depth} active ${selected.lastRecursionGuard.activeAgents} ${selected.lastRecursionGuard.reason}`, C.blocked],
208
- ]));
209
- }
210
346
 
211
- const nodes = selected.nodeOrder
212
- .map((nodeId) => selected.nodes.get(nodeId))
213
- .filter((node): node is NonNullable<typeof node> => Boolean(node));
214
- const focusNode = nodes[0];
215
- if (focusNode?.contract) {
216
- const toolCount = focusNode.contract.allowedTools?.length ?? 0;
217
- const evidenceCount = focusNode.contract.requiredEvidence?.length ?? 0;
218
- const scopeCount = focusNode.contract.writeScope?.length ?? 0;
219
- detailLines.push(buildPanelLine(width, [
220
- [' Contract: ', C.label],
221
- [`tools ${toolCount}`, C.value],
222
- [' evidence ', C.label],
223
- [String(evidenceCount), C.value],
224
- [' write scope ', C.label],
225
- [String(scopeCount), C.value],
226
- ]));
227
- detailLines.push(buildPanelLine(width, [
228
- [' Flow: ', C.label],
229
- [focusNode.contract.executionProtocol ?? 'direct', C.value],
230
- [' Review: ', C.label],
231
- [focusNode.contract.reviewMode ?? 'none', C.value],
232
- [' Lane: ', C.label],
233
- [focusNode.contract.communicationLane ?? 'default', C.value],
234
- [' Inherits: ', C.label],
235
- [focusNode.contract.inheritsParentConstraints ? 'yes' : 'no', C.value],
236
- ]));
237
- }
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));
238
351
 
239
352
  const nodeLines: Line[] = nodes.length === 0
240
353
  ? [buildPanelLine(width, [[' No nodes recorded yet.', C.dim]])]
241
- : 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 ? '▸ ' : ' ';
242
358
  const depends = node.dependencyNodeIds.length > 0 ? ` deps:${node.dependencyNodeIds.length}` : '';
243
359
  return buildPanelLine(width, [
244
- [' ', C.label],
245
- [node.status.padEnd(10), statusColor(node.status)],
246
- [` ${node.role.padEnd(10)}`, C.value],
247
- [` ${node.id.slice(0, 8)} `, C.dim],
248
- [truncateDisplay(`${node.title}${depends}`, 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],
249
365
  ]);
250
366
  });
251
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
+
252
375
  const scrollableLines: Line[] = graphs.map((graph, index) =>
253
376
  this.renderItem(graph, index, index === this.selectedIndex, width),
254
377
  );
255
378
 
256
- // Context-aware footer: surface position + only the keys that work here.
379
+ // Context-aware footer: surface position + only the keys that work in the
380
+ // currently-focused list (graph list vs node list).
257
381
  const footerLines: Line[] = [
258
382
  buildKeyboardHints(width, [
259
383
  { keys: `${this.selectedIndex + 1}/${graphs.length}`, label: 'graph' },
260
- { keys: '↑/↓', label: 'select' },
261
- { keys: 'g/G', label: 'top/bottom' },
262
- { keys: 'PgUp/PgDn', label: 'page' },
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' }]),
263
388
  ], C),
264
389
  ];
265
390
 
266
391
  const postureSection: PanelWorkspaceSection = { title: 'Orchestration posture', lines: postureLines };
267
392
  const selectedGraphSection: PanelWorkspaceSection = { title: 'Selected Graph', lines: detailLines };
268
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
+ }
269
398
  const graphsSection = resolveScrollablePanelSection(width, height, {
270
399
  intro,
271
400
  footerLines,
@@ -279,15 +408,14 @@ export class OrchestrationPanel extends ScrollableListPanel<OrchestrationGraphRe
279
408
  minRows: 4,
280
409
  appendWindowSummary: { dimColor: C.dim },
281
410
  },
282
- afterSections: [selectedGraphSection, nodesSection],
411
+ afterSections,
283
412
  });
284
413
  this.scrollStart = graphsSection.scrollOffset;
285
414
 
286
415
  const sections: PanelWorkspaceSection[] = [
287
416
  postureSection,
288
417
  graphsSection.section,
289
- selectedGraphSection,
290
- nodesSection,
418
+ ...afterSections,
291
419
  ];
292
420
  this.needsRender = false;
293
421
  const lines = buildPanelWorkspace(width, height, {
@@ -300,4 +428,38 @@ export class OrchestrationPanel extends ScrollableListPanel<OrchestrationGraphRe
300
428
  while (lines.length < height) lines.push(createEmptyLine(width));
301
429
  return lines.slice(0, height);
302
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
+ }
303
465
  }