@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 } from '../utils/terminal-width.ts';
3
4
  import { ScrollableListPanel } from './scrollable-list-panel.ts';
4
5
  import type { KeyName } from './types.ts';
5
6
  import type { ProviderSubscription, PendingSubscriptionLogin } from '@pellux/goodvibes-sdk/platform/config';
@@ -9,6 +10,7 @@ import type { ServiceInspectionQuery, SubscriptionAccessQuery } from '../runtime
9
10
  import {
10
11
  buildEmptyState,
11
12
  buildGuidanceLine,
13
+ buildKeyboardHints,
12
14
  buildKeyValueLine,
13
15
  buildPanelListRow,
14
16
  buildPanelLine,
@@ -94,7 +96,7 @@ export class SubscriptionPanel extends ScrollableListPanel<SubscriptionRow> {
94
96
  protected renderItem(row: SubscriptionRow, index: number, selected: boolean, width: number): Line {
95
97
  const status = statusOf(row);
96
98
  return buildPanelListRow(width, [
97
- { text: row.provider.padEnd(16).slice(0, 16), fg: C.value },
99
+ { text: fitDisplay(row.provider, 16), fg: C.value },
98
100
  { text: ` ${status.toUpperCase().padEnd(12)}`, fg: statusColor(status) },
99
101
  { text: ` oauth=${row.hasOAuthConfig ? 'yes' : 'no'} `, fg: row.hasOAuthConfig ? C.info : C.dim },
100
102
  { text: ` override=${row.subscription ? 'active' : 'off'}`, fg: row.subscription ? C.good : C.dim },
@@ -177,6 +179,22 @@ export class SubscriptionPanel extends ScrollableListPanel<SubscriptionRow> {
177
179
  this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.rows.length - 1));
178
180
  }
179
181
 
182
+ /** Footer hints that reflect the current view: confirm-pending vs browsing. */
183
+ private buildFooterHint(width: number): Line {
184
+ if (this.confirm) {
185
+ return buildKeyboardHints(width, [
186
+ { keys: 'y/Enter', label: 'confirm sign out' },
187
+ { keys: 'n/Esc', label: 'cancel' },
188
+ ], C);
189
+ }
190
+ const selected = this.rows[this.selectedIndex];
191
+ const hints: Array<{ keys: string; label: string }> = [{ keys: 'Up/Down', label: 'select' }];
192
+ if (selected?.subscription) hints.push({ keys: 'Enter', label: 'sign out' });
193
+ else if (selected?.hasOAuthConfig) hints.push({ keys: '/subscription login <p> start', label: 'sign in' });
194
+ hints.push({ keys: 'r', label: 'refresh' });
195
+ return buildKeyboardHints(width, hints, C);
196
+ }
197
+
180
198
  public render(width: number, height: number): Line[] {
181
199
  this.refresh();
182
200
  this.clampSelection();
@@ -214,7 +232,7 @@ export class SubscriptionPanel extends ScrollableListPanel<SubscriptionRow> {
214
232
  sections: [{ lines: [...summaryLines, ...emptyLines] }],
215
233
  footerLines: [
216
234
  buildGuidanceLine(width, '/subscription login <provider> start', 'start browser-based provider login from the packaged subscription surface', C),
217
- buildPanelLine(width, [[' Up/Down move Enter sign out selected provider y/Esc confirm/cancel r refresh', C.dim]]),
235
+ this.buildFooterHint(width),
218
236
  ],
219
237
  palette: C,
220
238
  });
@@ -264,7 +282,7 @@ export class SubscriptionPanel extends ScrollableListPanel<SubscriptionRow> {
264
282
  footer: [
265
283
  ...detailRows,
266
284
  buildGuidanceLine(width, '/subscription login <provider> start', 'start browser-based provider login from the packaged subscription surface', C),
267
- buildPanelLine(width, [[' Up/Down move Enter sign out selected provider y/Esc confirm/cancel r refresh', C.dim]]),
285
+ this.buildFooterHint(width),
268
286
  ],
269
287
  });
270
288
  }
@@ -3,13 +3,14 @@ import { createEmptyLine } from '../types/grid.ts';
3
3
  import type { Line } from '../types/grid.ts';
4
4
  import {
5
5
  buildEmptyState,
6
+ buildKeyboardHints,
6
7
  buildPanelLine,
7
- buildSelectablePanelLine,
8
+ buildTreeRow,
8
9
  buildPanelWorkspace,
9
10
  resolveScrollablePanelSection,
10
11
  DEFAULT_PANEL_PALETTE,
12
+ extendPalette,
11
13
  } from './polish.ts';
12
- import { getDisplayWidth } from '../utils/terminal-width.ts';
13
14
 
14
15
  // ── Symbol types ────────────────────────────────────────────────────────────
15
16
 
@@ -25,6 +26,10 @@ export interface SymbolEntry {
25
26
 
26
27
  // ── Rendering constants ──────────────────────────────────────────────────────
27
28
 
29
+ const C = extendPalette(DEFAULT_PANEL_PALETTE, {
30
+ selectedBg: '236',
31
+ });
32
+
28
33
  /** ANSI 256-color fg codes per symbol kind. */
29
34
  const KIND_COLORS: Record<SymbolKind, string> = {
30
35
  function: '87', // cyan
@@ -36,15 +41,15 @@ const KIND_COLORS: Record<SymbolKind, string> = {
36
41
  const: '245', // grey
37
42
  };
38
43
 
39
- /** Short type indicator labels. */
40
- const KIND_LABELS: Record<SymbolKind, string> = {
41
- function: 'fn ',
42
- method: 'fn ',
43
- class: 'cls',
44
- namespace: 'ns ',
45
- interface: 'int',
46
- type: 'typ',
47
- const: 'cst',
44
+ /** Single-char type icon for the tree row (kept ASCII for column safety). */
45
+ const KIND_ICONS: Record<SymbolKind, string> = {
46
+ function: 'ƒ',
47
+ method: 'ƒ',
48
+ class: 'C',
49
+ namespace: 'N',
50
+ interface: 'I',
51
+ type: 'T',
52
+ const: 'k',
48
53
  };
49
54
 
50
55
  /** Regex patterns to extract symbols. Each produces named groups: kind, name, line. */
@@ -188,8 +193,10 @@ export class SymbolOutlinePanel extends BasePanel {
188
193
  this.currentPath
189
194
  ? 'The current file did not produce outline entries with the lightweight parser heuristics.'
190
195
  : 'Load a file in the preview panel to populate its outline here.',
191
- [],
192
- DEFAULT_PANEL_PALETTE,
196
+ this.currentPath
197
+ ? []
198
+ : [{ command: '/explorer', summary: 'pick a file in the explorer, then Enter to preview and outline it' }],
199
+ C,
193
200
  ),
194
201
  },
195
202
  ],
@@ -201,7 +208,7 @@ export class SymbolOutlinePanel extends BasePanel {
201
208
  const outlineSection = resolveScrollablePanelSection(width, height, {
202
209
  intro: this.currentPath ? this.currentPath : 'Outline the current file into navigable symbols and lightweight parent/child structure.',
203
210
  footerLines: [
204
- buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim], [' Space', DEFAULT_PANEL_PALETTE.info], [' collapse', DEFAULT_PANEL_PALETTE.dim], [' Enter', DEFAULT_PANEL_PALETTE.info], [' jump target', DEFAULT_PANEL_PALETTE.dim]]),
211
+ buildKeyboardHints(width, [{ keys: '↑/↓', label: 'navigate' }, { keys: 'Space/←/→', label: 'collapse' }, { keys: 'Enter', label: 'jump to source →' }], DEFAULT_PANEL_PALETTE),
205
212
  ],
206
213
  palette: DEFAULT_PANEL_PALETTE,
207
214
  beforeSections: [
@@ -291,7 +298,7 @@ export class SymbolOutlinePanel extends BasePanel {
291
298
  },
292
299
  ],
293
300
  footerLines: [
294
- buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim], [' Space', DEFAULT_PANEL_PALETTE.info], [' collapse', DEFAULT_PANEL_PALETTE.dim], [' Enter', DEFAULT_PANEL_PALETTE.info], [' jump target', DEFAULT_PANEL_PALETTE.dim]]),
301
+ buildKeyboardHints(width, [{ keys: '↑/↓', label: 'navigate' }, { keys: 'Space/←/→', label: 'collapse' }, { keys: 'Enter', label: 'jump to source →' }], DEFAULT_PANEL_PALETTE),
295
302
  ],
296
303
  palette: DEFAULT_PANEL_PALETTE,
297
304
  });
@@ -434,10 +441,7 @@ function buildVisibleRows(symbols: SymbolEntry[], collapsed: Set<string>): Visib
434
441
  return rows;
435
442
  }
436
443
 
437
- /**
438
- * Write a string into a Line starting at column x, applying fg/bg/style.
439
- */
440
- /** Render a container header row (class / namespace). */
444
+ /** Render a container header row (class / namespace) via the shared tree row. */
441
445
  function _renderHeader(
442
446
  width: number,
443
447
  row: Extract<VisibleRow, { kind: 'header' }>,
@@ -445,24 +449,19 @@ function _renderHeader(
445
449
  bgColor: string,
446
450
  collapsed: Set<string>,
447
451
  ): Line {
448
- // Collapse indicator
449
452
  const isCollapsed = collapsed.has(row.name);
450
- const chevron = row.hasChildren ? (isCollapsed ? '▸ ' : '▾ ') : ' ';
451
- const lineNumStr = `:${row.line}`;
452
- const kindLabel = KIND_LABELS[row.symbolKind];
453
- const leadingWidth = 1 + getDisplayWidth(chevron) + getDisplayWidth(kindLabel) + 1 + getDisplayWidth(row.name);
454
- const gap = Math.max(1, width - leadingWidth - getDisplayWidth(lineNumStr) - 1);
455
- return buildSelectablePanelLine(width, [
456
- { text: ` ${chevron}`, fg: '245' },
457
- { text: kindLabel, fg: KIND_COLORS[row.symbolKind], bold: true },
458
- { text: ' ', fg: isSelected ? '255' : '252' },
459
- { text: row.name, fg: isSelected ? '255' : '252', bold: isSelected },
460
- { text: ' '.repeat(gap), fg: DEFAULT_PANEL_PALETTE.dim },
461
- { text: lineNumStr, fg: DEFAULT_PANEL_PALETTE.dim },
462
- ], { selected: isSelected, selectedBg: bgColor, fillFg: isSelected ? '255' : '' });
453
+ return buildTreeRow(width, {
454
+ depth: 0,
455
+ label: row.name,
456
+ icon: KIND_ICONS[row.symbolKind],
457
+ expandable: row.hasChildren,
458
+ expanded: !isCollapsed,
459
+ labelColor: KIND_COLORS[row.symbolKind],
460
+ metadata: [{ text: `:${row.line}`, fg: DEFAULT_PANEL_PALETTE.dim }],
461
+ }, C, { selected: isSelected, selectedBg: bgColor || C.selectedBg });
463
462
  }
464
463
 
465
- /** Render a regular symbol row. */
464
+ /** Render a regular symbol row via the shared tree row. */
466
465
  function _renderSymbol(
467
466
  width: number,
468
467
  row: Extract<VisibleRow, { kind: 'symbol' }>,
@@ -470,17 +469,11 @@ function _renderSymbol(
470
469
  bgColor: string,
471
470
  ): Line {
472
471
  const { symbol, depth } = row;
473
- const indent = depth === 0 ? 1 : 3; // children indented by 3 (chevron + space)
474
- const kindLabel = KIND_LABELS[symbol.kind];
475
- const lineNumStr = `:${symbol.line}`;
476
- const leadingWidth = indent + getDisplayWidth(kindLabel) + 1 + getDisplayWidth(symbol.name);
477
- const gap = Math.max(1, width - leadingWidth - getDisplayWidth(lineNumStr) - 1);
478
- return buildSelectablePanelLine(width, [
479
- { text: ' '.repeat(indent), fg: DEFAULT_PANEL_PALETTE.dim },
480
- { text: kindLabel, fg: KIND_COLORS[symbol.kind] },
481
- { text: ' ', fg: isSelected ? '255' : '251' },
482
- { text: symbol.name, fg: isSelected ? '255' : '251', bold: isSelected },
483
- { text: ' '.repeat(gap), fg: DEFAULT_PANEL_PALETTE.dim },
484
- { text: lineNumStr, fg: DEFAULT_PANEL_PALETTE.dim },
485
- ], { selected: isSelected, selectedBg: bgColor, fillFg: isSelected ? '255' : '' });
472
+ return buildTreeRow(width, {
473
+ depth,
474
+ label: symbol.name,
475
+ icon: KIND_ICONS[symbol.kind],
476
+ labelColor: KIND_COLORS[symbol.kind],
477
+ metadata: [{ text: `:${symbol.line}`, fg: DEFAULT_PANEL_PALETTE.dim }],
478
+ }, C, { selected: isSelected, selectedBg: bgColor || C.selectedBg });
486
479
  }
@@ -14,8 +14,10 @@ import {
14
14
  buildEmptyState,
15
15
  buildGuidanceLine,
16
16
  buildKeyValueLine,
17
+ buildKeyboardHints,
17
18
  buildPanelListRow,
18
19
  buildPanelLine,
20
+ buildStatusBadge,
19
21
  buildSummaryBlock,
20
22
  buildPanelWorkspace,
21
23
  resolvePrimaryScrollableSection,
@@ -59,6 +61,12 @@ export class SystemMessagesPanel extends ScrollableListPanel<SystemMessageEntry>
59
61
  constructor(configManager: ConfigManager, componentHealthMonitor?: ComponentHealthMonitor) {
60
62
  super('system-messages', 'System Messages', 'J', 'monitoring', componentHealthMonitor);
61
63
  this.configManager = configManager;
64
+ this.filterEnabled = true;
65
+ this.filterLabel = 'Filter messages';
66
+ }
67
+
68
+ protected override filterMatches(entry: SystemMessageEntry, q: string): boolean {
69
+ return entry.text.toLowerCase().includes(q) || entry.priority.toLowerCase().includes(q);
62
70
  }
63
71
 
64
72
  // ---------------------------------------------------------------------------
@@ -79,7 +87,7 @@ export class SystemMessagesPanel extends ScrollableListPanel<SystemMessageEntry>
79
87
  return buildPanelListRow(width, [
80
88
  { text: `${fmtTime(entry.ts)} `, fg: C.ts },
81
89
  {
82
- text: `${entry.priority === 'high' ? 'HIGH' : 'LOW '.padEnd(4)} `,
90
+ text: `${(entry.priority === 'high' ? 'HIGH' : 'LOW').padEnd(4)} `,
83
91
  fg: entry.priority === 'high' ? C.high : C.low,
84
92
  bold: entry.priority === 'high',
85
93
  },
@@ -129,6 +137,23 @@ export class SystemMessagesPanel extends ScrollableListPanel<SystemMessageEntry>
129
137
  // Render — multi-section layout (posture + list + detail)
130
138
  // ---------------------------------------------------------------------------
131
139
 
140
+ // Context-aware footer: navigation keys plus filter keys that reflect the
141
+ // current filter state (active typing / applied query / inactive).
142
+ private footerHints(): Array<{ keys: string; label: string }> {
143
+ const hints: Array<{ keys: string; label: string }> = [
144
+ { keys: 'j/k', label: 'scroll' },
145
+ { keys: 'g/G', label: 'jump' },
146
+ ];
147
+ if (this.filterActive) {
148
+ hints.push({ keys: 'Esc', label: 'clear filter' });
149
+ } else if (this.filterQuery) {
150
+ hints.push({ keys: '/', label: 'edit filter' }, { keys: 'Esc', label: 'clear filter' });
151
+ } else {
152
+ hints.push({ keys: '/', label: 'filter' });
153
+ }
154
+ return hints;
155
+ }
156
+
132
157
  override render(width: number, height: number): Line[] {
133
158
  return this.trackedRender(() => {
134
159
  const intro = 'Operational system traffic routed out of the main conversation to reduce noise and keep runtime status reviewable.';
@@ -151,7 +176,7 @@ export class SystemMessagesPanel extends ScrollableListPanel<SystemMessageEntry>
151
176
  ),
152
177
  }],
153
178
  footerLines: [
154
- buildPanelLine(width, [[' j/k or Up/Down scroll g/G jump low-priority system traffic lands here by default', C.dim]]),
179
+ buildPanelLine(width, [[' Low-priority system traffic lands here by default. Routing is configurable via /settings.', C.dim]]),
155
180
  ],
156
181
  palette: C,
157
182
  });
@@ -162,12 +187,16 @@ export class SystemMessagesPanel extends ScrollableListPanel<SystemMessageEntry>
162
187
  const lowCount = this._messages.length - highCount;
163
188
  this.selectedIndex = Math.min(this.selectedIndex, this._messages.length - 1);
164
189
  const ui = this.configManager.getRaw().ui;
190
+ const latest = this._messages[this._messages.length - 1];
165
191
  const postureLines = [
166
- buildKeyValueLine(width, [
167
- { label: 'messages', value: String(this._messages.length), valueColor: C.value },
168
- { label: 'high', value: String(highCount), valueColor: highCount > 0 ? C.high : C.dim },
169
- { label: 'low', value: String(lowCount), valueColor: lowCount > 0 ? C.low : C.dim },
170
- ], C),
192
+ // Severity + recency first: high-priority count leads, newest message age follows.
193
+ buildPanelLine(width, [
194
+ [' ', C.label],
195
+ ...buildStatusBadge(highCount > 0 ? 'failed' : 'completed', 'high', { count: highCount }),
196
+ [' ', C.dim],
197
+ ...buildStatusBadge('review', 'low', { count: lowCount }),
198
+ ...(latest ? ([[' newest ', C.label], [`${fmtTime(latest.ts)}`, C.value]] as Array<[string, string]>) : []),
199
+ ]),
171
200
  buildKeyValueLine(width, [
172
201
  { label: 'system route', value: ui.systemMessages, valueColor: C.info },
173
202
  { label: 'ops route', value: ui.operationalMessages, valueColor: C.info },
@@ -176,28 +205,33 @@ export class SystemMessagesPanel extends ScrollableListPanel<SystemMessageEntry>
176
205
  buildGuidanceLine(width, '/settings', 'adjust where operational and WRFC messages render across panels and conversation', C),
177
206
  ];
178
207
 
179
- const selected = this._messages[this.selectedIndex]!;
180
- const messageRows: Line[] = this._messages.map((entry, index) =>
181
- this.renderItem(entry, index, index === this.selectedIndex, width),
182
- );
208
+ const visible = this.getVisibleItems();
209
+ this.selectedIndex = Math.max(0, Math.min(this.selectedIndex, visible.length - 1));
210
+ const selected = visible[this.selectedIndex];
211
+ const messageRows: Line[] = visible.length > 0
212
+ ? visible.map((entry, index) => this.renderItem(entry, index, index === this.selectedIndex, width))
213
+ : [buildPanelLine(width, [[` No messages match "${this.filterQuery.trim()}" (Esc to clear)`, C.dim]])];
183
214
 
215
+ const filterSection: PanelWorkspaceSection = { lines: [this.buildFilterLine(width)] };
184
216
  const postureSection: PanelWorkspaceSection = { lines: buildSummaryBlock(width, 'System posture', postureLines, C) };
185
- const detailSection: PanelWorkspaceSection = {
186
- title: 'Selected Message',
187
- lines: [
188
- buildPanelLine(width, [
189
- [' Time ', C.label],
190
- [fmtTime(selected.ts), C.value],
191
- [' Priority ', C.label],
192
- [selected.priority, selected.priority === 'high' ? C.high : C.low],
193
- ]),
194
- ...buildBodyText(width, selected.text, C, C.value),
195
- ],
196
- };
217
+ const detailSection: PanelWorkspaceSection = selected
218
+ ? {
219
+ title: 'Selected Message',
220
+ lines: [
221
+ buildPanelLine(width, [
222
+ [' Time ', C.label],
223
+ [fmtTime(selected.ts), C.value],
224
+ [' Priority ', C.label],
225
+ [selected.priority, selected.priority === 'high' ? C.high : C.low],
226
+ ]),
227
+ ...buildBodyText(width, selected.text, C, C.value),
228
+ ],
229
+ }
230
+ : { title: 'Selected Message', lines: [] };
197
231
  const messagesSection = resolvePrimaryScrollableSection(width, height, {
198
232
  intro,
199
233
  palette: C,
200
- beforeSections: [postureSection],
234
+ beforeSections: [filterSection, postureSection],
201
235
  section: {
202
236
  title: 'Timeline',
203
237
  scrollableLines: messageRows,
@@ -210,6 +244,7 @@ export class SystemMessagesPanel extends ScrollableListPanel<SystemMessageEntry>
210
244
  });
211
245
  this.scrollStart = messagesSection.scrollOffset;
212
246
  const sections: PanelWorkspaceSection[] = [
247
+ filterSection,
213
248
  postureSection,
214
249
  messagesSection.section,
215
250
  detailSection,
@@ -219,9 +254,7 @@ export class SystemMessagesPanel extends ScrollableListPanel<SystemMessageEntry>
219
254
  title: 'System Messages',
220
255
  intro,
221
256
  sections,
222
- footerLines: [
223
- buildPanelLine(width, [[' j/k or Up/Down scroll PgUp/PgDn page g/G jump', C.dim]]),
224
- ],
257
+ footerLines: [buildKeyboardHints(width, this.footerHints(), C)],
225
258
  palette: C,
226
259
  });
227
260
  return lines;
@@ -1,5 +1,6 @@
1
1
  import type { Line } from '../types/grid.ts';
2
2
  import { createEmptyLine } from '../types/grid.ts';
3
+ import { truncateDisplay } from '../utils/terminal-width.ts';
3
4
  import { ScrollableListPanel } from './scrollable-list-panel.ts';
4
5
  import type { RuntimeTask, TaskLifecycleState } from '@/runtime/index.ts';
5
6
  import type { ManagedWorktreeMeta } from '@/runtime/index.ts';
@@ -8,12 +9,14 @@ import {
8
9
  buildDetailBlock,
9
10
  buildEmptyState,
10
11
  buildGuidanceLine,
12
+ buildKeyboardHints,
11
13
  buildPanelListRow,
12
14
  buildPanelLine,
13
15
  buildSummaryBlock,
14
16
  buildPanelWorkspace,
15
17
  DEFAULT_PANEL_PALETTE,
16
18
  } from './polish.ts';
19
+ import { formatElapsed } from '../utils/format-elapsed.ts';
17
20
 
18
21
  const C = {
19
22
  ...DEFAULT_PANEL_PALETTE,
@@ -35,13 +38,7 @@ function formatWhen(value?: number): string {
35
38
 
36
39
  function formatDuration(startedAt?: number, endedAt?: number): string {
37
40
  if (!startedAt) return 'n/a';
38
- const end = endedAt ?? Date.now();
39
- const ms = Math.max(0, end - startedAt);
40
- if (ms < 1_000) return `${ms}ms`;
41
- if (ms < 60_000) return `${(ms / 1_000).toFixed(ms < 10_000 ? 1 : 0)}s`;
42
- const mins = Math.floor(ms / 60_000);
43
- const secs = Math.floor((ms % 60_000) / 1_000);
44
- return `${mins}m ${secs}s`;
41
+ return formatElapsed(Math.max(0, (endedAt ?? Date.now()) - startedAt));
45
42
  }
46
43
 
47
44
  function kindLabel(kind: RuntimeTask['kind']): string {
@@ -157,6 +154,8 @@ export class TasksPanel extends ScrollableListPanel<RuntimeTask> {
157
154
  ) {
158
155
  super('tasks', 'Tasks', 'J', 'monitoring');
159
156
  this.showSelectionGutter = true; // I5: non-color selection affordance
157
+ this.filterEnabled = true;
158
+ this.filterLabel = 'Filter tasks';
160
159
  this.readModel = readModel;
161
160
  this.worktrees = worktrees;
162
161
  this.unsubscribers = [
@@ -183,6 +182,13 @@ export class TasksPanel extends ScrollableListPanel<RuntimeTask> {
183
182
  ];
184
183
  }
185
184
 
185
+ protected override filterMatches(task: RuntimeTask, q: string): boolean {
186
+ return task.title.toLowerCase().includes(q)
187
+ || task.status.toLowerCase().includes(q)
188
+ || task.id.toLowerCase().includes(q)
189
+ || String(task.kind).toLowerCase().includes(q);
190
+ }
191
+
186
192
  protected getItems(): readonly RuntimeTask[] {
187
193
  if (!this.readModel) return [];
188
194
  return sortTasks([...this.readModel.getSnapshot().tasks]);
@@ -193,7 +199,7 @@ export class TasksPanel extends ScrollableListPanel<RuntimeTask> {
193
199
  { text: task.status.padEnd(10), fg: statusColor(task.status) },
194
200
  { text: ` ${kindLabel(task.kind).padEnd(12)}`, fg: C.value },
195
201
  { text: ` ${task.id.slice(0, 8)} `, fg: C.dim },
196
- { text: task.title.slice(0, Math.max(0, width - 37)), fg: C.value },
202
+ { text: truncateDisplay(task.title, Math.max(0, width - 37)), fg: C.value },
197
203
  ], C, { selected });
198
204
  }
199
205
 
@@ -204,7 +210,7 @@ export class TasksPanel extends ScrollableListPanel<RuntimeTask> {
204
210
  return true;
205
211
  }
206
212
  if (key === 'end') {
207
- const tasks = this.getItems();
213
+ const tasks = this.getVisibleItems();
208
214
  this.selectedIndex = Math.max(0, tasks.length - 1);
209
215
  this.markDirty();
210
216
  return true;
@@ -215,7 +221,23 @@ export class TasksPanel extends ScrollableListPanel<RuntimeTask> {
215
221
  public render(width: number, height: number): Line[] {
216
222
  this.clampSelection();
217
223
  const intro = 'Live task lifecycle, ownership, retries, and result/error details across runtime execution domains.';
218
- const footerLines = [buildPanelLine(width, [[' Up/Down move Home/End jump', C.dim]])];
224
+ const visibleCount = this.getVisibleItems().length;
225
+ // Context-aware footer: position + only the keys that apply in the current
226
+ // (filtering vs browsing) state.
227
+ const footerLines = [
228
+ this.filterActive
229
+ ? buildKeyboardHints(width, [
230
+ { keys: 'type', label: 'filter tasks' },
231
+ { keys: 'Enter', label: 'apply' },
232
+ { keys: 'Esc', label: 'clear' },
233
+ ], C)
234
+ : buildKeyboardHints(width, [
235
+ { keys: visibleCount > 0 ? `${this.selectedIndex + 1}/${visibleCount}` : '0/0', label: 'task' },
236
+ { keys: '↑/↓', label: 'move' },
237
+ { keys: 'Home/End', label: 'jump' },
238
+ { keys: '/', label: 'filter' },
239
+ ], C),
240
+ ];
219
241
 
220
242
  if (!this.readModel) {
221
243
  const workspace = buildPanelWorkspace(width, height, {
@@ -272,7 +294,7 @@ export class TasksPanel extends ScrollableListPanel<RuntimeTask> {
272
294
  [' kind ', C.label],
273
295
  [selected.kind, C.value],
274
296
  [' owner ', C.label],
275
- [selected.owner.slice(0, Math.max(0, width - 46)), C.dim],
297
+ [truncateDisplay(selected.owner, Math.max(0, width - 46)), C.dim],
276
298
  ]));
277
299
  }
278
300
  postureLines.push(
@@ -359,7 +381,7 @@ export class TasksPanel extends ScrollableListPanel<RuntimeTask> {
359
381
  ]]));
360
382
  for (const record of attachedWorktrees.records.slice(0, 2)) {
361
383
  detailRows.push(buildPanelLine(width, [[
362
- ` ${record.state.padEnd(15)} ${record.path}`.slice(0, Math.max(0, width - 2)),
384
+ truncateDisplay(` ${record.state.padEnd(15)} ${record.path}`, Math.max(0, width - 2)),
363
385
  record.state === 'active' ? C.running : record.state === 'paused' ? C.blocked : C.dim,
364
386
  ]]));
365
387
  }
@@ -373,14 +395,14 @@ export class TasksPanel extends ScrollableListPanel<RuntimeTask> {
373
395
  if (selected.error) {
374
396
  detailRows.push(buildPanelLine(width, [
375
397
  [' Error: ', C.label],
376
- [selected.error.slice(0, Math.max(0, width - 10)), C.failed],
398
+ [truncateDisplay(selected.error, Math.max(0, width - 10)), C.failed],
377
399
  ]));
378
400
  }
379
401
  if (selected.result !== undefined) {
380
402
  const resultText = safeJson(selected.result);
381
403
  detailRows.push(buildPanelLine(width, [
382
404
  [' Result: ', C.label],
383
- [resultText.slice(0, Math.max(0, width - 11)), C.dim],
405
+ [truncateDisplay(resultText, Math.max(0, width - 11)), C.dim],
384
406
  ]));
385
407
  }
386
408
  }
@@ -12,25 +12,20 @@ import {
12
12
  buildStyledPanelLine,
13
13
  buildPanelWorkspace,
14
14
  resolveScrollablePanelSection,
15
+ extendPalette,
15
16
  DEFAULT_PANEL_PALETTE,
16
17
  type PanelWorkspaceSection,
17
18
  } from './polish.ts';
18
- import { truncateDisplay } from '../utils/terminal-width.ts';
19
+ import { truncateDisplay, wrapText } from '../utils/terminal-width.ts';
19
20
 
20
- const C = {
21
- headerBg: '#1a1a2e',
22
- headerFg: '#ffffff',
23
- statusBar: '#222233',
24
- statusFg: '#aaaaaa',
21
+ const C = extendPalette(DEFAULT_PANEL_PALETTE, {
22
+ // Domain-specific reasoning tones (purples) and the active-stream cyan have no
23
+ // clean shared equivalent; selection background maps to the shared selectBg.
25
24
  reasoningFg: '#aa88ff',
26
25
  activeFg: '#cc99ff',
27
- dimFg: '#555566',
28
26
  turnLabel: '#7766bb',
29
27
  activeLabel: '#00ffff',
30
- collapsedFg: '#888899',
31
- selected: '#00ffff',
32
- selectedBg: '#1a2a3a',
33
- } as const;
28
+ });
34
29
 
35
30
  interface ReasoningBlock {
36
31
  turnId: number;
@@ -39,23 +34,6 @@ interface ReasoningBlock {
39
34
  collapsed: boolean;
40
35
  }
41
36
 
42
- function wrapLines(text: string, width: number): string[] {
43
- const result: string[] = [];
44
- const raw = text.split('\n');
45
- for (const line of raw) {
46
- if (line.length <= width) {
47
- result.push(line);
48
- } else {
49
- let start = 0;
50
- while (start < line.length) {
51
- result.push(line.slice(start, start + width));
52
- start += width;
53
- }
54
- }
55
- }
56
- return result;
57
- }
58
-
59
37
  type FlatRow = { kind: 'header'; blockIndex: number; text: string } | { kind: 'content'; text: string };
60
38
 
61
39
  export class ThinkingPanel extends BasePanel {
@@ -126,8 +104,8 @@ export class ThinkingPanel extends BasePanel {
126
104
  lines: buildEmptyState(
127
105
  width,
128
106
  ' No reasoning content yet',
129
- 'When the model emits thinking or reasoning deltas, they accumulate here in expandable blocks.',
130
- [],
107
+ 'When the model emits thinking or reasoning deltas during a turn, they accumulate here in expandable per-turn blocks.',
108
+ [{ command: '/chat <prompt>', summary: 'start a turn with a reasoning-capable model to stream thinking here' }],
131
109
  DEFAULT_PANEL_PALETTE,
132
110
  ),
133
111
  },
@@ -157,12 +135,30 @@ export class ThinkingPanel extends BasePanel {
157
135
  ],
158
136
  };
159
137
 
160
- const selectedRow = flat[this.cursorIndex];
138
+ // Resolve the reasoning block that owns the cursor row (walk back to its
139
+ // header) so the detail block describes the actual turn, not just row kind.
140
+ let selectedBlockIndex = -1;
141
+ for (let i = Math.min(this.cursorIndex, flat.length - 1); i >= 0; i--) {
142
+ const row = flat[i];
143
+ if (row?.kind === 'header') { selectedBlockIndex = row.blockIndex; break; }
144
+ }
145
+ const selectedBlock = selectedBlockIndex >= 0 ? this.blocks[selectedBlockIndex] : undefined;
161
146
  const selectedSection: PanelWorkspaceSection = {
162
147
  title: 'Selected',
163
- lines: [
164
- buildPanelLine(width, [[' Row Type ', DEFAULT_PANEL_PALETTE.label], [selectedRow?.kind ?? 'none', DEFAULT_PANEL_PALETTE.value]]),
165
- ],
148
+ lines: selectedBlock
149
+ ? [
150
+ buildPanelLine(width, [
151
+ [' Turn ', DEFAULT_PANEL_PALETTE.label],
152
+ [String(selectedBlock.turnId), DEFAULT_PANEL_PALETTE.value],
153
+ [' State ', DEFAULT_PANEL_PALETTE.label],
154
+ [selectedBlock.active ? 'streaming' : 'complete', selectedBlock.active ? DEFAULT_PANEL_PALETTE.warn : DEFAULT_PANEL_PALETTE.good],
155
+ [' View ', DEFAULT_PANEL_PALETTE.label],
156
+ [selectedBlock.collapsed ? 'collapsed' : 'expanded', DEFAULT_PANEL_PALETTE.info],
157
+ [' Chars ', DEFAULT_PANEL_PALETTE.label],
158
+ [String(selectedBlock.content.length), DEFAULT_PANEL_PALETTE.dim],
159
+ ]),
160
+ ]
161
+ : [buildPanelLine(width, [[' No block selected', DEFAULT_PANEL_PALETTE.dim]])],
166
162
  };
167
163
 
168
164
  const reasoningSection = resolveScrollablePanelSection(width, height, {
@@ -195,7 +191,7 @@ export class ThinkingPanel extends BasePanel {
195
191
  }
196
192
 
197
193
  private _renderRow(width: number, row: FlatRow, isCursor: boolean): Line {
198
- const bg = isCursor ? C.selectedBg : '';
194
+ const bg = isCursor ? C.selectBg : '';
199
195
  if (row.kind === 'header') {
200
196
  const indicator = this.blocks[row.blockIndex]?.collapsed ? '▸' : '▾';
201
197
  const active = this.blocks[row.blockIndex]?.active;
@@ -217,7 +213,7 @@ export class ThinkingPanel extends BasePanel {
217
213
  const turnLabel = `Turn ${block.turnId}${block.active ? ' (streaming)' : ''}`;
218
214
  rows.push({ kind: 'header', blockIndex: i, text: turnLabel });
219
215
  if (!block.collapsed) {
220
- const wrapped = wrapLines(block.content || '(empty)', Math.max(1, width - 2));
216
+ const wrapped = wrapText(block.content || '(empty)', Math.max(1, width - 2));
221
217
  for (const line of wrapped) {
222
218
  rows.push({ kind: 'content', text: line });
223
219
  }