@@ -17,9 +17,76 @@ M.separator = {
1717 ' ' ,
1818}
1919
20+ local function default_win_width ()
21+ local output_win = state .windows and state .windows .output_win
22+ if output_win and vim .api .nvim_win_is_valid (output_win ) then
23+ return vim .api .nvim_win_get_width (output_win )
24+ end
25+
26+ if type (config .ui .window_width ) == ' number' and config .ui .window_width > 1 then
27+ return config .ui .window_width
28+ end
29+
30+ return 80
31+ end
32+
33+ local function resolve_render_ctx (render_ctx )
34+ render_ctx = render_ctx or {}
35+
36+ local current_messages = render_ctx .messages
37+ if current_messages == nil then
38+ current_messages = state .messages or {}
39+ end
40+
41+ local permission_renderer = render_ctx .permission_renderer
42+ if permission_renderer == nil then
43+ permission_renderer = function (output )
44+ permission_window .format_display (output )
45+ end
46+ end
47+
48+ local question_renderer = render_ctx .question_renderer
49+ if question_renderer == nil then
50+ question_renderer = function (output )
51+ require (' opencode.ui.question_window' ).format_display (output )
52+ end
53+ end
54+
55+ local show_reasoning_output = config .ui .output .tools .show_reasoning_output
56+ if render_ctx .show_reasoning_output ~= nil then
57+ show_reasoning_output = render_ctx .show_reasoning_output
58+ end
59+
60+ local show_debug_ids = config .debug .show_ids
61+ if render_ctx .show_debug_ids ~= nil then
62+ show_debug_ids = render_ctx .show_debug_ids
63+ end
64+
65+ local revert_info = state .active_session and state .active_session .revert or nil
66+ if render_ctx .revert_info ~= nil then
67+ revert_info = render_ctx .revert_info
68+ end
69+
70+ return {
71+ messages = current_messages ,
72+ current_mode = render_ctx .current_mode ~= nil and render_ctx .current_mode or state .current_mode ,
73+ show_reasoning_output = show_reasoning_output ,
74+ show_debug_ids = show_debug_ids ,
75+ win_width = render_ctx .win_width or default_win_width (),
76+ get_child_parts = render_ctx .get_child_parts ,
77+ child_session_parts = render_ctx .child_session_parts ,
78+ permission_renderer = permission_renderer ,
79+ question_renderer = question_renderer ,
80+ revert_info = revert_info ,
81+ is_last_part = render_ctx .is_last_part == true ,
82+ }
83+ end
84+
2085--- @param output Output
2186--- @param part OpencodeMessagePart
22- function M ._format_reasoning (output , part )
87+ --- @param render_ctx ? table
88+ function M ._format_reasoning (output , part , render_ctx )
89+ render_ctx = resolve_render_ctx (render_ctx )
2390 local text = vim .trim (part .text or ' ' )
2491
2592 local start_line = output :get_line_count () + 1
@@ -35,7 +102,7 @@ function M._format_reasoning(output, part)
35102
36103 format_utils .format_action (output , icons .get (' reasoning' ), title , ' ' )
37104
38- if config . ui . output . tools .show_reasoning_output and text ~= ' ' then
105+ if render_ctx .show_reasoning_output and text ~= ' ' then
39106 output :add_empty_line ()
40107 output :add_lines (vim .split (text , ' \n ' ))
41108 output :add_empty_line ()
54121--- Format the revert callout with statistics
55122--- @param session_data OpencodeMessage[] All messages in the session
56123--- @param start_idx number Index of the message where revert occurred
124+ --- @param render_ctx ? table
57125--- @return Output output object representing the lines , extmarks , and actions
58- function M ._format_revert_message (session_data , start_idx )
126+ function M .render_revert_message (session_data , start_idx , render_ctx )
127+ render_ctx = resolve_render_ctx (render_ctx )
59128 local output = Output .new ()
60- local stats = format_utils .calculate_revert_stats (session_data , start_idx , state . active_session . revert )
129+ local stats = format_utils .calculate_revert_stats (session_data , start_idx , render_ctx . revert_info )
61130 local message_text = stats .messages == 1 and ' message' or ' messages'
62131 local tool_text = stats .tool_calls == 1 and ' tool call' or ' tool calls'
63132
@@ -98,6 +167,8 @@ function M._format_revert_message(session_data, start_idx)
98167 return output
99168end
100169
170+ M ._format_revert_message = M .render_revert_message
171+
101172local function add_action (output , text , action_type , args , key , line )
102173 -- actions use api-indexing (e.g. 0 indexed)
103174 line = (line or output :get_line_count ()) - 1
@@ -150,8 +221,10 @@ function M._format_error(output, message)
150221end
151222
152223--- @param message OpencodeMessage
224+ --- @param render_ctx ? table
153225--- @return Output
154- function M .format_message_header (message )
226+ function M .render_message_header (message , render_ctx )
227+ render_ctx = resolve_render_ctx (render_ctx )
155228 local output = Output .new ()
156229
157230 output :add_lines (M .separator )
@@ -162,19 +235,16 @@ function M.format_message_header(message)
162235 local role_hl = ' OpencodeMessageRole' .. role :sub (1 , 1 ):upper () .. role :sub (2 )
163236 local model_text = message .info .modelID and ' ' .. message .info .modelID or ' '
164237
165- local debug_text = config . debug . show_ids and ' [' .. message .info .id .. ' ]' or ' '
238+ local debug_text = render_ctx . show_debug_ids and ' [' .. message .info .id .. ' ]' or ' '
166239
167240 local display_name
168241 if role == ' assistant' then
169242 local mode = message .info .mode
170243 if mode and mode ~= ' ' then
171244 display_name = mode :upper ()
172245 else
173- -- For the most recent assistant message, show current_mode if mode is missing
174- -- This handles new messages that haven't been stamped yet
175- local is_last_message = # state .messages == 0 or message .info .id == state .messages [# state .messages ].info .id
176- if is_last_message and state .current_mode and state .current_mode ~= ' ' then
177- display_name = state .current_mode :upper ()
246+ if render_ctx .current_mode and render_ctx .current_mode ~= ' ' then
247+ display_name = render_ctx .current_mode :upper ()
178248 else
179249 display_name = ' ASSISTANT'
180250 end
@@ -222,16 +292,21 @@ function M.format_message_header(message)
222292 return output
223293end
224294
295+ --- @param message OpencodeMessage
296+ --- @return Output
297+ function M .format_message_header (message )
298+ return M .render_message_header (message )
299+ end
300+
225301--- @param output Output Output object to write to
226302--- @param callout string Callout type (e.g. , ' ERROR' , ' TODO' )
227303--- @param text string Callout text content
228304--- @param title ? string Optional title for the callout
229- function M ._format_callout (output , callout , text , title )
305+ --- @param render_ctx ? table
306+ function M ._format_callout (output , callout , text , title , render_ctx )
307+ render_ctx = resolve_render_ctx (render_ctx )
230308 title = title and title .. ' ' or ' '
231- local win_width = (state .windows and state .windows .output_win and vim .api .nvim_win_is_valid (state .windows .output_win ))
232- and vim .api .nvim_win_get_width (state .windows .output_win )
233- or config .ui .window_width
234- or 80
309+ local win_width = render_ctx .win_width
235310 if # text > win_width - 4 then
236311 local ok , substituted = pcall (vim .fn .substitute , text , ' \v (.{' .. (win_width - 8 ) .. ' })' , ' \1 \n ' , ' g' )
237312 text = ok and substituted or text
@@ -259,17 +334,12 @@ function M._format_user_prompt(output, text, message)
259334
260335 local end_line = output :get_line_count ()
261336
262- local end_line_extmark_offset = 0
263-
264337 local mentions = {}
265338 if message and message .parts then
266339 -- message.parts will only be filled out on a re-render
267340 -- we need to collect the mentions here
268341 for _ , part in ipairs (message .parts ) do
269342 if part .type == ' file' then
270- -- we're rerendering this part and we have files, the space after the user prompt
271- -- also needs an extmark
272- end_line_extmark_offset = 1
273343 if part .source and part .source .text then
274344 table.insert (mentions , part .source .text )
275345 end
@@ -285,6 +355,25 @@ function M._format_user_prompt(output, text, message)
285355 mention .highlight_mentions_in_output (output , text , mentions , start_line )
286356 end
287357
358+ local end_line_extmark_offset = 0
359+ if message and message .parts then
360+ local has_file_parts = false
361+ local last_content_idx = 0
362+ local text_idx = 0
363+ for i , part in ipairs (message .parts ) do
364+ if part .type == ' file' then
365+ has_file_parts = true
366+ last_content_idx = i
367+ elseif part .type == ' text' and not part .synthetic then
368+ text_idx = i
369+ last_content_idx = i
370+ end
371+ end
372+ if has_file_parts and text_idx > 0 and text_idx == last_content_idx then
373+ end_line_extmark_offset = 1
374+ end
375+ end
376+
288377 M .add_vertical_border (output , start_line , end_line + end_line_extmark_offset , ' OpencodeMessageRoleUser' , - 3 )
289378end
290379
@@ -470,10 +559,10 @@ end
470559--- Formats a single message part and returns the resulting output object
471560--- @param part OpencodeMessagePart The part to format
472561--- @param message ? OpencodeMessage Optional message object to extract role and mentions from
473- --- @param is_last_part ? boolean Whether this is the last part in the message , used to show an error if there is one
474- --- @param get_child_parts ? fun ( session_id : string ): OpencodeMessagePart[] ?
562+ --- @param render_ctx ? { is_last_part ?: boolean , get_child_parts ?: fun ( session_id : string ): OpencodeMessagePart[] ?, child_session_parts ?: OpencodeMessagePart[] ?, show_reasoning_output ?: boolean , permission_renderer ?: fun ( output : Output ), question_renderer ?: fun ( output : Output ), win_width ?: integer , messages ?: OpencodeMessage[] , current_mode ?: string , show_debug_ids ?: boolean }
475563--- @return Output
476- function M .format_part (part , message , is_last_part , get_child_parts )
564+ function M .render_part (part , message , render_ctx )
565+ render_ctx = resolve_render_ctx (render_ctx )
477566 local output = Output .new ()
478567
479568 if not message or not message .info or not message .info .role then
@@ -505,9 +594,24 @@ function M.format_part(part, message, is_last_part, get_child_parts)
505594 M ._format_assistant_message (output , vim .trim (part .text ), part .messageID )
506595 content_added = true
507596 elseif part .type == ' reasoning' then
508- M ._format_reasoning (output , part )
597+ M ._format_reasoning (output , part , render_ctx )
509598 content_added = true
510599 elseif part .type == ' tool' then
600+ local get_child_parts = render_ctx .get_child_parts
601+ if render_ctx .child_session_parts ~= nil then
602+ local child_session_parts = render_ctx .child_session_parts
603+ local child_session_id = part .state and part .state .metadata and part .state .metadata .sessionId
604+ get_child_parts = function (session_id )
605+ if child_session_id and session_id == child_session_id then
606+ return child_session_parts
607+ end
608+ if type (render_ctx .get_child_parts ) == ' function' then
609+ return render_ctx .get_child_parts (session_id )
610+ end
611+ return nil
612+ end
613+ end
614+
511615 M .format_tool (output , part , get_child_parts )
512616 content_added = true
513617 elseif part .type == ' patch' and part .hash then
@@ -516,27 +620,39 @@ function M.format_part(part, message, is_last_part, get_child_parts)
516620 end
517621 elseif role == ' system' then
518622 if part .type == ' permissions-display' then
519- permission_window . format_display (output )
520- content_added = true
623+ render_ctx . permission_renderer (output )
624+ content_added = output : get_line_count () > 0
521625 elseif part .type == ' questions-display' then
522- local question_window = require (' opencode.ui.question_window' )
523- question_window .format_display (output )
524- content_added = true
626+ render_ctx .question_renderer (output )
627+ content_added = output :get_line_count () > 0
525628 end
526629 end
527630
528631 if content_added then
529632 output :add_empty_line ()
530633 end
531634
532- if is_last_part and role == ' assistant' and message .info .error and message .info .error ~= ' ' then
635+ if render_ctx . is_last_part and role == ' assistant' and message .info .error and message .info .error ~= ' ' then
533636 local error = message .info .error
534637 local error_message = error .data and error .data .message or vim .inspect (error )
535- M ._format_callout (output , ' ERROR' , error_message )
638+ M ._format_callout (output , ' ERROR' , error_message , nil , render_ctx )
536639 output :add_empty_line ()
537640 end
538641
539642 return output
540643end
541644
645+ --- Formats a single message part and returns the resulting output object
646+ --- @param part OpencodeMessagePart The part to format
647+ --- @param message ? OpencodeMessage Optional message object to extract role and mentions from
648+ --- @param is_last_part ? boolean Whether this is the last part in the message , used to show an error if there is one
649+ --- @param get_child_parts ? fun ( session_id : string ): OpencodeMessagePart[] ?
650+ --- @return Output
651+ function M .format_part (part , message , is_last_part , get_child_parts )
652+ return M .render_part (part , message , {
653+ is_last_part = is_last_part ,
654+ get_child_parts = get_child_parts ,
655+ })
656+ end
657+
542658return M
0 commit comments