Skip to content

Commit 626130f

Browse files
committed
wip: kind of working
1 parent 475c93f commit 626130f

File tree

22 files changed

+1047
-110
lines changed

22 files changed

+1047
-110
lines changed

docs/virtualized-renderer-plan.md

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
Virtualized Renderer Migration Plan
2+
=================================
3+
4+
Goal
5+
----
6+
7+
- Move from event-driven buffer mutations to a component-style renderer that produces reusable "RenderBlock" objects (message/part/overlay), then apply a visible slice to the output buffer via a backend.
8+
- Keep `state.messages` as the single source of truth.
9+
- Add virtualization by message first, then optionally by viewport.
10+
11+
High-level Direction
12+
--------------------
13+
14+
- Keep data model (`state.messages`) and formatter functions.
15+
- Build components that render messages/parts into independent payloads (`Output` / `RenderBlock`).
16+
- Add a cache for rendered blocks keyed by message/part identity and content fingerprint.
17+
- Select a visible slice of blocks (message-based virtualization) and apply them to the buffer with a backend applier that can later be upgraded to a patcher.
18+
19+
Component Model (concept)
20+
-------------------------
21+
22+
- RenderBlock example:
23+
24+
```
25+
{
26+
key = "message:msg_123",
27+
kind = "message",
28+
message_id = "msg_123",
29+
line_count = 12,
30+
output = Output.new(), -- lines/extmarks/actions
31+
}
32+
```
33+
34+
Phase-by-phase Plan
35+
-------------------
36+
37+
Phase 1 — Pure Render Functions
38+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
39+
40+
- Files to change / add:
41+
- `lua/opencode/ui/formatter.lua`
42+
- Split/ensure functions are pure and return `Output` objects.
43+
- Provide `render_message_header(message) -> Output` and `render_part(part, message, ctx) -> Output`.
44+
- `lua/opencode/ui/output.lua` — keep as transport object for lines/extmarks/actions.
45+
- Add `lua/opencode/ui/renderer/components/message.lua` (new)
46+
- `render(message, opts) -> RenderBlock` combines header + parts into a block.
47+
- Add `lua/opencode/ui/renderer/components/part.lua` (new)
48+
- Thin wrapper around part formatting and cache keys.
49+
- Add `lua/opencode/ui/renderer/components/overlays.lua` (new)
50+
- Renders revert/question/permission/hidden-history blocks.
51+
52+
Phase 2 — Block Cache and Metadata
53+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
54+
55+
- Files to change / add:
56+
- `lua/opencode/ui/render_state.lua`
57+
- Extend to track blocks: `set_block`, `get_block`, `set_message_block`, etc.
58+
- Preserve existing line-based APIs during migration.
59+
- Add `lua/opencode/ui/renderer/cache.lua` (new)
60+
- Cache rendered blocks by `message.id` and `part.id`.
61+
- Provide `get_message_block(message)`, `set_message_block(message, block)`, `invalidate_message(message_id)`, `invalidate_part(part_id)`.
62+
63+
Phase 3 — Session View + Virtualization Selector
64+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
65+
66+
- Files to add:
67+
- `lua/opencode/ui/renderer/session_view.lua` (new)
68+
- Build ordered list of blocks from `state.messages` and inject synthetic blocks (hidden notice, revert banner).
69+
- API: `build_blocks(messages, opts) -> RenderBlock[]`.
70+
- `lua/opencode/ui/renderer/virtualize.lua` (new)
71+
- Select visible blocks given budgets: `select_visible_blocks(blocks, { max_lines, max_messages }) -> VisibleRender`.
72+
73+
- Adjust `lua/opencode/ui/renderer.lua` (coordinator):
74+
- Full-session render: fetch -> build_blocks -> select_visible_blocks -> backend.apply
75+
- Keep event-by-event path until migration stabilizes.
76+
77+
Phase 4 — Backend & Buffer Application
78+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
79+
80+
- Files to change / add:
81+
- `lua/opencode/ui/renderer/buffer.lua`
82+
- Convert to backend applier that flattens visible blocks into a single `Output` and applies it.
83+
- Add `lua/opencode/ui/renderer/backend.lua` (new)
84+
- Start with `apply_full(visible_render)` (full replace) and later add `apply_patch(prev_visible, next_visible)`.
85+
- `lua/opencode/ui/output_window.lua` unchanged role but add helpers for chunk/range updates.
86+
87+
Phase 5 — Event Handling & Scheduler
88+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
89+
90+
- Files to change / add:
91+
- `lua/opencode/ui/renderer/events.lua`
92+
- Stop direct buffer edits; instead mutate message state, mark blocks dirty, and schedule a render pass.
93+
- Add `lua/opencode/ui/renderer/scheduler.lua` (new)
94+
- Batch event bursts and choose incremental vs full visible recompose.
95+
- `lua/opencode/ui/renderer/ctx.lua` becomes render-pass transient state (prev visible keys, guards).
96+
97+
Phase 6 — Viewport Virtualization & Measurement
98+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
99+
100+
- Files to add:
101+
- `lua/opencode/ui/renderer/measure.lua` (new)
102+
- Track message/block heights (measured or estimated).
103+
- `lua/opencode/ui/renderer/viewport.lua` (new)
104+
- Compute which blocks to mount around scroll position. API: `get_visible_range(blocks, scroll_top, win_height, overscan)`.
105+
- Extend `virtualize.lua` to use viewport computations.
106+
107+
File-by-File Summary
108+
--------------------
109+
110+
- Modify:
111+
- `lua/opencode/ui/formatter.lua` — make pure builders for message/part
112+
- `lua/opencode/ui/render_state.lua` — add block metadata API
113+
- `lua/opencode/ui/renderer.lua` — coordinator, route full/session rendering through session_view/virtualize/backend
114+
- `lua/opencode/ui/renderer/buffer.lua` — convert to applier/patch utilities
115+
- `lua/opencode/ui/renderer/events.lua` — mutate state + invalidate + schedule
116+
- `lua/opencode/ui/renderer/ctx.lua` — render-pass context only
117+
118+
- Add:
119+
- `lua/opencode/ui/renderer/components/message.lua`
120+
- `lua/opencode/ui/renderer/components/part.lua`
121+
- `lua/opencode/ui/renderer/components/overlays.lua`
122+
- `lua/opencode/ui/renderer/cache.lua`
123+
- `lua/opencode/ui/renderer/session_view.lua`
124+
- `lua/opencode/ui/renderer/virtualize.lua`
125+
- `lua/opencode/ui/renderer/backend.lua`
126+
- `lua/opencode/ui/renderer/scheduler.lua`
127+
- `lua/opencode/ui/renderer/measure.lua` (later)
128+
- `lua/opencode/ui/renderer/viewport.lua` (later)
129+
130+
Recommended Migration Order
131+
--------------------------
132+
133+
1. Add `RenderBlock` components and cache helpers; don't change existing behavior.
134+
2. Route full-session rendering through `session_view` but keep old event handlers for incremental updates (feature flag by path).
135+
3. Implement message-level virtualization (tail-only) and replace the previous trimming code with the new selector.
136+
4. Replace event handlers to mark blocks dirty + scheduled render passes.
137+
5. Add backend patcher (diff by block key) and minimize buffer updates.
138+
6. Add measurement/viewport virtualization and support arbitrary scroll position (harder step).
139+
140+
Testing Plan
141+
------------
142+
143+
- Unit tests to add:
144+
- `tests/unit/renderer/cache_spec.lua` — cache hit/miss/invalidate
145+
- `tests/unit/renderer/virtualize_spec.lua` — visible block selection
146+
- `tests/unit/renderer/session_view_spec.lua` — block building + synthetic overlays
147+
- Extend `tests/unit/render_state_spec.lua` to cover block indexing/invalidation
148+
149+
- Keep existing `tests/replay/renderer_spec.lua` fixtures and run them during migration to ensure parity. Migrate tests gradually from the old full-session path to the new pipeline.
150+
151+
Practical First Implementation Tasks (low-risk)
152+
-----------------------------------------------
153+
154+
1. Implement `components/message.lua` and `components/part.lua` that call `formatter` and return `RenderBlock`.
155+
2. Implement `renderer/cache.lua` and add simple cache APIs.
156+
3. Implement `renderer/session_view.lua` that composes blocks from `state.messages` and returns ordered blocks including hidden-history/revert overlays.
157+
4. Modify `renderer._render_full_session_data` to call `session_view.build_blocks` and then `virtualize.select_visible_blocks` (initially a simple `select tail N messages` using `max_rendered_lines`), and pass to `buffer.apply_full`.
158+
159+
Notes / Tips
160+
-----------
161+
162+
- Virtualize by message first — it keeps block math manageable and preserves test fixtures.
163+
- Keep extmarks/actions separate from text lines in patch calculations.
164+
- Avoid doing heavy Levenshtein diffs over the whole buffer until you have a stable block-level pipeline; ultimately you can diff blocks or block text to further reduce updates.
165+
- Add a small interactive "load older" action on the hidden-history notice to re-mount older messages on demand.
166+
167+
If you want, I can scaffold the new file stubs and implement the first 3 small files (`components/message.lua`, `components/part.lua`, `renderer/cache.lua`) to get started.

lua/opencode/config.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,10 @@ M.defaults = {
150150
show_reasoning_output = true,
151151
},
152152
always_scroll_to_bottom = false,
153+
viewport = {
154+
enabled = true,
155+
overscan = 8,
156+
},
153157
},
154158
questions = {
155159
use_vim_ui_select = false, -- If true, render questions with vim.ui.select instead of in the output buffer

lua/opencode/ui/dialog.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,11 +260,11 @@ function Dialog:format_dialog(output, config)
260260

261261
local end_line = output:get_line_count()
262262

263+
output:add_line('')
264+
263265
if config.border_hl then
264266
formatter.add_vertical_border(output, start_line + 1, end_line, config.border_hl, -2)
265267
end
266-
267-
output:add_line('')
268268
end
269269

270270
---Format options list with selection indicator

lua/opencode/ui/formatter.lua

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -243,11 +243,7 @@ function M.render_message_header(message, render_ctx)
243243
if mode and mode ~= '' then
244244
display_name = mode:upper()
245245
else
246-
-- For the most recent assistant message, show current_mode if mode is missing
247-
-- This handles new messages that haven't been stamped yet
248-
local messages = render_ctx.messages or {}
249-
local is_last_message = #messages == 0 or message.info.id == messages[#messages].info.id
250-
if is_last_message and render_ctx.current_mode and render_ctx.current_mode ~= '' then
246+
if render_ctx.current_mode and render_ctx.current_mode ~= '' then
251247
display_name = render_ctx.current_mode:upper()
252248
else
253249
display_name = 'ASSISTANT'
@@ -338,17 +334,12 @@ function M._format_user_prompt(output, text, message)
338334

339335
local end_line = output:get_line_count()
340336

341-
local end_line_extmark_offset = 0
342-
343337
local mentions = {}
344338
if message and message.parts then
345339
-- message.parts will only be filled out on a re-render
346340
-- we need to collect the mentions here
347341
for _, part in ipairs(message.parts) do
348342
if part.type == 'file' then
349-
-- we're rerendering this part and we have files, the space after the user prompt
350-
-- also needs an extmark
351-
end_line_extmark_offset = 1
352343
if part.source and part.source.text then
353344
table.insert(mentions, part.source.text)
354345
end
@@ -364,7 +355,7 @@ function M._format_user_prompt(output, text, message)
364355
mention.highlight_mentions_in_output(output, text, mentions, start_line)
365356
end
366357

367-
M.add_vertical_border(output, start_line, end_line + end_line_extmark_offset, 'OpencodeMessageRoleUser', -3)
358+
M.add_vertical_border(output, start_line, end_line, 'OpencodeMessageRoleUser', -3)
368359
end
369360

370361
---@param output Output Output object to write to

lua/opencode/ui/formatter/tools/task.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ function M.format(output, part, get_child_parts)
7979
type = 'select_child_session',
8080
args = {},
8181
key = 'S',
82-
display_line = start_line - 1,
83-
range = { from = start_line, to = end_line },
82+
display_line = start_line,
83+
range = { from = start_line + 1, to = end_line + 1 },
8484
})
8585
end
8686

lua/opencode/ui/output.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ function Output:add_extmark(idx, extmark)
102102
if not self.extmarks[idx] then
103103
self.extmarks[idx] = {}
104104
end
105+
105106
table.insert(self.extmarks[idx], extmark)
106107
end
107108

0 commit comments

Comments
 (0)