@pellux/goodvibes-tui 0.18.19 → 0.18.23

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 (42) hide show
  1. package/CHANGELOG.md +170 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/core/conversation-rendering.ts +20 -6
  5. package/src/input/commands/session.ts +0 -1
  6. package/src/input/feed-context-factory.ts +236 -0
  7. package/src/input/handler-feed-routes.ts +10 -0
  8. package/src/input/handler-feed.ts +44 -6
  9. package/src/input/handler-shortcuts.ts +138 -125
  10. package/src/input/handler.ts +121 -119
  11. package/src/input/keybindings.ts +30 -0
  12. package/src/panels/approval-panel.ts +54 -74
  13. package/src/panels/automation-control-panel.ts +119 -161
  14. package/src/panels/base-panel.ts +71 -0
  15. package/src/panels/communication-panel.ts +68 -107
  16. package/src/panels/confirm-state.ts +61 -0
  17. package/src/panels/control-plane-panel.ts +116 -172
  18. package/src/panels/git-panel.ts +9 -0
  19. package/src/panels/hooks-panel.ts +101 -138
  20. package/src/panels/incident-review-panel.ts +55 -107
  21. package/src/panels/knowledge-panel.ts +63 -14
  22. package/src/panels/local-auth-panel.ts +76 -93
  23. package/src/panels/marketplace-panel.ts +19 -12
  24. package/src/panels/mcp-panel.ts +108 -155
  25. package/src/panels/ops-control-panel.ts +50 -85
  26. package/src/panels/panel-manager.ts +22 -2
  27. package/src/panels/plugins-panel.ts +36 -60
  28. package/src/panels/routes-panel.ts +89 -141
  29. package/src/panels/scrollable-list-panel.ts +71 -16
  30. package/src/panels/security-panel.ts +101 -137
  31. package/src/panels/services-panel.ts +58 -102
  32. package/src/panels/settings-sync-panel.ts +76 -122
  33. package/src/panels/skills-panel.ts +44 -0
  34. package/src/panels/subscription-panel.ts +69 -80
  35. package/src/panels/tasks-panel.ts +129 -179
  36. package/src/panels/watchers-panel.ts +88 -137
  37. package/src/renderer/buffer.ts +11 -0
  38. package/src/renderer/diff.ts +8 -0
  39. package/src/renderer/help-overlay.ts +37 -28
  40. package/src/renderer/markdown.ts +3 -145
  41. package/src/renderer/status-token.ts +71 -0
  42. package/src/version.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,176 @@ All notable changes to GoodVibes TUI.
4
4
 
5
5
  ---
6
6
 
7
+ ## [0.18.23] — 2026-04-16
8
+
9
+ ### Wave 4α+β performance bundle + α review follow-ups + regression fixes
10
+
11
+ Bundles Wave 4α (conversation-rendering double-parse elimination) and Wave 4β
12
+ (feed-context object reuse) into a single release, adds documentation and test
13
+ follow-ups from the 4α review, and fixes two regressions surfaced during that
14
+ review.
15
+
16
+ ### Performance (Wave 4α — conversation-rendering)
17
+
18
+ - **`src/core/conversation-rendering.ts`** — eliminates the legacy `renderMarkdown()`
19
+ duplicate call used for `'code'` mode line counting. The 'all' mode retain its
20
+ intentional measurement pass (see inline comment for rationale); the 'code' mode
21
+ now derives its gutter width from the single `renderMarkdownTracked()` call.
22
+
23
+ ### Performance (Wave 4β — feed-context object reuse)
24
+
25
+ - **`src/input/handler.ts`** — `InputHandler` now allocates a single `InputFeedContext`
26
+ at startup (`initFeedContext()`) and mutates only the 14 mutable fields before
27
+ each `feedInputTokens()` call via `syncFeedContextMutableFields()`. Stable service
28
+ handles, closures, and callbacks are wired once and never re-assigned. Eliminates
29
+ per-keystroke allocations on the hot input path.
30
+ - **`src/input/feed-context-factory.ts`** (new) — extracts `buildInitialFeedContext()`
31
+ and the `FeedContextMutableInit` / `FeedContextStableRefs` / `FeedContextClosures`
32
+ interfaces out of `handler.ts` to keep `handler.ts` under the 800-line architecture
33
+ cap.
34
+
35
+ ### Regression fixes
36
+
37
+ - **R1 — `handler.ts` architecture cap** — `handler.ts` was 830 lines after Wave 4β.
38
+ Extracted factory functions into `src/input/feed-context-factory.ts`; `handler.ts`
39
+ is now exactly 800 lines. `bun run architecture:check` passes.
40
+ - **R2 — `SearchableListPanel.buildFilterInputLine` cursor glyph** — the focused
41
+ filter line (`[Label] query_`) was rendering the trailing `_` as the block cursor
42
+ glyph `█` because `buildSearchInputLine` substitutes `_` → `█` when `active:true`.
43
+ Fixed by passing `active:false` with explicit `inputBg`/`info` colors; the
44
+ `[Label] ` bracket format provides the focused visual affordance without triggering
45
+ the substitution. Fixes the pre-existing test failure from 0.18.22.
46
+
47
+ ### Quality bumps (Wave 4α review follow-ups)
48
+
49
+ - **F1** — `src/test/input/feed-context-reuse.test.ts`: added mutable-field assertion
50
+ verifying `ctx.prompt` and `ctx.cursorPos` update between feeds (feeds 'a' → 'b',
51
+ asserts accumulated mutation).
52
+ - **F2** — `src/test/input/keybinding-lookup.test.ts`: replaced no-op reload test
53
+ with a real temp-file config override (remaps `search` → Ctrl+G, verifies Ctrl+F
54
+ returns null); added a second test for conflicting bindings (two actions mapped to
55
+ same combo resolves to one of them, not null).
56
+ - **F3** — `src/input/handler-feed.ts`: added JSDoc to `InputFeedContext` interface
57
+ documenting all mutable-per-feed fields vs. stable service handles and explaining
58
+ the reuse rationale.
59
+ - **F4** — `src/input/feed-context-factory.ts`: `syncFeedContextMutableFields()` has
60
+ full JSDoc listing every synced field and documenting intentional exclusions.
61
+ - **F5** — `src/core/conversation-rendering.ts`: added inline comment block explaining
62
+ why 'all' mode requires a double-call to `renderMarkdownTracked` (Option B) and
63
+ why single-pass is not feasible.
64
+
65
+ ---
66
+
67
+ ## [0.18.22] — 2026-04-16
68
+
69
+ ### Wave 3b / Tier 2 TUI UX Consistency — Panel Migration Batch
70
+
71
+ Migrates 7 more BasePanel-direct panels to `ScrollableListPanel<T>`, restores
72
+ section-title text lost during prior migrations, and fixes 8 pre-existing test regressions.
73
+
74
+ ### Migrated panels
75
+
76
+ - **`src/panels/hooks-panel.ts`** — `HooksPanel` → `ScrollableListPanel<HookEntry>`.
77
+ Contracts/chains/managed/file stats in `header`; selected hook detail, activity (with `Recent Activity` label), authoring (with `Authoring` label) in `footer`. Empty state shows extra context via `header` parameter.
78
+ - **`src/panels/mcp-panel.ts`** — `McpPanel` → `ScrollableListPanel<McpServerSecurityEntry>`.
79
+ Derived type via `ReturnType<McpRegistry['listServerSecurity']>[number]` since no named export exists.
80
+ `MCP posture` label + stats + guidance in `header`; selected server detail, repair actions, decision log in `footer`.
81
+ - **`src/panels/approval-panel.ts`** — `ApprovalPanel` → `ScrollableListPanel<ApprovalRow>`.
82
+ `Approval posture` label + approval counts + guidance + `Selected Lane` label + detail in `header`; nav hint in `footer`.
83
+ - **`src/panels/security-panel.ts`** — `SecurityPanel` → `ScrollableListPanel<TokenAuditResult>`.
84
+ Governance + threat lines in `header`; selected detail + attack path findings in `footer`.
85
+ - **`src/panels/services-panel.ts`** — `ServicesPanel` → `ScrollableListPanel<ServicePanelEntry>`.
86
+ `r` (refresh) and `t` (test selected) key overrides preserved; loading state handled via early return.
87
+ - **`src/panels/subscription-panel.ts`** — `SubscriptionPanel` → `ScrollableListPanel<SubscriptionRow>`.
88
+ Fully overrides `handleInput` (uses `ArrowUp`/`ArrowDown`); logout confirm state preserved; empty state uses direct `buildPanelWorkspace` path.
89
+ - **`src/panels/tasks-panel.ts`** — `TasksPanel` → `ScrollableListPanel<RuntimeTask>`.
90
+ `!readModel` early-exit preserved; `buildSummaryBlock` in `header`, `buildDetailBlock` in `footer`.
91
+ - **`src/panels/incident-review-panel.ts`** — `IncidentReviewPanel` → `ScrollableListPanel<FailureReport>`.
92
+ `Action Rail` label added before action guidance lines in `footer`.
93
+ - **`src/panels/communication-panel.ts`** — `CommunicationPanel` → `ScrollableListPanel<CommunicationRecord>`.
94
+ `Communication posture` label added to both posture line arrays (empty + populated states).
95
+
96
+ ### Panels kept as BasePanel
97
+
98
+ - `PolicyPanel`, `RemotePanel`, `ProviderHealthPanel`, `PanelListPanel`, `OrchestrationPanel`,
99
+ `MarketplacePanel`, `SchedulePanel`, `MemoryPanel`, `KnowledgePanel`, `SkillsPanel`,
100
+ `SessionBrowserPanel` — all use `resolveScrollablePanelSection`, multi-line-per-item render,
101
+ dual-mode browsing, `setInterval`, or `canRenderNow()` / `reportRenderDuration()` patterns
102
+ incompatible with `ScrollableListPanel<T>`.
103
+
104
+ ### Test fixes
105
+
106
+ Restored section-title strings (`'Approval posture'`, `'Communication posture'`, `'MCP posture'`,
107
+ `'Action Rail'`, `'Recent Activity'`, `'Selected Lane'`) dropped during migration, fixing 8
108
+ regressions across `approval-panel`, `communication-panel`, `hooks-panel`, `mcp-panel`, and
109
+ `incident-review-panel` test files.
110
+
111
+ ---
112
+
113
+ ## [0.18.21] — 2026-04-16
114
+
115
+ ### Wave 3a / Tier 2 TUI UX Consistency Infrastructure — I5
116
+
117
+ Final item from Wave 3a: selection gutter and filter input UX consistency across list panels.
118
+
119
+ ### I5 — Selection gutter + filter input label conventions
120
+
121
+ - **`src/panels/scrollable-list-panel.ts`** — `ScrollableListPanel`: added opt-in `protected showSelectionGutter = false`. When enabled, `renderList()` post-processes each item line to prepend a 2-column left gutter: `▸ ` (info color, bold) for the selected row, ` ` for all others. Line width is preserved by dropping the last 2 cells. Default off to avoid breaking panels with custom selection indicators.
122
+ - **`src/panels/scrollable-list-panel.ts`** — `SearchableListPanel`: added `protected buildFilterInputLine(width, label, focused)`. Renders the filter line with context-sensitive label formatting: `[Filter] query_` when `focused=true` (active, bold, cursor visible), `Filter: query` when `focused=false` (dim, no cursor). Delegates to `buildSearchInputLine` from `polish.ts`.
123
+ - **New test file `src/test/panels/scrollable-list-panel-i5.test.ts`**: 13 tests covering gutter on/off, column position of `▸`, line-width preservation, and filter label format in both focused/unfocused states.
124
+
125
+ ---
126
+
127
+ ## [0.18.20] — 2026-04-16
128
+
129
+ ### Wave 3a / Tier 2 TUI UX Consistency Infrastructure
130
+
131
+ Six infrastructure items that make the TUI behave consistently across all panels.
132
+
133
+ ### I1 — Reusable inline confirm dialog
134
+
135
+ - **New file `src/panels/confirm-state.ts`**: exports `ConfirmState<T>`, `handleConfirmInput<T>`, and `renderConfirmLines<T>`. Identical y/n UX across all panels: y confirms, n/Esc cancels, any other key is absorbed while confirm is active.
136
+ - **SkillsPanel**: 'd' key now shows an inline `Confirmation` section before surfacing the shell delete hint. Pressing Esc on the confirm panel cancels it via the generic helper.
137
+ - **KnowledgePanel**: 's' (stale) and 'c' (contradicted) now prompt confirm before calling `registry.review()`. Error from review mutation is surfaced via I2 `setError()`.
138
+ - **SubscriptionPanel**: 'n' and Escape now cancel a pending logout confirmation via the confirm helper.
139
+
140
+ ### I2 — Error surface slot on BasePanel
141
+
142
+ - **`src/panels/base-panel.ts`**: added `protected lastError`, `setError()`, `clearError()`, `renderErrorLine(width)`. Auto-clear on next keypress in `ScrollableListPanel.handleInput()`.
143
+ - **`src/panels/scrollable-list-panel.ts`**: `renderList()` prepends the error line to the `effectiveFooter` — visible in both normal and empty states.
144
+ - **MarketplacePanel**: `refresh()` now wraps catalog load in try/catch and calls `setError()` on failure. Clears error on successful reload.
145
+ - **KnowledgePanel**: `registry.review()` call in confirm dispatch wrapped in try/catch, wired to `setError()`.
146
+
147
+ ### I3 — Loading spinner slot on BasePanel
148
+
149
+ - **`src/panels/base-panel.ts`**: added `loadingState: 'idle'|'loading'|'error'`, `startLoading(label?)`, `stopLoading()`, `renderLoadingLine(width, frame)`. Uses `SPINNER_FRAMES` from `src/renderer/progress.ts`.
150
+ - **`src/panels/scrollable-list-panel.ts`**: `renderList()` short-circuits to a spinner-only view when `loadingState === 'loading'`.
151
+ - **GitPanel**: `openDiff()` now calls `startLoading('Loading diff...')` before the await and `stopLoading()` in both success and error paths. The `render()` method checks `this.loadingState === 'loading'` to show the spinner while the diff is being fetched.
152
+
153
+ ### I4 — Accessible status tokens
154
+
155
+ - **New file `src/renderer/status-token.ts`**: exports `buildStatusToken(state, label, opts?)` → `Cell[]`. State map: `good=✓`, `warn=⚠`, `bad=✕`, `info=○`. Glyph + color together so colorblind users can distinguish states without relying on color alone.
156
+ - **ApprovalPanel**: recent approvals/denials/pending row now uses inline `✓ approvals (N) ✕ denials (N) ○ pending (N)` cells instead of bare color-only counts.
157
+
158
+ ### I6 — Two-stage Escape in panel focus
159
+
160
+ - **`src/input/handler-feed-routes.ts`**: `handlePanelFocusToken()` now passes `'escape'` to the active panel's `handleInput()` BEFORE deciding to unfocus. If the panel returns `true` (e.g. dismisses a confirm dialog or clears a search), the panel stays focused. Only if the panel returns `false` does the router set `panelFocused = false`.
161
+
162
+ ### Tests
163
+
164
+ - **`src/test/renderer/status-token.test.ts`** (8 tests): glyph/color/count/override coverage for `buildStatusToken`.
165
+ - **`src/test/panels/base-panel-ux.test.ts`** (16 tests): `setError`/`clearError`/`renderErrorLine` and `startLoading`/`stopLoading`/`renderLoadingLine` state transitions.
166
+ - **`src/test/panels/confirm-state.test.ts`** (13 tests): `handleConfirmInput` all four return values + `renderConfirmLines` width/content.
167
+ - **`src/test/panels/knowledge-panel.test.ts`**: updated to reflect I1 two-step confirm for `'s'` (stale) action.
168
+
169
+ ### Tests & Checks
170
+
171
+ - Test suite: 441/441 passing (3 new test files)
172
+ - Architecture check: passing (298 non-test source files)
173
+ - Typecheck: clean
174
+
175
+ ---
176
+
7
177
  ## [0.18.19] — 2026-04-16
8
178
 
9
179
  ### Quality bump — address sub-10 dimensions from 0.18.18 review
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.18.19-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
5
+ [![Version](https://img.shields.io/badge/version-0.18.23-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.18.19",
3
+ "version": "0.18.23",
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",
@@ -1,5 +1,5 @@
1
1
  import { UIFactory } from '../renderer/ui-factory.ts';
2
- import { renderMarkdown, renderMarkdownTracked } from '../renderer/markdown.ts';
2
+ import { renderMarkdownTracked } from '../renderer/markdown.ts';
3
3
  import { renderToolCallBlock } from '../renderer/tool-call.ts';
4
4
  import { renderThinkingBlock } from '../renderer/thinking.ts';
5
5
  import { renderSystemMessage } from '../renderer/system-message.ts';
@@ -102,10 +102,24 @@ export function renderConversationAssistantMessage(
102
102
  if (message.content) {
103
103
  const showAllLineNumbers = lineNumberMode === 'all';
104
104
  const showCodeBlockLineNumbers = lineNumberMode === 'all' ? false : lineNumberMode === 'code';
105
- const preRendered = showAllLineNumbers
106
- ? renderMarkdown(message.content, width, { codeBlockLineNumbers: false })
107
- : null;
108
- const totalLines = preRendered?.length ?? 0;
105
+ // First pass: measure totalLines for gutter sizing (only when line-numbers='all').
106
+ // When line numbers are off, skip the measurement pass entirely.
107
+ //
108
+ // NOTE: The 'all' mode intentionally calls renderMarkdownTracked twice:
109
+ // 1. Measure pass: render at full `width` to get the total line count, which
110
+ // determines `numWidth` (digit count) and thus `gutterW` (gutter column width).
111
+ // 2. Render pass: render at `width - gutterW` with the gutter factored in.
112
+ //
113
+ // Single-pass is not feasible here because `numWidth` depends on `totalLines`,
114
+ // which is unknown before rendering. The 4α commit message claim that this
115
+ // "eliminates double-parse when line numbers are enabled" was inaccurate:
116
+ // 4α eliminated the legacy `renderMarkdown()` duplicate that was used for the
117
+ // code-block line-number mode ('code'). The 'all' mode double-call is unavoidable
118
+ // by design and remains unchanged.
119
+ const measureWidth = showAllLineNumbers ? width : 0;
120
+ const totalLines = showAllLineNumbers
121
+ ? renderMarkdownTracked(message.content, measureWidth, { codeBlockLineNumbers: false }).lines.length
122
+ : 0;
109
123
  const numWidth = Math.max(3, String(totalLines).length);
110
124
  const gutterW = numWidth + 3;
111
125
  const contentWidth = showAllLineNumbers ? width - gutterW : width;
@@ -239,7 +253,7 @@ export function renderConversationToolMessage(
239
253
  // Leave invalid JSON as-is.
240
254
  }
241
255
  }
242
- context.history.addLines(renderMarkdown(contentToRender, width));
256
+ context.history.addLines(renderMarkdownTracked(contentToRender, width).lines);
243
257
  }
244
258
 
245
259
  const renderedLineCount = context.history.getLineCount() - startLine;
@@ -347,7 +347,6 @@ export const sessionCommand: SlashCommand = {
347
347
  break;
348
348
 
349
349
  case 'cancel':
350
- case 'x':
351
350
  handleCancel(rest, context);
352
351
  break;
353
352
 
@@ -0,0 +1,236 @@
1
+ /**
2
+ * feed-context-factory.ts — Construction and mutable-field sync for InputFeedContext.
3
+ *
4
+ * Extracted from handler.ts (Wave 4α review, 0.18.23) to keep handler.ts under the
5
+ * 800-line architecture cap.
6
+ *
7
+ * Two exported functions:
8
+ * - `buildInitialFeedContext` — assembles the long-lived InputFeedContext from
9
+ * pre-built field groups. Called once at InputHandler construction time.
10
+ * - `syncFeedContextMutableFields` — copies mutable scalar fields into the
11
+ * already-allocated context before each feed() dispatch.
12
+ */
13
+ import type { InputFeedContext } from './handler-feed.ts';
14
+ import type { SelectionManager } from './selection.ts';
15
+ import type { InfiniteBuffer } from '../core/history.ts';
16
+ import type { CommandRegistry, CommandContext } from './command-registry.ts';
17
+ import type { AutocompleteEngine } from './autocomplete.ts';
18
+ import type { FilePickerModal } from './file-picker.ts';
19
+ import type { ModelPickerModal } from './model-picker.ts';
20
+ import type { SelectionModal } from './selection-modal.ts';
21
+ import type { SelectionResult } from './selection-modal.ts';
22
+ import type { SearchManager } from './search.ts';
23
+ import type { InputHistory, HistorySearch } from './input-history.ts';
24
+ import type { ConversationManager } from '../core/conversation';
25
+ import type { ProcessModal } from '../renderer/process-modal.ts';
26
+ import type { LiveTailModal } from '../renderer/live-tail-modal.ts';
27
+ import type { BlockActionsMenu } from '../renderer/block-actions.ts';
28
+ import type { AgentDetailModal } from '../renderer/agent-detail-modal.ts';
29
+ import type { ContextInspectorModal } from '../renderer/context-inspector.ts';
30
+ import type { BookmarkModal } from './bookmark-modal.ts';
31
+ import type { SettingsModal } from './settings-modal.ts';
32
+ import type { SessionPickerModal } from './session-picker-modal.ts';
33
+ import type { ProfilePickerModal } from './profile-picker-modal.ts';
34
+ import type { WrappedPromptInfo } from './handler-prompt-buffer.ts';
35
+ import type { Panel } from '../panels/types.ts';
36
+ import type { PanelManager } from '../panels/panel-manager.ts';
37
+ import type { KeybindingsManager } from './keybindings.ts';
38
+
39
+ /**
40
+ * Initial mutable scalar values for InputFeedContext.
41
+ *
42
+ * **Mutable fields** (synced per-feed via syncFeedContextMutableFields or inside
43
+ * action closures that call syncFeedContextMutableFields):
44
+ * - `prompt`, `cursorPos` — current text buffer state
45
+ * - `commandMode`, `panelFocused`, `indicatorFocused` — focus-mode flags
46
+ * - `helpOverlayActive`, `helpScrollOffset` — help overlay state
47
+ * - `shortcutsOverlayActive`, `shortcutsScrollOffset` — shortcuts overlay state
48
+ * - `nextPasteId`, `nextImageId` — monotonically increasing ID counters
49
+ * - `mouseDownRow`, `mouseDownCol` — drag-tracking coordinates
50
+ * - `contentWidth` — reflow width, updated by setContentWidth()
51
+ * - `selectionCallback` — current selection modal callback (nullable)
52
+ */
53
+ export interface FeedContextMutableInit {
54
+ prompt: string;
55
+ cursorPos: number;
56
+ commandMode: boolean;
57
+ panelFocused: boolean;
58
+ indicatorFocused: boolean;
59
+ helpOverlayActive: boolean;
60
+ helpScrollOffset: number;
61
+ shortcutsOverlayActive: boolean;
62
+ shortcutsScrollOffset: number;
63
+ nextPasteId: number;
64
+ nextImageId: number;
65
+ mouseDownRow: number;
66
+ mouseDownCol: number;
67
+ contentWidth: number;
68
+ selectionCallback: ((result: SelectionResult | null) => void) | null;
69
+ }
70
+
71
+ /**
72
+ * Stable (readonly) service references for InputFeedContext.
73
+ *
74
+ * **Stable references** (set once at construction, never reallocated):
75
+ * - `pasteRegistry`, `imageRegistry` — owned Maps, never replaced
76
+ * - `selectionModal`, `bookmarkModal`, `settingsModal`, `sessionPickerModal`,
77
+ * `profilePickerModal` — modal objects constructed once
78
+ * - `filePicker`, `modelPicker`, `processModal`, `liveTailModal`,
79
+ * `agentDetailModal`, `contextInspectorModal`, `blockActionsMenu`,
80
+ * `searchManager`, `historySearch` — service objects constructed once
81
+ * - `panelManager`, `keybindingsManager` — from uiServices, stable
82
+ * - `modalStack` — reference to the handler's shared array
83
+ * - `getHistory`, `getViewportHeight`, `getScrollTop`, `scroll`, `exitApp` — callbacks
84
+ * - `commandRegistry`, `commandContext`, `autocomplete`, `inputHistory`,
85
+ * `conversationManager` — wired after construction; synced at feed() entry only
86
+ * (not per-action) since no in-feed action changes them
87
+ *
88
+ * **Rationale:** per-feed mutation on a single object avoids per-keystroke GC pressure
89
+ * from ~80-field object allocation. Stable references are service handles that never
90
+ * need to change after construction.
91
+ */
92
+ export interface FeedContextStableRefs {
93
+ selection: SelectionManager;
94
+ pasteRegistry: Map<string, string>;
95
+ imageRegistry: Map<string, { data: string; mediaType: string }>;
96
+ selectionModal: SelectionModal;
97
+ bookmarkModal: BookmarkModal;
98
+ settingsModal: SettingsModal;
99
+ sessionPickerModal: SessionPickerModal;
100
+ profilePickerModal: ProfilePickerModal;
101
+ historySearch: HistorySearch;
102
+ commandRegistry: CommandRegistry | null;
103
+ commandContext: CommandContext | undefined;
104
+ autocomplete: AutocompleteEngine | null;
105
+ filePicker: FilePickerModal;
106
+ modelPicker: ModelPickerModal;
107
+ processModal: ProcessModal;
108
+ liveTailModal: LiveTailModal;
109
+ agentDetailModal: AgentDetailModal;
110
+ contextInspectorModal: ContextInspectorModal;
111
+ blockActionsMenu: BlockActionsMenu;
112
+ searchManager: SearchManager;
113
+ modalStack: string[];
114
+ inputHistory: InputHistory | null;
115
+ conversationManager: ConversationManager | null;
116
+ panelManager: PanelManager;
117
+ keybindingsManager: KeybindingsManager;
118
+ getHistory: () => InfiniteBuffer;
119
+ getViewportHeight: () => number;
120
+ getScrollTop: () => number;
121
+ scroll: (delta: number) => void;
122
+ exitApp: () => void;
123
+ }
124
+
125
+ /** Bound method closures for InputFeedContext. Built in handler.ts where private members are accessible. */
126
+ export interface FeedContextClosures {
127
+ modalOpened: (name: string) => void;
128
+ handleEscape: () => void;
129
+ handleCopy: () => void;
130
+ handleCtrlC: () => void;
131
+ handleBlockCopy: () => void;
132
+ handleBookmark: () => void;
133
+ handleBlockSave: () => void;
134
+ handleDiffApply: () => boolean;
135
+ handleUndo: () => void;
136
+ handleRedo: () => void;
137
+ handlePaste: () => void;
138
+ saveUndoState: () => void;
139
+ ensureInputCursorVisible: (contentWidth?: number) => void;
140
+ registerPaste: (content: string) => string;
141
+ executeBlockAction: (id: string) => void;
142
+ cyclePanelTab: (direction: 'next' | 'prev') => void;
143
+ onPanelInputConsumed: (activePanel: Panel | null, key: string) => void;
144
+ getWrappedPromptInfo: (contentWidth: number) => WrappedPromptInfo;
145
+ moveCursorVertical: (direction: -1 | 1) => boolean;
146
+ handlePathCompletion: () => boolean;
147
+ handleBlockToggle: () => void;
148
+ findMarkerAtPos: (pos: number) => { start: number; end: number } | null;
149
+ cleanupMarkerRegistry: (text: string) => void;
150
+ expandPrompt: (text: string) => string | import('@pellux/goodvibes-sdk/platform/providers/interface').ContentPart[];
151
+ }
152
+
153
+ /**
154
+ * buildInitialFeedContext — Allocate the single InputFeedContext that lives for the
155
+ * lifetime of the InputHandler.
156
+ *
157
+ * Accepts pre-built field groups from handler.ts where private access is available.
158
+ * The resulting context object is mutated in place on every feed() call via
159
+ * syncFeedContextMutableFields — no re-allocation occurs per keystroke.
160
+ *
161
+ * @param mutable Initial mutable scalar values (synced per-feed).
162
+ * @param stable Stable service references (set once, never replaced).
163
+ * @param closures Pre-built bound method closures (built in handler.ts).
164
+ */
165
+ export function buildInitialFeedContext(
166
+ mutable: FeedContextMutableInit,
167
+ stable: FeedContextStableRefs,
168
+ closures: FeedContextClosures,
169
+ ): InputFeedContext {
170
+ const noop = (): void => {};
171
+ return {
172
+ // --- mutable scalars ---
173
+ ...mutable,
174
+ // --- requestRender: placeholder, swapped per-feed to buffered version ---
175
+ requestRender: noop,
176
+ // --- stable refs ---
177
+ ...stable,
178
+ // --- closures ---
179
+ ...closures,
180
+ };
181
+ }
182
+
183
+ /**
184
+ * syncFeedContextMutableFields — Copy mutable scalar fields into the reused
185
+ * InputFeedContext object.
186
+ *
187
+ * **Fields synced (updated on every call):**
188
+ * - `prompt` — current prompt text buffer
189
+ * - `cursorPos` — caret position within prompt
190
+ * - `commandMode` — whether command-mode prefix is active
191
+ * - `panelFocused` — whether the active panel owns keyboard focus
192
+ * - `indicatorFocused` — whether the status indicator owns focus
193
+ * - `helpOverlayActive` / `helpScrollOffset` — help overlay visibility and scroll
194
+ * - `shortcutsOverlayActive` / `shortcutsScrollOffset` — shortcuts overlay state
195
+ * - `selectionCallback` — the current in-flight selection modal callback
196
+ * - `nextPasteId` / `nextImageId` — monotonically increasing allocation counters
197
+ * - `mouseDownRow` / `mouseDownCol` — drag-start coordinates
198
+ *
199
+ * **Fields intentionally excluded (synced only at feed() entry, not here):**
200
+ * - `contentWidth` — semi-stable; only changes on terminal resize via setContentWidth().
201
+ * Synced at feed() entry because it is only relevant for layout, not action-reaction
202
+ * sequences within a single feed.
203
+ * - `commandRegistry`, `commandContext` — wired after construction via
204
+ * setCommandRegistry(); synced at feed() entry since no in-feed action changes them.
205
+ * - `autocomplete` — wired after construction; synced at feed() entry.
206
+ * - `inputHistory`, `conversationManager` — late-wired service handles; stable within
207
+ * a feed. Synced at feed() entry only.
208
+ * - `requestRender` — swapped to a buffered version at feed() entry and restored in
209
+ * the finally block; not managed by this function.
210
+ *
211
+ * Called from within action closures (`handleEscape`, `handleCtrlC`, `handleUndo`,
212
+ * `handleRedo`, `handlePaste`) that mutate handler state mid-feed, so subsequent
213
+ * route handlers see updated values without re-reading handler fields.
214
+ *
215
+ * @param fields Current mutable field values from the handler.
216
+ * @param ctx The InputFeedContext to update in place.
217
+ */
218
+ export function syncFeedContextMutableFields(
219
+ fields: FeedContextMutableInit,
220
+ ctx: InputFeedContext,
221
+ ): void {
222
+ ctx.prompt = fields.prompt;
223
+ ctx.cursorPos = fields.cursorPos;
224
+ ctx.commandMode = fields.commandMode;
225
+ ctx.panelFocused = fields.panelFocused;
226
+ ctx.indicatorFocused = fields.indicatorFocused;
227
+ ctx.helpOverlayActive = fields.helpOverlayActive;
228
+ ctx.helpScrollOffset = fields.helpScrollOffset;
229
+ ctx.shortcutsOverlayActive = fields.shortcutsOverlayActive;
230
+ ctx.shortcutsScrollOffset = fields.shortcutsScrollOffset;
231
+ ctx.selectionCallback = fields.selectionCallback;
232
+ ctx.nextPasteId = fields.nextPasteId;
233
+ ctx.nextImageId = fields.nextImageId;
234
+ ctx.mouseDownRow = fields.mouseDownRow;
235
+ ctx.mouseDownCol = fields.mouseDownCol;
236
+ }
@@ -54,7 +54,17 @@ export function handlePanelFocusToken(state: PanelFocusRouteState, token: InputT
54
54
  }
55
55
 
56
56
  if (token.type === 'key') {
57
+ // I6: two-stage Escape — give the panel a chance to consume escape first
58
+ // (e.g. dismiss a confirm dialog or clear search). Only unfocus if the
59
+ // panel returns false (unconsumed) or there is no active panel.
57
60
  if (token.logicalName === 'escape') {
61
+ const activePanel = state.panelManager.getActive();
62
+ const panelConsumedEscape = activePanel?.handleInput?.('escape') ?? false;
63
+ if (panelConsumedEscape) {
64
+ state.onPanelInputConsumed?.(activePanel!, 'escape');
65
+ state.requestRender();
66
+ return { handled: true, panelFocused };
67
+ }
58
68
  panelFocused = false;
59
69
  state.requestRender();
60
70
  return { handled: true, panelFocused };
@@ -40,6 +40,44 @@ import { SelectionManager } from './selection.ts';
40
40
  import type { PanelManager } from '../panels/panel-manager.ts';
41
41
  import type { KeybindingsManager } from './keybindings.ts';
42
42
 
43
+ /**
44
+ * InputFeedContext — The single long-lived context object passed to feedInputTokens
45
+ * on every keystroke. Allocated once at InputHandler construction; mutated in place
46
+ * per-feed to avoid per-keystroke GC pressure from ~80-field object allocation.
47
+ *
48
+ * **Mutable per-feed** (synced from handler at the top of every feed() call, and
49
+ * updated inside action closures via syncFeedContextMutableFields):
50
+ * - `prompt`, `cursorPos` — current text buffer state
51
+ * - `commandMode`, `panelFocused`, `indicatorFocused` — focus-mode flags
52
+ * - `helpOverlayActive`, `helpScrollOffset` — help overlay visibility and scroll
53
+ * - `shortcutsOverlayActive`, `shortcutsScrollOffset` — shortcuts overlay state
54
+ * - `nextPasteId`, `nextImageId` — monotonically increasing ID counters
55
+ * - `mouseDownRow`, `mouseDownCol` — drag-tracking coordinates
56
+ * - `contentWidth` — reflow width (semi-stable; synced at feed() entry only)
57
+ * - `selectionCallback` — current in-flight selection modal callback (nullable)
58
+ * - `requestRender` — swapped per-feed to a buffered version, restored after
59
+ *
60
+ * **Stable service handles** (set once at construction, never reallocated):
61
+ * - `commandRegistry`, `commandContext` — wired via setCommandRegistry() after
62
+ * construction; synced at feed() entry (not per-action) since no action changes them
63
+ * - `autocomplete` — wired after construction; synced at feed() entry
64
+ * - `inputHistory`, `conversationManager` — late-wired service handles; synced at
65
+ * feed() entry only since no in-feed action rewires them
66
+ * - `pasteRegistry`, `imageRegistry` — owned Maps, never replaced
67
+ * - `selectionModal`, `bookmarkModal`, `settingsModal`, `sessionPickerModal`,
68
+ * `profilePickerModal` — modal objects constructed once in InputHandler constructor
69
+ * - `filePicker`, `modelPicker`, `processModal`, `liveTailModal`, `agentDetailModal`,
70
+ * `contextInspectorModal`, `blockActionsMenu`, `searchManager`, `historySearch` —
71
+ * service objects constructed once
72
+ * - `panelManager`, `keybindingsManager` — from uiServices, stable for app lifetime
73
+ * - `modalStack` — reference to the handler's shared array (mutated in place)
74
+ * - `getHistory`, `getViewportHeight`, `getScrollTop`, `scroll`, `exitApp` — stable
75
+ * callbacks bound in the InputHandler constructor
76
+ * - All method closures (`modalOpened`, `handleEscape`, etc.) — bound once at init
77
+ *
78
+ * **Rationale:** per-feed mutation avoids per-keystroke allocation cost; stable
79
+ * references are service handles whose identity never changes after construction.
80
+ */
43
81
  export interface InputFeedContext {
44
82
  prompt: string;
45
83
  cursorPos: number;
@@ -65,9 +103,9 @@ export interface InputFeedContext {
65
103
  readonly sessionPickerModal: SessionPickerModal;
66
104
  readonly profilePickerModal: ProfilePickerModal;
67
105
  readonly historySearch: HistorySearch;
68
- readonly commandRegistry: CommandRegistry | null;
69
- readonly commandContext: CommandContext | undefined;
70
- readonly autocomplete: AutocompleteEngine | null;
106
+ commandRegistry: CommandRegistry | null;
107
+ commandContext: CommandContext | undefined;
108
+ autocomplete: AutocompleteEngine | null;
71
109
  readonly filePicker: FilePickerModal;
72
110
  readonly modelPicker: ModelPickerModal;
73
111
  readonly processModal: ProcessModal;
@@ -79,13 +117,13 @@ export interface InputFeedContext {
79
117
  readonly panelManager: PanelManager;
80
118
  readonly keybindingsManager: KeybindingsManager;
81
119
  readonly modalStack: string[];
82
- readonly inputHistory: InputHistory | null;
83
- readonly conversationManager: ConversationManager | null;
120
+ inputHistory: InputHistory | null;
121
+ conversationManager: ConversationManager | null;
84
122
  readonly getHistory: () => InfiniteBuffer;
85
123
  readonly getViewportHeight: () => number;
86
124
  readonly getScrollTop: () => number;
87
125
  readonly scroll: (delta: number) => void;
88
- readonly requestRender: () => void;
126
+ requestRender: () => void;
89
127
  readonly modalOpened: (name: string) => void;
90
128
  readonly handleEscape: () => void;
91
129
  readonly handleCopy: () => void;