Skip to content

Commit 1d40cc0

Browse files
committed
perf(renderer): index task parts by child session
Avoid scanning every message part when child session updates arrive. Track the owning task part in render state so task tool rerenders stay constant time.
1 parent fe8e6a7 commit 1d40cc0

3 files changed

Lines changed: 135 additions & 15 deletions

File tree

lua/opencode/ui/render_state.lua

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,52 @@ function RenderState:reset()
3939
line_to_message = {},
4040
}
4141
self._child_session_parts = {}
42+
self._child_session_task_parts = {}
43+
self._task_part_child_sessions = {}
4244
self._line_index_valid = false
4345
end
4446

47+
---@param part OpencodeMessagePart?
48+
---@return string?
49+
local function get_child_session_id_for_task_part(part)
50+
if not part or part.tool ~= 'task' then
51+
return nil
52+
end
53+
54+
local part_state = part.state
55+
local metadata = part_state and part_state.metadata
56+
57+
return metadata and metadata.sessionId or nil
58+
end
59+
60+
---@param part_id string
61+
function RenderState:_clear_task_part_child_session(part_id)
62+
local child_session_id = self._task_part_child_sessions[part_id]
63+
if not child_session_id then
64+
return
65+
end
66+
67+
if self._child_session_task_parts[child_session_id] == part_id then
68+
self._child_session_task_parts[child_session_id] = nil
69+
end
70+
71+
self._task_part_child_sessions[part_id] = nil
72+
end
73+
74+
---@param part_id string
75+
---@param part OpencodeMessagePart
76+
function RenderState:_index_task_part_child_session(part_id, part)
77+
self:_clear_task_part_child_session(part_id)
78+
79+
local child_session_id = get_child_session_id_for_task_part(part)
80+
if not child_session_id then
81+
return
82+
end
83+
84+
self._child_session_task_parts[child_session_id] = part_id
85+
self._task_part_child_sessions[part_id] = child_session_id
86+
end
87+
4588
---Get parts for a child session
4689
---@param session_id string
4790
---@return OpencodeMessagePart[]?|nil
@@ -52,6 +95,17 @@ function RenderState:get_child_session_parts(session_id)
5295
return self._child_session_parts and self._child_session_parts[session_id]
5396
end
5497

98+
---Get the owning task part for a child session
99+
---@param session_id string
100+
---@return string?
101+
function RenderState:get_task_part_by_child_session(session_id)
102+
if not session_id then
103+
return nil
104+
end
105+
106+
return self._child_session_task_parts and self._child_session_task_parts[session_id]
107+
end
108+
55109
---Upsert a part associated with a child session
56110
---@param session_id string
57111
---@param part OpencodeMessagePart
@@ -246,6 +300,8 @@ function RenderState:set_part(part, line_start, line_end)
246300
if line_start and line_end then
247301
self._line_index_valid = false
248302
end
303+
304+
self:_index_task_part_child_session(part_id, part)
249305
end
250306

251307
---Update part line positions and shift subsequent content
@@ -290,6 +346,7 @@ function RenderState:update_part_data(part_ref)
290346
end
291347

292348
rendered_part.part = part_ref
349+
self:_index_task_part_child_session(part_ref.id, part_ref)
293350
return rendered_part
294351
end
295352

@@ -356,10 +413,17 @@ end
356413
---@return boolean success
357414
function RenderState:remove_part(part_id)
358415
local part_data = self._parts[part_id]
359-
if not part_data or not part_data.line_start or not part_data.line_end then
416+
if not part_data then
360417
return false
361418
end
362419

420+
self:_clear_task_part_child_session(part_id)
421+
422+
if not part_data.line_start or not part_data.line_end then
423+
self._parts[part_id] = nil
424+
return true
425+
end
426+
363427
local line_count = part_data.line_end - part_data.line_start + 1
364428
local shift_from = part_data.line_end + 1
365429

lua/opencode/ui/renderer.lua

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1136,23 +1136,12 @@ end
11361136
---Find and re-render the task tool part in the active session that owns a given child session
11371137
---@param child_session_id string The child session ID to look up
11381138
function M._rerender_task_tool_for_child_session(child_session_id)
1139-
if not state.messages then
1139+
local part_id = M._render_state:get_task_part_by_child_session(child_session_id)
1140+
if not part_id then
11401141
return
11411142
end
11421143

1143-
for _, message in ipairs(state.messages) do
1144-
for _, part in ipairs(message.parts or {}) do
1145-
if
1146-
part.tool == 'task'
1147-
and part.state
1148-
and part.state.metadata
1149-
and part.state.metadata.sessionId == child_session_id
1150-
then
1151-
M._rerender_part(part.id)
1152-
return
1153-
end
1154-
end
1155-
end
1144+
M._rerender_part(part_id)
11561145
end
11571146

11581147
---Re-render existing part with current state

tests/unit/render_state_spec.lua

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,23 @@ describe('RenderState', function()
103103
assert.is_table(result.actions)
104104
assert.equals(0, #result.actions)
105105
end)
106+
107+
it('indexes task parts by child session ID', function()
108+
local part = {
109+
id = 'part1',
110+
messageID = 'msg1',
111+
tool = 'task',
112+
state = {
113+
metadata = {
114+
sessionId = 'child-1',
115+
},
116+
},
117+
}
118+
119+
render_state:set_part(part, 1, 2)
120+
121+
assert.equals('part1', render_state:get_task_part_by_child_session('child-1'))
122+
end)
106123
end)
107124

108125
describe('get_part_at_line', function()
@@ -332,6 +349,25 @@ describe('RenderState', function()
332349
local success = render_state:remove_part('nonexistent')
333350
assert.is_false(success)
334351
end)
352+
353+
it('clears child session index when removing unrendered task parts', function()
354+
local part = {
355+
id = 'part1',
356+
messageID = 'msg1',
357+
tool = 'task',
358+
state = {
359+
metadata = {
360+
sessionId = 'child-1',
361+
},
362+
},
363+
}
364+
365+
render_state:set_part(part)
366+
local success = render_state:remove_part('part1')
367+
368+
assert.is_true(success)
369+
assert.is_nil(render_state:get_task_part_by_child_session('child-1'))
370+
end)
335371
end)
336372

337373
describe('remove_message', function()
@@ -487,5 +523,36 @@ describe('RenderState', function()
487523
it('does nothing for non-existent part', function()
488524
render_state:update_part_data({ id = 'nonexistent' })
489525
end)
526+
527+
it('updates child session index when task metadata changes', function()
528+
local original = {
529+
id = 'part1',
530+
content = 'original',
531+
messageID = 'msg1',
532+
tool = 'task',
533+
state = {
534+
metadata = {
535+
sessionId = 'child-1',
536+
},
537+
},
538+
}
539+
local updated = {
540+
id = 'part1',
541+
content = 'updated',
542+
messageID = 'msg1',
543+
tool = 'task',
544+
state = {
545+
metadata = {
546+
sessionId = 'child-2',
547+
},
548+
},
549+
}
550+
551+
render_state:set_part(original, 10, 15)
552+
render_state:update_part_data(updated)
553+
554+
assert.is_nil(render_state:get_task_part_by_child_session('child-1'))
555+
assert.equals('part1', render_state:get_task_part_by_child_session('child-2'))
556+
end)
490557
end)
491558
end)

0 commit comments

Comments
 (0)