@pellux/goodvibes-tui 0.26.0 → 0.28.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 (148) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +12 -7
  3. package/docs/foundation-artifacts/operator-contract.json +4 -1
  4. package/package.json +2 -2
  5. package/src/cli/bundle-command.ts +2 -1
  6. package/src/cli/service-posture.ts +2 -1
  7. package/src/core/context-usage.ts +53 -0
  8. package/src/core/conversation-rendering.ts +2 -2
  9. package/src/core/conversation.ts +53 -8
  10. package/src/core/session-recovery.ts +26 -18
  11. package/src/core/system-message-router.ts +42 -26
  12. package/src/core/turn-event-wiring.ts +13 -16
  13. package/src/daemon/handlers/calendar/caldav-client.ts +2 -1
  14. package/src/daemon/handlers/inbox/index.ts +2 -1
  15. package/src/daemon/handlers/inbox/poller.ts +2 -1
  16. package/src/daemon/handlers/inbox/providers/discord.ts +2 -1
  17. package/src/daemon/handlers/inbox/providers/email.ts +2 -1
  18. package/src/daemon/handlers/inbox/providers/route-util.ts +2 -1
  19. package/src/daemon/handlers/inbox/providers/slack.ts +2 -1
  20. package/src/daemon/handlers/triage/integration.ts +2 -1
  21. package/src/daemon/handlers/triage/pipeline.ts +2 -1
  22. package/src/daemon/handlers/triage/tagger/discord.ts +2 -1
  23. package/src/daemon/handlers/triage/tagger/imap.ts +4 -3
  24. package/src/export/gist-uploader.ts +3 -1
  25. package/src/input/command-registry.ts +1 -0
  26. package/src/input/commands/cloudflare-runtime.ts +2 -1
  27. package/src/input/commands/eval.ts +4 -3
  28. package/src/input/commands/guidance-runtime.ts +2 -4
  29. package/src/input/commands/health-runtime.ts +13 -7
  30. package/src/input/commands/knowledge.ts +2 -1
  31. package/src/input/commands/runtime-services.ts +40 -10
  32. package/src/input/handler-command-route.ts +1 -1
  33. package/src/input/handler-feed-routes.ts +61 -4
  34. package/src/input/handler-feed.ts +13 -0
  35. package/src/input/handler-interactions.ts +2 -1
  36. package/src/input/handler-modal-routes.ts +2 -1
  37. package/src/input/handler-onboarding-cloudflare.ts +2 -1
  38. package/src/input/handler-onboarding.ts +8 -8
  39. package/src/input/handler-picker-routes.ts +18 -13
  40. package/src/input/handler-ui-state.ts +5 -1
  41. package/src/input/keybindings.ts +5 -0
  42. package/src/input/model-picker.ts +5 -0
  43. package/src/input/panel-integration-actions.ts +10 -0
  44. package/src/input/tts-settings-actions.ts +2 -1
  45. package/src/main.ts +52 -60
  46. package/src/panels/agent-inspector-panel.ts +90 -15
  47. package/src/panels/agent-inspector-shared.ts +3 -3
  48. package/src/panels/agent-logs-panel.ts +149 -72
  49. package/src/panels/approval-panel.ts +146 -95
  50. package/src/panels/automation-control-panel.ts +24 -1
  51. package/src/panels/base-panel.ts +28 -3
  52. package/src/panels/builtin/agent.ts +3 -1
  53. package/src/panels/builtin/development.ts +2 -1
  54. package/src/panels/builtin/session.ts +1 -1
  55. package/src/panels/cockpit-panel.ts +24 -5
  56. package/src/panels/cockpit-read-model.ts +2 -1
  57. package/src/panels/communication-panel.ts +108 -43
  58. package/src/panels/context-visualizer-panel.ts +49 -15
  59. package/src/panels/control-plane-panel.ts +27 -1
  60. package/src/panels/cost-tracker-panel.ts +87 -65
  61. package/src/panels/debug-panel.ts +61 -37
  62. package/src/panels/diff-panel.ts +59 -30
  63. package/src/panels/docs-panel.ts +30 -24
  64. package/src/panels/eval-panel.ts +130 -81
  65. package/src/panels/expandable-list-panel.ts +189 -0
  66. package/src/panels/file-explorer-panel.ts +42 -42
  67. package/src/panels/file-preview-panel.ts +11 -3
  68. package/src/panels/forensics-panel.ts +27 -13
  69. package/src/panels/git-panel.ts +65 -84
  70. package/src/panels/hooks-panel.ts +36 -2
  71. package/src/panels/incident-review-panel.ts +31 -10
  72. package/src/panels/intelligence-panel.ts +152 -71
  73. package/src/panels/knowledge-graph-panel.ts +89 -27
  74. package/src/panels/local-auth-panel.ts +17 -11
  75. package/src/panels/marketplace-panel.ts +33 -13
  76. package/src/panels/memory-panel.ts +7 -5
  77. package/src/panels/ops-control-panel.ts +69 -7
  78. package/src/panels/ops-strategy-panel.ts +61 -43
  79. package/src/panels/orchestration-panel.ts +34 -4
  80. package/src/panels/panel-list-panel.ts +33 -8
  81. package/src/panels/panel-manager.ts +23 -4
  82. package/src/panels/plan-dashboard-panel.ts +183 -66
  83. package/src/panels/plugins-panel.ts +48 -10
  84. package/src/panels/policy-panel.ts +60 -23
  85. package/src/panels/polish-core.ts +157 -0
  86. package/src/panels/polish-tables.ts +258 -0
  87. package/src/panels/polish.ts +44 -145
  88. package/src/panels/project-planning-panel.ts +27 -4
  89. package/src/panels/provider-accounts-panel.ts +55 -18
  90. package/src/panels/provider-health-panel.ts +118 -87
  91. package/src/panels/provider-stats-panel.ts +47 -22
  92. package/src/panels/qr-panel.ts +23 -11
  93. package/src/panels/remote-panel.ts +12 -5
  94. package/src/panels/routes-panel.ts +27 -5
  95. package/src/panels/sandbox-panel.ts +62 -27
  96. package/src/panels/schedule-panel.ts +44 -24
  97. package/src/panels/scrollable-list-panel.ts +124 -10
  98. package/src/panels/security-panel.ts +72 -20
  99. package/src/panels/services-panel.ts +68 -22
  100. package/src/panels/session-browser-panel.ts +42 -26
  101. package/src/panels/session-maintenance.ts +2 -2
  102. package/src/panels/settings-sync-panel.ts +30 -15
  103. package/src/panels/skills-panel.ts +2 -1
  104. package/src/panels/subscription-panel.ts +21 -3
  105. package/src/panels/symbol-outline-panel.ts +40 -47
  106. package/src/panels/system-messages-panel.ts +60 -27
  107. package/src/panels/tasks-panel.ts +36 -14
  108. package/src/panels/thinking-panel.ts +32 -36
  109. package/src/panels/token-budget-panel.ts +80 -53
  110. package/src/panels/tool-inspector-panel.ts +37 -39
  111. package/src/panels/types.ts +25 -0
  112. package/src/panels/watchers-panel.ts +25 -5
  113. package/src/panels/work-plan-panel.ts +127 -35
  114. package/src/panels/worktree-panel.ts +65 -20
  115. package/src/panels/wrfc-panel-format.ts +133 -0
  116. package/src/panels/wrfc-panel.ts +53 -147
  117. package/src/renderer/agent-detail-modal.ts +2 -2
  118. package/src/renderer/compaction-preview.ts +2 -1
  119. package/src/renderer/compositor.ts +46 -43
  120. package/src/renderer/modal-utils.ts +0 -10
  121. package/src/renderer/model-picker-overlay.ts +9 -4
  122. package/src/renderer/model-workspace.ts +2 -3
  123. package/src/renderer/panel-composite.ts +7 -20
  124. package/src/renderer/panel-workspace-bar.ts +14 -8
  125. package/src/renderer/process-modal.ts +2 -2
  126. package/src/renderer/progress.ts +3 -2
  127. package/src/renderer/tab-strip.ts +148 -34
  128. package/src/renderer/ui-factory.ts +4 -6
  129. package/src/runtime/bootstrap-command-context.ts +3 -0
  130. package/src/runtime/bootstrap-command-parts.ts +3 -1
  131. package/src/runtime/bootstrap-core.ts +31 -31
  132. package/src/runtime/bootstrap-shell.ts +1 -0
  133. package/src/runtime/onboarding/apply.ts +4 -3
  134. package/src/runtime/onboarding/snapshot.ts +4 -3
  135. package/src/runtime/process-lifecycle.ts +195 -0
  136. package/src/runtime/services.ts +4 -1
  137. package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
  138. package/src/runtime/ui/model-picker/types.ts +4 -0
  139. package/src/runtime/wrfc-persistence.ts +20 -5
  140. package/src/shell/ui-openers.ts +3 -2
  141. package/src/utils/format-duration.ts +55 -0
  142. package/src/utils/format-number.ts +71 -0
  143. package/src/verification/live-verifier.ts +4 -3
  144. package/src/version.ts +1 -1
  145. package/src/core/context-auto-compact.ts +0 -110
  146. package/src/panels/panel-picker.ts +0 -106
  147. package/src/renderer/panel-picker-overlay.ts +0 -202
  148. package/src/renderer/panel-tab-bar.ts +0 -69
@@ -1,5 +1,6 @@
1
1
  import type { Line } from '../types/grid.ts';
2
2
  import { createEmptyLine } from '../types/grid.ts';
3
+ import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
3
4
  import { ScrollableListPanel } from './scrollable-list-panel.ts';
4
5
  import {
5
6
  buildDetailBlock,
@@ -136,8 +137,8 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
136
137
 
137
138
  protected renderItem(user: LocalAuthUser, _index: number, selected: boolean, width: number): Line {
138
139
  return buildPanelListRow(width, [
139
- { text: user.username.padEnd(20), fg: C.value },
140
- { text: ` roles=${formatRoles(user.roles)}`.slice(0, Math.max(0, width - 24)), fg: C.info },
140
+ { text: fitDisplay(user.username, 20), fg: C.value },
141
+ { text: truncateDisplay(` roles=${formatRoles(user.roles)}`, Math.max(0, width - 24)), fg: C.info },
141
142
  ], C, { selected });
142
143
  }
143
144
 
@@ -171,10 +172,10 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
171
172
  [' bootstrap ', C.label],
172
173
  [snapshot.bootstrapCredentialPresent ? 'present' : 'cleared', snapshot.bootstrapCredentialPresent ? C.warn : C.good],
173
174
  ]),
174
- buildPanelLine(width, [[' user store ', C.label], [snapshot.userStorePath.slice(0, Math.max(0, width - 13)), C.dim]]),
175
- buildPanelLine(width, [[' bootstrap file ', C.label], [snapshot.bootstrapCredentialPath.slice(0, Math.max(0, width - 18)), C.dim]]),
175
+ buildPanelLine(width, [[' user store ', C.label], [truncateDisplay(snapshot.userStorePath, Math.max(0, width - 13)), C.dim]]),
176
+ buildPanelLine(width, [[' bootstrap file ', C.label], [truncateDisplay(snapshot.bootstrapCredentialPath, Math.max(0, width - 18)), C.dim]]),
176
177
  ...(issueMessages.length > 0
177
- ? issueMessages.map((issue) => buildPanelLine(width, [[` issue: ${issue}`.slice(0, Math.max(0, width)), C.warn]]))
178
+ ? issueMessages.map((issue) => buildPanelLine(width, [[truncateDisplay(` issue: ${issue}`, width), C.warn]]))
178
179
  : [buildPanelLine(width, [[' local auth posture looks healthy.', C.good]])]),
179
180
  buildGuidanceLine(width, '/auth local rotate-password <user>', 'open masked password entry for the selected user (no plaintext in history)', C),
180
181
  ], C),
@@ -198,19 +199,20 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
198
199
  if (selected) {
199
200
  footerLines.push(
200
201
  ...buildDetailBlock(width, 'Selected user', [
201
- buildPanelLine(width, [[' username ', C.label], [selected.username, C.value], [' roles ', C.label], [formatRoles(selected.roles).slice(0, Math.max(0, width - 23)), C.info]]),
202
- buildPanelLine(width, [[` next: /auth local rotate-password ${selected.username}`.slice(0, Math.max(0, width)), C.dim]]),
203
- buildPanelLine(width, [[` next: /auth local delete-user ${selected.username}`.slice(0, Math.max(0, width)), C.dim]]),
202
+ buildPanelLine(width, [[' username ', C.label], [selected.username, C.value], [' roles ', C.label], [truncateDisplay(formatRoles(selected.roles), Math.max(0, width - 23)), C.info]]),
203
+ buildPanelLine(width, [[truncateDisplay(` next: /auth local rotate-password ${selected.username}`, width), C.dim]]),
204
+ buildPanelLine(width, [[truncateDisplay(` next: /auth local delete-user ${selected.username}`, width), C.dim]]),
204
205
  ], C),
205
206
  );
206
207
  }
207
208
 
208
209
  if (snapshot.sessions.length > 0) {
210
+ footerLines.push(buildPanelLine(width, [[` Active sessions (${snapshot.sessions.length})`, C.label]]));
209
211
  footerLines.push(
210
212
  ...snapshot.sessions.slice(0, 8).map((session) => buildPanelLine(width, [
211
213
  [' ', C.label],
212
- [session.username.padEnd(18), C.value],
213
- [` expires ${new Date(session.expiresAt).toLocaleString()}`.slice(0, Math.max(0, width - 20)), C.dim],
214
+ [fitDisplay(session.username, 18), C.value],
215
+ [truncateDisplay(` expires ${new Date(session.expiresAt).toLocaleString()}`, Math.max(0, width - 20)), C.dim],
214
216
  ])),
215
217
  );
216
218
  }
@@ -220,6 +222,10 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
220
222
  title: 'Local Auth Control Room',
221
223
  header: headerLines,
222
224
  footer: footerLines,
225
+ hints: [
226
+ { keys: '↑/↓', label: 'select user' },
227
+ { keys: '/auth local rotate-password', label: 'masked entry' },
228
+ ],
223
229
  });
224
230
  }
225
231
 
@@ -233,7 +239,7 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
233
239
  const promptLines: Line[] = [
234
240
  buildPanelLine(width, [[` ${actionLabel}: ${state.username}`, C.value]]),
235
241
  buildPanelLine(width, [['', C.label]]),
236
- buildPanelLine(width, [[' Password ', C.label], [maskedDisplay.slice(0, Math.max(0, width - 12)), C.info]]),
242
+ buildPanelLine(width, [[' Password ', C.label], [truncateDisplay(maskedDisplay, Math.max(0, width - 12)), C.info]]),
237
243
  buildPanelLine(width, [['', C.label]]),
238
244
  buildPanelLine(width, [[' [Enter] Confirm [Esc] Cancel [Backspace] Delete char', C.dim]]),
239
245
  ];
@@ -1,13 +1,14 @@
1
1
  import type { Line } from '../types/grid.ts';
2
+ import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
2
3
  import { ScrollableListPanel } from './scrollable-list-panel.ts';
3
4
  import {
4
5
  buildEmptyState,
5
6
  buildGuidanceLine,
6
7
  buildKeyValueLine,
8
+ buildKeyboardHints,
7
9
  buildPanelLine,
8
10
  buildPanelWorkspace,
9
11
  DEFAULT_PANEL_PALETTE,
10
- type PanelWorkspaceSection,
11
12
  } from './polish.ts';
12
13
  import {
13
14
  type EcosystemCatalogPathOptions,
@@ -18,6 +19,7 @@ import {
18
19
  type EcosystemEntryKind,
19
20
  } from '@/runtime/index.ts';
20
21
  import type { UiMarketplaceSnapshot, UiReadModel } from '../runtime/ui-read-models.ts';
22
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
21
23
 
22
24
  const C = {
23
25
  ...DEFAULT_PANEL_PALETTE,
@@ -44,9 +46,17 @@ export class MarketplacePanel extends ScrollableListPanel<MarketplaceRow> {
44
46
  private readonly ecosystemPaths?: EcosystemCatalogPathOptions,
45
47
  ) {
46
48
  super('marketplace', 'Marketplace', 'M', 'monitoring');
49
+ this.filterEnabled = true;
50
+ this.filterLabel = 'Filter marketplace';
47
51
  this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
48
52
  }
49
53
 
54
+ protected override filterMatches(row: MarketplaceRow, q: string): boolean {
55
+ return row.kind.toLowerCase().includes(q)
56
+ || row.entry.name.toLowerCase().includes(q)
57
+ || (row.entry.provenance ?? 'local').toLowerCase().includes(q);
58
+ }
59
+
50
60
  public override onDestroy(): void {
51
61
  this.unsub?.();
52
62
  }
@@ -70,8 +80,8 @@ export class MarketplacePanel extends ScrollableListPanel<MarketplaceRow> {
70
80
  return buildPanelLine(width, [
71
81
  [' ', C.label, bg],
72
82
  [row.kind.padEnd(11), C.info, bg],
73
- [row.entry.name.slice(0, 20).padEnd(20), C.value, bg],
74
- [` ${provenance.slice(0, 16).padEnd(16)}`, provenance === 'local' ? C.dim : C.info, bg],
83
+ [fitDisplay(row.entry.name, 20), C.value, bg],
84
+ [` ${fitDisplay(provenance, 16)}`, provenance === 'local' ? C.dim : C.info, bg],
75
85
  [` ${(row.installed ? 'INSTALLED' : 'CURATED').padEnd(9)} `, statusColor(row.installed), bg],
76
86
  [` ${row.entry.version ?? 'n/a'}`, C.dim, bg],
77
87
  ]);
@@ -114,7 +124,7 @@ export class MarketplacePanel extends ScrollableListPanel<MarketplaceRow> {
114
124
  this.clearError();
115
125
  } catch (e) {
116
126
  // I2: surface catalog load failure
117
- this.setError(`Catalog load failed: ${e instanceof Error ? e.message : String(e)}`);
127
+ this.setError(`Catalog load failed: ${summarizeError(e)}`);
118
128
  }
119
129
  }
120
130
 
@@ -166,13 +176,13 @@ export class MarketplacePanel extends ScrollableListPanel<MarketplaceRow> {
166
176
  const recommendationLines = recommendations.length > 0
167
177
  ? recommendations.slice(0, 4).map((recommendation) => buildPanelLine(width, [
168
178
  [' ', C.label],
169
- [`${recommendation.kind} ${recommendation.entry.id}`.slice(0, 28).padEnd(28), C.info],
170
- [` ${recommendation.title}`.slice(0, Math.max(0, width - 31)), C.dim],
179
+ [fitDisplay(`${recommendation.kind} ${recommendation.entry.id}`, 28), C.info],
180
+ [truncateDisplay(` ${recommendation.title}`, Math.max(0, width - 31)), C.dim],
171
181
  ]))
172
182
  : [buildPanelLine(width, [[' No contextual marketplace recommendations right now.', C.dim]])];
173
183
 
174
184
  const startupIssueLines = startupIssues.length > 0
175
- ? startupIssues.slice(0, 4).map((issue) => buildPanelLine(width, [[' ', C.label], [issue.slice(0, Math.max(0, width - 2)), C.warn]]))
185
+ ? startupIssues.slice(0, 4).map((issue) => buildPanelLine(width, [[' ', C.label], [truncateDisplay(issue, Math.max(0, width - 2)), C.warn]]))
176
186
  : [buildPanelLine(width, [[' No startup or lifecycle issues are currently pushing marketplace repair recommendations.', C.dim]])];
177
187
 
178
188
  const selectedRow = this.rows[this.selectedIndex];
@@ -181,11 +191,11 @@ export class MarketplacePanel extends ScrollableListPanel<MarketplaceRow> {
181
191
  const review = reviewEcosystemCatalogEntry(selectedRow.entry, this.ecosystemPaths!);
182
192
  selectedLines.push(buildPanelLine(width, [
183
193
  [' Provenance: ', C.label],
184
- [(selectedRow.entry.provenance ?? '(none)').slice(0, Math.max(0, width - 15)), selectedRow.entry.provenance ? C.info : C.dim],
194
+ [truncateDisplay(selectedRow.entry.provenance ?? '(none)', Math.max(0, width - 15)), selectedRow.entry.provenance ? C.info : C.dim],
185
195
  ]));
186
196
  selectedLines.push(buildPanelLine(width, [
187
197
  [' Source: ', C.label],
188
- [selectedRow.entry.source.slice(0, Math.max(0, width - 11)), C.value],
198
+ [truncateDisplay(selectedRow.entry.source, Math.max(0, width - 11)), C.value],
189
199
  ]));
190
200
  selectedLines.push(buildKeyValueLine(width, [
191
201
  { label: 'Compatibility', value: review.compatibility.status, valueColor: review.compatibility.status === 'supported' ? C.good : C.warn },
@@ -195,9 +205,19 @@ export class MarketplacePanel extends ScrollableListPanel<MarketplaceRow> {
195
205
  selectedLines.push(buildGuidanceLine(width, '/marketplace review <id>', 'inspect full compatibility and receipt detail for the selected entry', C));
196
206
  }
197
207
 
198
- const postureSection: PanelWorkspaceSection = { title: 'Marketplace posture', lines: postureLines };
199
- const startupIssuesSection: PanelWorkspaceSection = { title: 'Startup Issues', lines: startupIssueLines };
200
- const recommendationsSection: PanelWorkspaceSection = { title: 'Recommendations', lines: recommendationLines };
208
+ // Context-aware hints: filter mode vs. browse mode (install only makes sense
209
+ // when a curated, not-yet-installed entry is selected).
210
+ const hints = this.filterActive
211
+ ? [{ keys: 'type', label: 'filter' }, { keys: 'Enter', label: 'apply' }, { keys: 'Esc', label: 'clear' }]
212
+ : [
213
+ { keys: 'Up/Down', label: 'move' },
214
+ ...(selectedRow && !selectedRow.installed ? [{ keys: '/marketplace install', label: 'add' }] : []),
215
+ { keys: '/', label: 'filter' },
216
+ ];
217
+
218
+ const footer: Line[] = selectedLines.length > 0 && height >= 20
219
+ ? [...selectedLines, buildKeyboardHints(width, hints, C)]
220
+ : [buildKeyboardHints(width, hints, C)];
201
221
 
202
222
  return this.renderList(width, height, {
203
223
  title: 'Marketplace Control Room',
@@ -206,7 +226,7 @@ export class MarketplacePanel extends ScrollableListPanel<MarketplaceRow> {
206
226
  ...startupIssueLines,
207
227
  ...recommendationLines,
208
228
  ],
209
- footer: selectedLines.length > 0 && height >= 20 ? selectedLines : [],
229
+ footer,
210
230
  });
211
231
  }
212
232
  }
@@ -14,6 +14,7 @@ import type { Line } from '../types/grid.ts';
14
14
  import type { MemoryRegistry } from '@pellux/goodvibes-sdk/platform/state';
15
15
  import type { MemoryClass, MemoryRecord, MemoryReviewState } from '@pellux/goodvibes-sdk/platform/state';
16
16
  import { ScrollableListPanel, SearchableListPanel } from './scrollable-list-panel.ts';
17
+ import { truncateDisplay } from '../utils/terminal-width.ts';
17
18
  import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
18
19
  import {
19
20
  buildBodyText,
@@ -28,6 +29,7 @@ import {
28
29
  getPanelSearchFocusTransition,
29
30
  isPanelSearchCancel,
30
31
  } from './search-focus.ts';
32
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
31
33
 
32
34
  // ---------------------------------------------------------------------------
33
35
  // Colour palette
@@ -175,7 +177,7 @@ export class MemoryPanel extends SearchableListPanel<MemoryRecord> {
175
177
  [' ', C.label, bg],
176
178
  [record.reviewState.padEnd(13), reviewStateColor(record.reviewState), bg],
177
179
  [` ${formatConfidence(record.confidence)} `, C.value, bg],
178
- [record.summary.slice(0, Math.max(0, width - 26)), C.value, bg],
180
+ [truncateDisplay(record.summary, Math.max(0, width - 26)), C.value, bg],
179
181
  ]);
180
182
  }
181
183
  // All-mode row: scope/class + id + time + summary (matches former MemoryPanel row)
@@ -187,7 +189,7 @@ export class MemoryPanel extends SearchableListPanel<MemoryRecord> {
187
189
  [' ', C.label, bg],
188
190
  [fmtTime(record.createdAt), C.dim, bg],
189
191
  [' ', C.label, bg],
190
- [record.summary.slice(0, Math.max(0, width - 33)), C.value, bg],
192
+ [truncateDisplay(record.summary, Math.max(0, width - 33)), C.value, bg],
191
193
  ]);
192
194
  }
193
195
 
@@ -267,7 +269,7 @@ export class MemoryPanel extends SearchableListPanel<MemoryRecord> {
267
269
  });
268
270
  }
269
271
  } catch (e) {
270
- this.setError(`Review update failed: ${e instanceof Error ? e.message : String(e)}`);
272
+ this.setError(`Review update failed: ${summarizeError(e)}`);
271
273
  }
272
274
  }
273
275
  this.refreshReviewRecords();
@@ -305,13 +307,13 @@ export class MemoryPanel extends SearchableListPanel<MemoryRecord> {
305
307
  }
306
308
  if (key === 's') {
307
309
  if (!selected) return false;
308
- this.confirm = { subject: { id: selected.id, action: 'stale' }, label: selected.summary.slice(0, 40) };
310
+ this.confirm = { subject: { id: selected.id, action: 'stale' }, label: truncateDisplay(selected.summary, 40) };
309
311
  this.markDirty();
310
312
  return true;
311
313
  }
312
314
  if (key === 'c') {
313
315
  if (!selected) return false;
314
- this.confirm = { subject: { id: selected.id, action: 'contradicted' }, label: selected.summary.slice(0, 40) };
316
+ this.confirm = { subject: { id: selected.id, action: 'contradicted' }, label: truncateDisplay(selected.summary, 40) };
315
317
  this.markDirty();
316
318
  return true;
317
319
  }
@@ -8,12 +8,15 @@
8
8
  * Open via Ctrl+O keybind or `/ops view` command.
9
9
  */
10
10
  import type { Line } from '../types/grid.ts';
11
+ import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
11
12
  import type { OpsEvent } from '@/runtime/index.ts';
12
13
  import type { UiEventFeed } from '../runtime/ui-events.ts';
13
14
  import type { OpsAuditEntry } from '../runtime/diagnostics/panels/ops.ts';
14
15
  import { OpsPanel } from '../runtime/diagnostics/panels/ops.ts';
15
16
  import { ScrollableListPanel } from './scrollable-list-panel.ts';
16
17
  import {
18
+ buildKeyValueLine,
19
+ buildKeyboardHints,
17
20
  buildPanelLine,
18
21
  DEFAULT_PANEL_PALETTE,
19
22
  type PanelPalette,
@@ -77,6 +80,8 @@ export class OpsControlPanel extends ScrollableListPanel<OpsAuditEntry> {
77
80
  public constructor(eventFeed: UiEventFeed<OpsEvent>) {
78
81
  super('ops-control', 'Ops Control', 'Q', 'agent');
79
82
  this.showSelectionGutter = true; // I5: non-color selection affordance
83
+ this.filterEnabled = true;
84
+ this.filterLabel = 'Filter audit';
80
85
  this._opsPanel = new OpsPanel(eventFeed);
81
86
  this._unsub = this._opsPanel.subscribe(() => this.markDirty());
82
87
  }
@@ -103,16 +108,23 @@ export class OpsControlPanel extends ScrollableListPanel<OpsAuditEntry> {
103
108
  return [...this._opsPanel.getSnapshot()].reverse();
104
109
  }
105
110
 
111
+ protected override filterMatches(entry: OpsAuditEntry, q: string): boolean {
112
+ return entry.action.toLowerCase().includes(q)
113
+ || entry.targetKind.toLowerCase().includes(q)
114
+ || entry.targetId.toLowerCase().includes(q)
115
+ || entry.outcome.toLowerCase().includes(q);
116
+ }
117
+
106
118
  protected renderItem(entry: OpsAuditEntry, _index: number, _selected: boolean, width: number): Line {
107
119
  const seqStr = String(entry.seq).padStart(4, ' ');
108
120
  const timeStr = fmtTime(entry.ts);
109
- const action = entry.action.slice(0, 15).padEnd(15, ' ');
121
+ const action = fitDisplay(entry.action, 15);
110
122
  const kindTag = entry.targetKind === 'task' ? 'T:' : 'A:';
111
- // Truncation is intentional: TUI column width limits target ID display to 14 chars
123
+ // Last 10 chars of the id keep the unique suffix; fitDisplay caps display width.
112
124
  const shortId = entry.targetId.slice(-10);
113
- const target = (kindTag + shortId).slice(0, 14).padEnd(14, ' ');
125
+ const target = fitDisplay(kindTag + shortId, 14);
114
126
  const outLabel = outcomeLabel(entry.outcome);
115
- const noteRaw = (entry.note ?? entry.errorMessage ?? '').slice(0, Math.max(0, width - 63));
127
+ const noteRaw = truncateDisplay(entry.note ?? entry.errorMessage ?? '', Math.max(0, width - 63));
116
128
 
117
129
  const segs: Array<[string, string, string?]> = [
118
130
  [` ${seqStr} `, C.seq],
@@ -134,12 +146,62 @@ export class OpsControlPanel extends ScrollableListPanel<OpsAuditEntry> {
134
146
  }
135
147
 
136
148
  public render(width: number, height: number): Line[] {
149
+ const entries = this.getVisibleItems();
150
+ this.clampSelection();
151
+
152
+ // Outcome tallies surface posture at a glance (most important runtime info first).
153
+ let ok = 0;
154
+ let rejected = 0;
155
+ let errored = 0;
156
+ for (const e of entries) {
157
+ if (e.outcome === 'success') ok++;
158
+ else if (e.outcome === 'rejected') rejected++;
159
+ else errored++;
160
+ }
161
+
137
162
  const headerLines: Line[] = [
163
+ buildKeyValueLine(width, [
164
+ { label: 'logged', value: String(entries.length), valueColor: entries.length > 0 ? C.value : C.dim },
165
+ { label: 'ok', value: String(ok), valueColor: ok > 0 ? C.success : C.dim },
166
+ { label: 'rejected', value: String(rejected), valueColor: rejected > 0 ? C.rejected : C.dim },
167
+ { label: 'errors', value: String(errored), valueColor: errored > 0 ? C.error : C.dim },
168
+ ], C),
138
169
  buildPanelLine(width, [[' SEQ TIME ACTION TARGET OUT NOTE', C.label]]),
139
170
  ];
140
- const footerLines: Line[] = [
141
- buildPanelLine(width, [[' Up/Down scroll the intervention log', C.dim]]),
142
- ];
171
+
172
+ const selected = entries[this.selectedIndex];
173
+ const footerLines: Line[] = [];
174
+ if (selected) {
175
+ const detail = selected.note ?? selected.errorMessage ?? '(no note)';
176
+ footerLines.push(
177
+ buildPanelLine(width, [
178
+ [' #', C.label],
179
+ [String(selected.seq), C.value],
180
+ [' ', C.dim],
181
+ [outcomeLabel(selected.outcome).trim(), outcomeColor(selected.outcome)],
182
+ [' ', C.dim],
183
+ [selected.action, C.value],
184
+ [' ', C.dim],
185
+ [`${selected.targetKind}:${selected.targetId}`, targetColor(selected.targetKind)],
186
+ ]),
187
+ buildPanelLine(width, [
188
+ [' ', C.label],
189
+ [truncateDisplay(detail, Math.max(0, width - 4)), C.note],
190
+ ]),
191
+ );
192
+ }
193
+ footerLines.push(
194
+ this.filterActive
195
+ ? buildKeyboardHints(width, [
196
+ { keys: 'type', label: 'filter audit' },
197
+ { keys: 'Enter', label: 'apply' },
198
+ { keys: 'Esc', label: 'clear' },
199
+ ], C)
200
+ : buildKeyboardHints(width, [
201
+ { keys: 'Up/Down', label: 'browse log' },
202
+ { keys: '/', label: 'filter' },
203
+ ], C),
204
+ );
143
205
 
144
206
  return this.renderList(width, height, {
145
207
  title: 'Operator Control Plane',
@@ -9,25 +9,37 @@
9
9
 
10
10
  import { BasePanel } from './base-panel.ts';
11
11
  import type { Line } from '../types/grid.ts';
12
+ import { getDisplayWidth } from '../utils/terminal-width.ts';
12
13
  import type { PlannerDecision, ExecutionStrategy } from '@pellux/goodvibes-sdk/platform/core';
13
14
  import type { PlannerEvent } from '@/runtime/index.ts';
14
15
  import type { UiEventFeed } from '../runtime/ui-events.ts';
15
16
  import type { OpsStrategyQuery } from '../runtime/ui-service-queries.ts';
16
17
  import {
18
+ buildAlignedRow,
17
19
  buildEmptyState,
20
+ buildKeyboardHints,
18
21
  buildPanelLine,
19
22
  buildStyledPanelLine,
20
23
  buildPanelWorkspace,
24
+ extendPalette,
21
25
  resolveScrollablePanelSection,
22
26
  DEFAULT_PANEL_PALETTE,
23
27
  } from './polish.ts';
24
28
 
25
- const STRATEGY_FG: Record<ExecutionStrategy, string> = {
29
+ const C = extendPalette(DEFAULT_PANEL_PALETTE, {
26
30
  auto: '#00cccc',
27
31
  single: '#00cc66',
28
32
  cohort: '#cccc00',
29
33
  background: '#cc66cc',
30
34
  remote: '#cccccc',
35
+ });
36
+
37
+ const STRATEGY_FG: Record<ExecutionStrategy, string> = {
38
+ auto: C.auto,
39
+ single: C.single,
40
+ cohort: C.cohort,
41
+ background: C.background,
42
+ remote: C.remote,
31
43
  };
32
44
 
33
45
  const STRATEGY_ICON: Record<ExecutionStrategy, string> = {
@@ -104,58 +116,63 @@ export class OpsStrategyPanel extends BasePanel {
104
116
  const latest = this.adaptivePlanner.getLatest();
105
117
  const mode = this.adaptivePlanner.getMode();
106
118
  const override = this.adaptivePlanner.getOverride();
119
+ const intro = 'Review adaptive execution planner decisions, overrides, and recent strategy history.';
120
+ const footerLines = [
121
+ buildKeyboardHints(width, [
122
+ { keys: 'Up/Down', label: 'scroll history' },
123
+ { keys: 'g/G', label: 'top/bottom' },
124
+ ], C),
125
+ ];
107
126
  const statusLines: Line[] = [
108
127
  buildPanelLine(width, [
109
- [' Mode ', DEFAULT_PANEL_PALETTE.label],
110
- [mode.toUpperCase(), DEFAULT_PANEL_PALETTE.value],
111
- [' Override ', DEFAULT_PANEL_PALETTE.label],
112
- [override ? `${override.toUpperCase()} [ACTIVE]` : 'none', override ? DEFAULT_PANEL_PALETTE.warn : DEFAULT_PANEL_PALETTE.dim],
128
+ [' Mode ', C.label],
129
+ [mode.toUpperCase(), C.value],
130
+ [' Override ', C.label],
131
+ [override ? `${override.toUpperCase()} [ACTIVE]` : 'none', override ? C.warn : C.dim],
132
+ [' Decisions ', C.label],
133
+ [String(this.history.length), this.history.length > 0 ? C.info : C.dim],
113
134
  ]),
114
135
  ];
115
136
 
116
137
  if (latest) {
117
138
  statusLines.push(buildPanelLine(width, [
118
- [' Last ', DEFAULT_PANEL_PALETTE.label],
139
+ [' Last ', C.label],
119
140
  [`${STRATEGY_ICON[latest.selected]} ${latest.selected.toUpperCase()}`, STRATEGY_FG[latest.selected]],
120
- [' Reason ', DEFAULT_PANEL_PALETTE.label],
121
- [latest.reasonCode, DEFAULT_PANEL_PALETTE.dim],
141
+ [' Reason ', C.label],
142
+ [latest.reasonCode, C.dim],
122
143
  ]));
123
144
  }
124
145
 
125
146
  if (this.history.length === 0) {
126
147
  return buildPanelWorkspace(width, height, {
127
148
  title: ' Ops Strategy',
128
- intro: 'Review adaptive execution planner decisions, overrides, and recent strategy history.',
149
+ intro,
129
150
  sections: [
130
151
  { title: 'Status', lines: statusLines },
131
152
  {
132
153
  lines: buildEmptyState(
133
154
  width,
134
155
  ' No decisions recorded yet',
135
- 'Adaptive planner decisions appear here once the planner begins selecting strategies.',
136
- [],
137
- DEFAULT_PANEL_PALETTE,
156
+ 'Adaptive planner decisions appear here once the planner begins selecting strategies. Run agents or use /ops to drive runtime activity.',
157
+ [{ command: '/ops', summary: 'open the operator control room to drive planner activity' }],
158
+ C,
138
159
  ),
139
160
  },
140
161
  ],
141
- footerLines: [
142
- buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' scroll history', DEFAULT_PANEL_PALETTE.dim], [' g/G', DEFAULT_PANEL_PALETTE.info], [' top/bottom', DEFAULT_PANEL_PALETTE.dim]]),
143
- ],
144
- palette: DEFAULT_PANEL_PALETTE,
162
+ footerLines,
163
+ palette: C,
145
164
  });
146
165
  }
147
166
 
148
167
  const historyLines = this._renderHistory(width);
149
168
  const statusSection = { title: 'Status', lines: statusLines } as const;
150
169
  const historySection = resolveScrollablePanelSection(width, height, {
151
- intro: 'Review adaptive execution planner decisions, overrides, and recent strategy history.',
152
- footerLines: [
153
- buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' scroll history', DEFAULT_PANEL_PALETTE.dim], [' g/G', DEFAULT_PANEL_PALETTE.info], [' top/bottom', DEFAULT_PANEL_PALETTE.dim]]),
154
- ],
155
- palette: DEFAULT_PANEL_PALETTE,
170
+ intro,
171
+ footerLines,
172
+ palette: C,
156
173
  beforeSections: [statusSection],
157
174
  section: {
158
- title: 'Decision History',
175
+ title: `Decision History (${this.history.length})`,
159
176
  scrollableLines: historyLines,
160
177
  scrollOffset: Math.min(this.scrollOffset, Math.max(0, historyLines.length - 1)),
161
178
  minRows: 8,
@@ -165,15 +182,13 @@ export class OpsStrategyPanel extends BasePanel {
165
182
 
166
183
  return buildPanelWorkspace(width, height, {
167
184
  title: ' Ops Strategy',
168
- intro: 'Review adaptive execution planner decisions, overrides, and recent strategy history.',
185
+ intro,
169
186
  sections: [
170
187
  statusSection,
171
188
  historySection.section,
172
189
  ],
173
- footerLines: [
174
- buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' scroll history', DEFAULT_PANEL_PALETTE.dim], [' g/G', DEFAULT_PANEL_PALETTE.info], [' top/bottom', DEFAULT_PANEL_PALETTE.dim]]),
175
- ],
176
- palette: DEFAULT_PANEL_PALETTE,
190
+ footerLines,
191
+ palette: C,
177
192
  });
178
193
  }
179
194
 
@@ -187,11 +202,10 @@ export class OpsStrategyPanel extends BasePanel {
187
202
 
188
203
  private _renderHistory(width: number): Line[] {
189
204
  if (this.history.length === 0) {
190
- return [buildStyledPanelLine(width, [{ text: ' No history yet.', fg: DEFAULT_PANEL_PALETTE.dim, dim: true }])];
205
+ return [buildStyledPanelLine(width, [{ text: ' No history yet.', fg: C.dim, dim: true }])];
191
206
  }
192
207
 
193
208
  const lines: Line[] = [];
194
- lines.push(buildStyledPanelLine(width, [{ text: ' Decision History', fg: DEFAULT_PANEL_PALETTE.value, bold: true }]));
195
209
 
196
210
  const reversed = [...this.history].reverse();
197
211
  for (let i = 0; i < reversed.length; i++) {
@@ -202,29 +216,33 @@ export class OpsStrategyPanel extends BasePanel {
202
216
  const num = String(i + 1).padStart(3);
203
217
  const overrideMark = d.overrideActive ? ' [O]' : '';
204
218
 
205
- // Row 1: index + icon + strategy + timestamp (right-aligned)
206
- const leftBase = ` ${num}. ${icon} ${d.selected.toUpperCase()}${overrideMark}`;
207
- const rightText = ` ${ts}`;
208
- const pad = Math.max(1, width - leftBase.length - rightText.length);
209
- lines.push(buildStyledPanelLine(width, [
210
- { text: ` ${num}. `, fg: DEFAULT_PANEL_PALETTE.dim, dim: true },
211
- { text: `${icon} ${d.selected.toUpperCase()}`, fg, bold: true },
212
- { text: overrideMark, fg: DEFAULT_PANEL_PALETTE.warn },
213
- { text: ' '.repeat(pad), fg: DEFAULT_PANEL_PALETTE.dim },
214
- { text: rightText, fg: DEFAULT_PANEL_PALETTE.dim, dim: true },
215
- ]));
219
+ // Row 1: index + icon + strategy on the left, timestamp right-aligned.
220
+ // buildAlignedRow keeps the timestamp flush-right even with wide glyphs.
221
+ const tsCol = getDisplayWidth(`${ts} `);
222
+ lines.push(buildAlignedRow(
223
+ width,
224
+ [
225
+ { text: ` ${num}. ${icon} ${d.selected.toUpperCase()}${overrideMark}`, fg, bold: true },
226
+ { text: `${ts} `, fg: C.dim, dim: true },
227
+ ],
228
+ [
229
+ { width: Math.max(0, width - tsCol - 1) },
230
+ { width: tsCol, align: 'right' },
231
+ ],
232
+ { gap: 1 },
233
+ ));
216
234
 
217
235
  // Row 2: reason code
218
- lines.push(buildStyledPanelLine(width, [{ text: ` ${d.reasonCode}`, fg: DEFAULT_PANEL_PALETTE.dim, dim: true }]));
236
+ lines.push(buildStyledPanelLine(width, [{ text: ` ${d.reasonCode}`, fg: C.dim, dim: true }]));
219
237
 
220
238
  // Row 3+: top-2 scored candidates (auto mode only)
221
239
  if (!d.overrideActive && d.candidates.length > 1) {
222
240
  const top2 = d.candidates.slice(0, 2);
223
241
  for (const c of top2) {
224
242
  lines.push(buildStyledPanelLine(width, [
225
- { text: ' ', fg: DEFAULT_PANEL_PALETTE.dim, dim: true },
243
+ { text: ' ', fg: C.dim, dim: true },
226
244
  { text: c.strategy.padEnd(12), fg: STRATEGY_FG[c.strategy] },
227
- { text: ` score ${c.score}`, fg: DEFAULT_PANEL_PALETTE.dim, dim: true },
245
+ { text: ` score ${c.score}`, fg: C.dim, dim: true },
228
246
  ]));
229
247
  }
230
248
  }