@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
package/CHANGELOG.md CHANGED
@@ -4,6 +4,107 @@ All notable changes to GoodVibes TUI.
4
4
 
5
5
  ---
6
6
 
7
+ ## [0.29.0] — 2026-07-03
8
+
9
+ ### Changes
10
+ - 2842a791 perf(ci): parallelize the per-file test runner; raise the test job timeout
11
+ - 218bb217 merge: panel-system remediation — 38 work orders, consoles not signposts (0.29.0)
12
+ - a37c028e fix(input): WO-160 smoke findings — overlay write-back clobber, /ops view parity, panel-list honesty, modal viewport
13
+ - 5741f633 fix(tests): isolate GitPanel non-repo test from the runner's in-tree TMPDIR
14
+ - 6eaea7c4 fix(panels): WO-160 consistency sweep — alias tests, action-substitute signposts
15
+ - 42164e34 fix(panels): cockpit c/i leave the key unconsumed on terminal or absent roster entries
16
+ - 10247db1 feat(input): make Ctrl+O a real keybind for the Ops Control panel
17
+ - ac598536 refactor(panels): route selected-row reads through getSelectedItem and ban raw [this.selectedIndex]
18
+ - 34bac3f0 chore(panels): tighten hex-literal baseline to exact actual counts
19
+ - 3d2caf90 integrate WO-153 into panel-ux-remediation
20
+ - 1f0ed5ca integrate WO-152 into panel-ux-remediation
21
+ - 8b6fbce6 integrate WO-151 into panel-ux-remediation
22
+ - ce0b7cb8 fix(panels): closure-audit findings — incident/settings-sync filtered desyncs, subscription start signposts
23
+ - ba7db35e feat(panels): WO-152 registry taxonomy, icons, always-register, lifecycle flags
24
+ - 7cbdd4ec feat(panels): WO-153 filter-model convergence onto ScrollableListPanel
25
+ - 40cb9153 feat(panels): WO-151 keybindings, help discoverability, one hint grammar
26
+ - 782e7752 fix(panels): services test/detail act on the filtered selection
27
+ - e8783a89 integrate WO-154 into panel-ux-remediation
28
+ - cdb399b9 integrate WO-140 into panel-ux-remediation
29
+ - 943089ea integrate WO-139 into panel-ux-remediation
30
+ - d527c223 integrate WO-141 into panel-ux-remediation
31
+ - a65ee307 integrate WO-150 into panel-ux-remediation
32
+ - a73e874b feat(panels): WO-140 planning cluster polish (wrfc/project-planning/work-plan)
33
+ - e1921ab8 feat(panels): WO-141 keep-panel hardening + residual signpost sweep
34
+ - 8b018746 feat(panels): WO-139 provider-cost leftovers — subscription/local-auth/cost-tracker
35
+ - da7b9550 feat(panels): WO-150 unify focus ownership in PanelManager
36
+ - 4c569ae8 feat(shell): WO-154 main-UX shell splash, footer cost/compact, overlay posture
37
+ - 7218349c fix(panels): wave 3a review findings — filtered-selection desyncs, residual signposts, dead constants
38
+ - 1cc9f195 integrate WO-138 into panel-ux-remediation
39
+ - 179a9a91 integrate WO-137 into panel-ux-remediation
40
+ - 4df3036e integrate WO-136 into panel-ux-remediation
41
+ - 20533671 integrate WO-135 into panel-ux-remediation
42
+ - 2f116f82 integrate WO-134 into panel-ux-remediation
43
+ - 595df267 integrate WO-133 into panel-ux-remediation
44
+ - 490dc881 feat(panels): WO-137 diagnostics trio — security, debug, system-messages
45
+ - 066fd91a feat(panels): WO-138 session-comm quad real actions (remote/communication/routes/qr)
46
+ - a9a1ebe0 feat(panels): WO-134 Plugins + Hooks control rooms
47
+ - e4380896 feat(panels): WO-136 Docs + Intelligence live actions
48
+ - 8f6a3f45 feat(panels): WO-133 explorer + preview lazy loading, git decorations, diff/reload keys
49
+ - 744dc460 feat(panels): WO-135 marketplace + skills real actions
50
+ - a04c86e9 fix(panels): reserve summary row inside stacked scrollable section budgets
51
+ - b2a43d9b fix(panels): reserve a row for the window-summary inside the scrollable budget
52
+ - acb3e3b5 fix(panels): bind w to dispatch teamwork review from Tasks; stop git panel swallowing i on loaded repos
53
+ - 4bc04330 integrate WO-130 into panel-ux-remediation
54
+ - e15c010b integrate WO-128 into panel-ux-remediation
55
+ - 3020d2e1 integrate WO-127 into panel-ux-remediation
56
+ - 1a0c0d76 integrate WO-132 into panel-ux-remediation
57
+ - 79ac40bd integrate WO-131 into panel-ux-remediation
58
+ - 61092aa0 integrate WO-126 into panel-ux-remediation
59
+ - f83d6820 feat(panels): WO-131 agent-runtime trio — tools + tasks + orchestration
60
+ - 8dafc5c0 feat(panels): WO-132 Git + Diff consoles — stage/unstage/commit, explicit init confirm, self-loading diff panel
61
+ - 24c5127e feat(panels): WO-128 plan dashboard revival
62
+ - 2c8e8d1f feat(panels): WO-126 symbol outline on tree-sitter
63
+ - 2d424e1c feat(panels): WO-130 Cockpit operator polish
64
+ - 66c0deeb feat(panels): WO-127 eval console — baseline seed, live run, regressions
65
+ - 1a53ef2c fix(panels): drop redundant slash-command guidance from knowledge review mode
66
+ - 16ba104d fix(tests): operator-surfaces gate verifies forensics alias resolves to incident
67
+ - a5b8f4e9 integrate FOLLOWUPS into panel-ux-remediation
68
+ - f60988eb integrate WO-125 into panel-ux-remediation
69
+ - 39996bfd integrate WO-124 into panel-ux-remediation
70
+ - 78fb4916 integrate WO-123 into panel-ux-remediation
71
+ - e5294103 integrate WO-122 into panel-ux-remediation
72
+ - 371f4d60 integrate WO-121 into panel-ux-remediation
73
+ - 1ff1e0ae integrate WO-120 into panel-ux-remediation
74
+ - 933b09a3 feat(panels): WO-120 register ops-control panel and wire operator actions
75
+ - 39bfc1a1 feat(panels): WO-125 wire worktree and sandbox lifecycle verbs to their registries
76
+ - 4992fd03 feat(panels): WO-121 control-plane approvals console
77
+ - ea1c47b0 feat(panels): WO-123 knowledge console replaces static command catalogue
78
+ - 91273a60 feat(panels): WO-124 settings-sync console — resolve conflicts in-panel, Tab browse modes
79
+ - 74f5d39d feat(panels): WO-122 policy governance console actions and depth
80
+ - f4679a7e fix(panels): broaden hex-literal ratchet to 3/6/8-digit forms, clear remaining raw hex in fullscreen-primitives and diff-panel
81
+ - 6e1b814c fix(panels): forensics resolves as alias to incident, not duplicate registration
82
+ - 6a0f4405 fix(tests): operator-surfaces gate asserts merged provider-health id instead of retired providers id
83
+ - 09d81371 fix(tests): retarget legacy-turn-bus ban from deleted context-visualizer-panel to its WO-113 successor token-budget-panel
84
+ - e835a821 integrate WO-114 into panel-ux-remediation
85
+ - 433b4c25 integrate WO-113 into panel-ux-remediation
86
+ - 70466337 integrate WO-112 into panel-ux-remediation
87
+ - a1ebf5da fix(tests): contract module count 30 after WO-110+WO-111 both deleted a module
88
+ - fd25fb19 integrate WO-111 into panel-ux-remediation
89
+ - 0d72a2ef integrate WO-110 into panel-ux-remediation
90
+ - 0a344b60 WO-112: Provider console merge: providers + accounts into provider-health
91
+ - d4c7c8b4 WO-111: Automation console merge: schedule + watchers into automation
92
+ - 1fc22d27 WO-110: Agent console merge: agent-logs into inspector
93
+ - 0d4858ff WO-114: Incident console merge: forensics into incident
94
+ - a4cdfb3c WO-113: Context/token merge: context into tokens
95
+ - fd8e84ef integrate WO-002 into panel-ux-remediation
96
+ - 1a85ece3 WO-002: Panel palette and chrome sweep (extendPalette everywhere)
97
+ - 1cd556da integrate WO-006 into panel-ux-remediation
98
+ - d6effd51 integrate WO-005 into panel-ux-remediation
99
+ - d7735d20 integrate WO-004 into panel-ux-remediation
100
+ - 9b1de69a integrate WO-003 into panel-ux-remediation
101
+ - e149323f integrate WO-001 into panel-ux-remediation
102
+ - 45e90fa7 WO-006: Shared-test decongestion: per-panel spec modules
103
+ - 2d7d5777 WO-001: Theme-token backbone + architecture-gate hex enforcement
104
+ - 207cd46b WO-003: Action-callback plumbing pattern + ConfirmState generalization
105
+ - abf6771b WO-005: session-maintenance evaluator dedup
106
+ - 4fe16129 WO-004: Dead code and dead keybinding removal
107
+
7
108
  ## [0.28.0] — 2026-06-30
8
109
 
9
110
  Panel navigation & UI/UX overhaul, plus the accumulated post-0.27.0 UX fixes.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml/badge.svg)](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
- [![Version](https://img.shields.io/badge/version-0.28.0-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
5
+ [![Version](https://img.shields.io/badge/version-0.29.0-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
6
6
 
7
7
  A terminal-native AI coding, operations, automation, knowledge, and integration console with a typed runtime, omnichannel surfaces, structured memory/knowledge, and a raw ANSI renderer.
8
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-tui",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
5
5
  "type": "module",
6
6
  "main": "src/main.ts",
@@ -0,0 +1,49 @@
1
+ import type { CommandRegistry } from '../command-registry.ts';
2
+ import { CostTrackerPanel } from '../../panels/cost-tracker-panel.ts';
3
+ import { openCommandPanel, requirePanelManager } from './runtime-services.ts';
4
+
5
+ /**
6
+ * /cost budget <usd> — makes the CostTrackerPanel's budget alert real (WO-139).
7
+ * Opens the cost panel (creating it via the registered factory if not already
8
+ * open) and sets its budget threshold directly, so the meter+alert already
9
+ * built into CostTrackerPanel.render() (:266-290) actually fires. 0 disables
10
+ * the alert. Mirrors the /auth local rotate-password → panel-instance-call
11
+ * pattern (local-auth-runtime.ts) rather than only printing a signpost.
12
+ */
13
+ export function registerCostRuntimeCommands(registry: CommandRegistry): void {
14
+ registry.register({
15
+ name: 'cost',
16
+ description: 'Inspect session/agent cost tracking and configure the budget alert threshold',
17
+ usage: '[panel|budget <usd>]',
18
+ handler(args, ctx) {
19
+ const sub = (args[0] ?? 'panel').toLowerCase();
20
+
21
+ if (sub === 'panel' || sub === 'open') {
22
+ openCommandPanel(ctx, 'cost');
23
+ return;
24
+ }
25
+
26
+ if (sub === 'budget') {
27
+ const raw = args[1];
28
+ const usd = raw !== undefined ? Number(raw) : NaN;
29
+ if (raw === undefined || !Number.isFinite(usd) || usd < 0) {
30
+ ctx.print('Usage: /cost budget <usd> (0 disables the alert)');
31
+ return;
32
+ }
33
+ const panelManager = requirePanelManager(ctx);
34
+ const panel = panelManager.open('cost');
35
+ if (panel instanceof CostTrackerPanel) {
36
+ panel.setBudgetThreshold(usd);
37
+ ctx.print(usd > 0
38
+ ? `Cost budget alert set to $${usd.toFixed(2)}.`
39
+ : 'Cost budget alert disabled.');
40
+ } else {
41
+ ctx.print('Cost tracking is not available in this session.');
42
+ }
43
+ return;
44
+ }
45
+
46
+ ctx.print('Usage: /cost [panel|budget <usd>]');
47
+ },
48
+ });
49
+ }
@@ -175,8 +175,12 @@ export function registerOperatorRuntimeCommands(registry: CommandRegistry): void
175
175
  const sub = args[0];
176
176
 
177
177
  if (sub === 'view' || sub === undefined) {
178
+ // Parity with the Ctrl+O keybind (handler-shortcuts.ts): prefer the
179
+ // flag-wired opener, else fall back to the always-registered panel,
180
+ // which renders its own honest not-configured state.
178
181
  if (ctx.openOpsPanel) ctx.openOpsPanel();
179
- else ctx.print('Operator Control Plane panel is not available. Enable the operator-control-plane feature flag.');
182
+ else if (ctx.showPanel) ctx.showPanel('ops-control');
183
+ else ctx.print('Operator Control Plane panel is not available in this shell.');
180
184
  return;
181
185
  }
182
186
 
@@ -56,6 +56,7 @@ import { registerOnboardingRuntimeCommands } from './commands/onboarding-runtime
56
56
  import { registerTtsRuntimeCommands } from './commands/tts-runtime.ts';
57
57
  import { registerCloudflareRuntimeCommands } from './commands/cloudflare-runtime.ts';
58
58
  import { registerWorkPlanRuntimeCommands } from './commands/work-plan-runtime.ts';
59
+ import { registerCostRuntimeCommands } from './commands/cost-runtime.ts';
59
60
 
60
61
  /**
61
62
  * registerBuiltinCommands - Register all built-in slash commands into the registry.
@@ -107,6 +108,7 @@ export function registerBuiltinCommands(registry: CommandRegistry): void {
107
108
  registerTtsRuntimeCommands(registry);
108
109
  registerCloudflareRuntimeCommands(registry);
109
110
  registerWorkPlanRuntimeCommands(registry);
111
+ registerCostRuntimeCommands(registry);
110
112
  registerLocalRuntimeCommands(registry);
111
113
  registerDiscoveryRuntimeCommands(registry);
112
114
  registerPlanningRuntimeCommands(registry);
@@ -78,14 +78,14 @@ export function handlePanelFocusToken(state: PanelFocusRouteState, token: InputT
78
78
  return { handled: true, panelFocused };
79
79
  }
80
80
  const kb = state.keybindingsManager;
81
- if (kb.matches('panel-tab-next', token)) {
82
- state.cyclePanelTab('next');
83
- return { handled: true, panelFocused };
84
- }
85
- if (kb.matches('panel-tab-prev', token)) {
86
- state.cyclePanelTab('prev');
87
- return { handled: true, panelFocused };
88
- }
81
+ // NOTE: panel-tab-next/prev, panel-close, and panel-close-all are handled
82
+ // globally in handleGlobalShortcutToken, which runs earlier in the feed
83
+ // loop and consumes those tokens before they ever reach this focused-panel
84
+ // route. Their old copies lived here too and had drifted (this route never
85
+ // called pm.hide() on close-all); they were unreachable, so they are gone
86
+ // and close/close-all semantics now live in exactly one place. Only
87
+ // panel-focus-toggle stays here, because it is meaningful only while the
88
+ // panel workspace already owns focus (it swaps between the two panes).
89
89
  if (kb.matches('panel-focus-toggle', token)) {
90
90
  // Switch keyboard focus between the top and bottom panes (no-op when
91
91
  // there is no visible, non-empty bottom pane).
@@ -93,33 +93,10 @@ export function handlePanelFocusToken(state: PanelFocusRouteState, token: InputT
93
93
  state.requestRender();
94
94
  return { handled: true, panelFocused };
95
95
  }
96
- // Alt+1..9 jump directly to the Nth workspace tab (across both panes).
97
- // The tokenizer maps the Alt modifier onto `meta`, so an Alt-held digit
98
- // arrives as { logicalName: '1'..'9', meta: true }. Gating on the modifier
99
- // keeps plain digits reaching the focused panel.
100
- if (token.meta && !token.ctrl && /^[1-9]$/.test(token.logicalName ?? '')) {
101
- const index = Number(token.logicalName) - 1;
102
- state.panelManager.activateWorkspaceIndex(index);
103
- state.requestRender();
104
- return { handled: true, panelFocused };
105
- }
106
- if (kb.matches('panel-close-all', token)) {
107
- const pm = state.panelManager;
108
- for (const p of pm.getAllOpen()) pm.close(p.id);
109
- panelFocused = false;
110
- state.requestRender();
111
- return { handled: true, panelFocused };
112
- }
113
- if (kb.matches('panel-close', token)) {
114
- const pm = state.panelManager;
115
- const active = pm.getActivePanel();
116
- if (active) {
117
- pm.close(active.id);
118
- }
119
- panelFocused = false;
120
- state.requestRender();
121
- return { handled: true, panelFocused };
122
- }
96
+ // Alt+1..9 (panel-tab-1..9) is a global KeyAction consumed earlier by
97
+ // handleGlobalShortcutToken, so those meta+digit tokens never reach here.
98
+ // Any other ctrl/meta combo this route does not own falls through so the
99
+ // global handler (which ran first) or the prompt routes can act on it.
123
100
  if (token.ctrl || token.meta) {
124
101
  return { handled: false, panelFocused };
125
102
  }
@@ -251,14 +251,10 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
251
251
  }
252
252
 
253
253
  if (token.type === 'key') {
254
- if (
255
- context.panelFocused
256
- && (!context.panelManager.isVisible()
257
- || context.panelManager.getAllOpen().length === 0
258
- || context.panelManager.getActivePanel() === null)
259
- ) {
260
- context.panelFocused = false;
261
- }
254
+ // Focus can never disagree with workspace visibility: PanelManager owns
255
+ // focusTarget and self-heals it, and context.panelFocused was seeded from
256
+ // it at feed entry — so no manual "unfocus if panels vanished" patch is
257
+ // needed here anymore.
262
258
  const shortcutState = {
263
259
  panelFocused: context.panelFocused,
264
260
  prompt: context.prompt,
@@ -115,6 +115,21 @@ export function handleGlobalShortcutToken(
115
115
  state.requestRender();
116
116
  return true;
117
117
 
118
+ case 'panel-focus-toggle': {
119
+ // Global entry point for the focus-toggle key (Ctrl+G): from the prompt
120
+ // it grabs focus for the panel workspace. Once the workspace already has
121
+ // focus we let it fall through (return false) so handlePanelFocusToken
122
+ // can do the top/bottom pane swap — keeping that behavior in one place.
123
+ if (state.panelFocused) return false;
124
+ const pm = state.panelManager;
125
+ if (pm.isVisible() && pm.getAllOpen().length > 0) {
126
+ state.panelFocused = true;
127
+ state.requestRender();
128
+ return true;
129
+ }
130
+ return false;
131
+ }
132
+
118
133
  case 'panel-tab-next':
119
134
  state.cyclePanelTab('next');
120
135
  return true;
@@ -123,6 +138,42 @@ export function handleGlobalShortcutToken(
123
138
  state.cyclePanelTab('prev');
124
139
  return true;
125
140
 
141
+ case 'panel-tab-1':
142
+ case 'panel-tab-2':
143
+ case 'panel-tab-3':
144
+ case 'panel-tab-4':
145
+ case 'panel-tab-5':
146
+ case 'panel-tab-6':
147
+ case 'panel-tab-7':
148
+ case 'panel-tab-8':
149
+ case 'panel-tab-9': {
150
+ // Alt+1..9: jump directly to the Nth workspace tab. Routed globally (like
151
+ // panel-tab-next/prev) so the jump works whether focus is on the prompt or
152
+ // the workspace; gated on visibility, matching cyclePanelTab semantics.
153
+ const pm = state.panelManager;
154
+ if (pm.isVisible()) {
155
+ const index = Number(action.slice('panel-tab-'.length)) - 1;
156
+ pm.activateWorkspaceIndex(index);
157
+ state.requestRender();
158
+ }
159
+ return true;
160
+ }
161
+
162
+ case 'panel-ops': {
163
+ // Ctrl+O: open the Ops Control panel. Prefer the command-context callback
164
+ // (wired only when the operator-control-plane feature flag is on) so it
165
+ // reuses whatever show/open semantics bootstrap.ts set up; otherwise fall
166
+ // back to opening the always-registered 'ops-control' panel type directly
167
+ // — it renders an honest not-configured state when its deps are absent.
168
+ if (state.commandContext?.openOpsPanel) {
169
+ state.commandContext.openOpsPanel();
170
+ } else {
171
+ state.panelManager.open('ops-control');
172
+ state.requestRender();
173
+ }
174
+ return true;
175
+ }
176
+
126
177
  case 'history-search':
127
178
  state.historySearch.open(state.prompt);
128
179
  state.requestRender();
@@ -140,8 +140,33 @@ export class InputHandler implements InputHandlerLike {
140
140
  public commandMode = false;
141
141
  /** True when the process indicator bar has keyboard focus. */
142
142
  public indicatorFocused = false;
143
- /** True when keyboard focus is on the active panel (arrow/enter go to panel, not prompt). */
144
- public panelFocused = false;
143
+ /**
144
+ * Fallback focus store used only when the panel manager does not implement
145
+ * focus ownership (lightweight test stubs). Production always delegates to
146
+ * PanelManager, which is the single source of truth.
147
+ */
148
+ private _panelFocusedFallback = false;
149
+ /**
150
+ * True when keyboard focus is on the active panel (arrow/enter go to panel,
151
+ * not prompt). Ownership lives in PanelManager (focusTarget); this reads and
152
+ * writes through to it so handler focus can never disagree with what the
153
+ * panel workspace actually shows.
154
+ */
155
+ public get panelFocused(): boolean {
156
+ const pm = this.uiServices.shell.panelManager;
157
+ return typeof pm.getFocusTarget === 'function'
158
+ ? pm.getFocusTarget() === 'panel'
159
+ : this._panelFocusedFallback;
160
+ }
161
+ public set panelFocused(value: boolean) {
162
+ const pm = this.uiServices.shell.panelManager;
163
+ if (typeof pm.focusPanels === 'function' && typeof pm.focusPrompt === 'function') {
164
+ if (value) pm.focusPanels();
165
+ else pm.focusPrompt();
166
+ } else {
167
+ this._panelFocusedFallback = value;
168
+ }
169
+ }
145
170
 
146
171
  public tokenizer = new InputTokenizer();
147
172
  public pasteRegistry = new Map<string, string>();
@@ -468,6 +493,15 @@ export class InputHandler implements InputHandlerLike {
468
493
  };
469
494
 
470
495
  this.requestRender = bufferedRequestRender;
496
+ // Snapshot the overlay flags before the feed: command handlers executed
497
+ // synchronously during token processing (e.g. /shortcuts, /help via
498
+ // ui-openers) mutate these directly on the handler, and an unconditional
499
+ // write-back of the pre-feed snapshot would silently revert them in the
500
+ // same keystroke (the "/shortcuts never displays" defect).
501
+ const helpOverlayActiveBefore = this.helpOverlayActive;
502
+ const helpScrollOffsetBefore = this.helpScrollOffset;
503
+ const shortcutsOverlayActiveBefore = this.shortcutsOverlayActive;
504
+ const shortcutsScrollOffsetBefore = this.shortcutsScrollOffset;
471
505
  try {
472
506
  const context = this.feedContext;
473
507
  // Sync mutable scalars from handler into the reused context.
@@ -506,10 +540,13 @@ export class InputHandler implements InputHandlerLike {
506
540
  this.commandMode = context.commandMode;
507
541
  this.panelFocused = context.panelFocused;
508
542
  this.indicatorFocused = context.indicatorFocused;
509
- this.helpOverlayActive = context.helpOverlayActive;
510
- this.helpScrollOffset = context.helpScrollOffset;
511
- this.shortcutsOverlayActive = context.shortcutsOverlayActive;
512
- this.shortcutsScrollOffset = context.shortcutsScrollOffset;
543
+ // Overlay flags: only apply the pipeline's value when the pipeline
544
+ // itself changed it (e.g. Escape closing the overlay). Otherwise keep
545
+ // the live handler value, which a command handler may have just set.
546
+ if (context.helpOverlayActive !== helpOverlayActiveBefore) this.helpOverlayActive = context.helpOverlayActive;
547
+ if (context.helpScrollOffset !== helpScrollOffsetBefore) this.helpScrollOffset = context.helpScrollOffset;
548
+ if (context.shortcutsOverlayActive !== shortcutsOverlayActiveBefore) this.shortcutsOverlayActive = context.shortcutsOverlayActive;
549
+ if (context.shortcutsScrollOffset !== shortcutsScrollOffsetBefore) this.shortcutsScrollOffset = context.shortcutsScrollOffset;
513
550
  this.selectionCallback = context.selectionCallback;
514
551
  this.nextPasteId = context.nextPasteId;
515
552
  this.nextImageId = context.nextImageId;
@@ -38,6 +38,16 @@ export type KeyAction =
38
38
  | 'panel-close-all'
39
39
  | 'panel-tab-next'
40
40
  | 'panel-tab-prev'
41
+ | 'panel-tab-1'
42
+ | 'panel-tab-2'
43
+ | 'panel-tab-3'
44
+ | 'panel-tab-4'
45
+ | 'panel-tab-5'
46
+ | 'panel-tab-6'
47
+ | 'panel-tab-7'
48
+ | 'panel-tab-8'
49
+ | 'panel-tab-9'
50
+ | 'panel-ops'
41
51
  | 'panel-focus-toggle'
42
52
  | 'history-search'
43
53
  | 'search'
@@ -52,7 +62,6 @@ export type KeyAction =
52
62
  | 'undo'
53
63
  | 'redo'
54
64
  | 'paste'
55
- | 'replay-panel'
56
65
  | 'word-back'
57
66
  | 'word-forward'
58
67
  | 'kill-to-start'
@@ -70,6 +79,16 @@ export const ACTION_DESCRIPTIONS: Record<KeyAction, string> = {
70
79
  'panel-close-all': 'Close all open panels',
71
80
  'panel-tab-next': 'Next workspace panel tab',
72
81
  'panel-tab-prev': 'Previous workspace panel tab',
82
+ 'panel-tab-1': 'Jump to workspace panel tab 1',
83
+ 'panel-tab-2': 'Jump to workspace panel tab 2',
84
+ 'panel-tab-3': 'Jump to workspace panel tab 3',
85
+ 'panel-tab-4': 'Jump to workspace panel tab 4',
86
+ 'panel-tab-5': 'Jump to workspace panel tab 5',
87
+ 'panel-tab-6': 'Jump to workspace panel tab 6',
88
+ 'panel-tab-7': 'Jump to workspace panel tab 7',
89
+ 'panel-tab-8': 'Jump to workspace panel tab 8',
90
+ 'panel-tab-9': 'Jump to workspace panel tab 9',
91
+ 'panel-ops': 'Open the Ops Control panel',
73
92
  'panel-focus-toggle': 'Switch keyboard focus between top and bottom pane',
74
93
  'history-search': 'Reverse input history search',
75
94
  'search': 'Toggle conversation search',
@@ -84,7 +103,6 @@ export const ACTION_DESCRIPTIONS: Record<KeyAction, string> = {
84
103
  'undo': 'Undo last prompt edit',
85
104
  'redo': 'Redo last undone edit',
86
105
  'paste': 'Paste from clipboard (image priority)',
87
- 'replay-panel': 'Open / close the Replay panel',
88
106
  'word-back': 'Move cursor to start of previous word (Alt+B)',
89
107
  'word-forward': 'Move cursor to end of next word (Alt+F)',
90
108
  'kill-to-start': 'Kill from cursor to start of line into kill ring (Ctrl+U)',
@@ -103,6 +121,24 @@ export const DEFAULT_KEYBINDINGS: Record<KeyAction, KeyCombo[]> = {
103
121
  'panel-close-all': [{ key: 'x', ctrl: true, shift: true }],
104
122
  'panel-tab-next': [{ key: ']', ctrl: true }],
105
123
  'panel-tab-prev': [{ key: '[', ctrl: true }],
124
+ // Alt+1..9: jump directly to the Nth workspace panel tab (across both panes).
125
+ // The tokenizer delivers Alt as the token's `meta` modifier; comboMatches /
126
+ // lookup treat `meta` as an alias for `alt`, so these alt-combos route through
127
+ // the same rebindable path as every other action.
128
+ 'panel-tab-1': [{ key: '1', alt: true }],
129
+ 'panel-tab-2': [{ key: '2', alt: true }],
130
+ 'panel-tab-3': [{ key: '3', alt: true }],
131
+ 'panel-tab-4': [{ key: '4', alt: true }],
132
+ 'panel-tab-5': [{ key: '5', alt: true }],
133
+ 'panel-tab-6': [{ key: '6', alt: true }],
134
+ 'panel-tab-7': [{ key: '7', alt: true }],
135
+ 'panel-tab-8': [{ key: '8', alt: true }],
136
+ 'panel-tab-9': [{ key: '9', alt: true }],
137
+ // Ctrl+O: open the Ops Control panel (operator intervention console).
138
+ // Routed globally in handleGlobalShortcutToken: prefers commandContext.openOpsPanel()
139
+ // when the operator-control-plane feature flag wired it, else falls back to opening
140
+ // the always-registered 'ops-control' panel type directly via the panel manager.
141
+ 'panel-ops': [{ key: 'o', ctrl: true }],
106
142
  // Ctrl+G: toggle keyboard focus between the top and bottom panes. Ctrl+G is
107
143
  // otherwise unbound in the default table.
108
144
  'panel-focus-toggle': [{ key: 'g', ctrl: true }],
@@ -122,7 +158,6 @@ export const DEFAULT_KEYBINDINGS: Record<KeyAction, KeyCombo[]> = {
122
158
  'undo': [{ key: 'z', ctrl: true }],
123
159
  'redo': [{ key: 'z', ctrl: true, shift: true }],
124
160
  'paste': [{ key: 'v', ctrl: true }],
125
- 'replay-panel': [{ key: 'r', ctrl: true, shift: true }],
126
161
  // Word navigation (Alt+B / Alt+F — emacs readline standard)
127
162
  'word-back': [{ key: 'b', alt: true }],
128
163
  'word-forward': [{ key: 'f', alt: true }],
@@ -245,9 +280,13 @@ export class KeybindingsManager {
245
280
  * lookup — O(1) keybinding lookup by token.
246
281
  * Returns the matching KeyAction, or null if no binding matches.
247
282
  */
248
- lookup(token: { logicalName?: string; ctrl?: boolean; shift?: boolean; alt?: boolean }): KeyAction | null {
283
+ lookup(token: { logicalName?: string; ctrl?: boolean; shift?: boolean; alt?: boolean; meta?: boolean }): KeyAction | null {
249
284
  if (!token.logicalName) return null;
250
- const key = `${token.logicalName}:${token.ctrl ? 1 : 0}:${token.shift ? 1 : 0}:${token.alt ? 1 : 0}`;
285
+ // The tokenizer delivers the Alt modifier as `meta`; the binding table stores
286
+ // it as `alt`. Treat the two as one modifier at the matching boundary so
287
+ // Alt-based combos (word-nav, kill-ring, panel-tab-1..9) resolve at runtime.
288
+ const alt = token.alt ?? token.meta;
289
+ const key = `${token.logicalName}:${token.ctrl ? 1 : 0}:${token.shift ? 1 : 0}:${alt ? 1 : 0}`;
251
290
  return this.lookupMap.get(key) ?? null;
252
291
  }
253
292
 
@@ -268,7 +307,7 @@ export class KeybindingsManager {
268
307
  */
269
308
  matches(
270
309
  action: KeyAction,
271
- token: { logicalName?: string; ctrl?: boolean; shift?: boolean; alt?: boolean },
310
+ token: { logicalName?: string; ctrl?: boolean; shift?: boolean; alt?: boolean; meta?: boolean },
272
311
  ): boolean {
273
312
  const combos = this.bindings[action];
274
313
  if (!combos) return false;
@@ -277,12 +316,13 @@ export class KeybindingsManager {
277
316
 
278
317
  private comboMatches(
279
318
  combo: KeyCombo,
280
- token: { logicalName?: string; ctrl?: boolean; shift?: boolean; alt?: boolean },
319
+ token: { logicalName?: string; ctrl?: boolean; shift?: boolean; alt?: boolean; meta?: boolean },
281
320
  ): boolean {
282
321
  if (token.logicalName !== combo.key) return false;
283
322
  if (!!combo.ctrl !== !!token.ctrl) return false;
284
323
  if (!!combo.shift !== !!token.shift) return false;
285
- if (!!combo.alt !== !!token.alt) return false;
324
+ // Alt arrives as `meta` from the tokenizer; accept either as the Alt modifier.
325
+ if (!!combo.alt !== !!(token.alt ?? token.meta)) return false;
286
326
  return true;
287
327
  }
288
328