@@ -10,6 +10,8 @@ local permission_window = require('opencode.ui.permission_window')
1010
1111local M = {}
1212
13+ --- @note child-session parts are requested from the renderer at format time
14+
1315M .separator = {
1416 ' ----' ,
1517 ' ' ,
@@ -31,7 +33,7 @@ function M._format_reasoning(output, part)
3133 end
3234 end
3335
34- M .format_action (output , icons . get ( ' reasoning' ) .. ' ' .. title , ' ' )
36+ M .format_action (output , ' reasoning' , title , ' ' )
3537
3638 if config .ui .output .tools .show_reasoning_output and text ~= ' ' then
3739 output :add_empty_line ()
@@ -173,7 +175,7 @@ function M._format_patch(output, part)
173175 end
174176
175177 local restore_points = snapshot .get_restore_points_by_parent (part .hash ) or {}
176- M .format_action (output , icons . get ( ' snapshot' ) .. ' Created Snapshot' , vim .trim (part .hash :sub (1 , 8 )))
178+ M .format_action (output , ' snapshot' , ' Created Snapshot' , vim .trim (part .hash :sub (1 , 8 )))
177179
178180 -- Anchor all snapshot-level actions to the snapshot header line
179181 add_action (output , ' [R]evert file' , ' diff_revert_selected_file' , { part .hash }, ' R' )
@@ -465,27 +467,36 @@ function M._format_assistant_message(output, text)
465467 output :add_lines (vim .split (result , ' \n ' ))
466468end
467469
468- --- @param output Output Output object to write to
470+ --- Build the formatted action line string without writing to output
471+ --- @param icon_name string Name of the icon to fetch with ` icons.get`
469472--- @param tool_type string Tool type (e.g. , ' run' , ' read' , ' edit' , etc. )
470473--- @param value string Value associated with the action (e.g. , filename , command )
471474--- @param duration_text ? string
472- function M .format_action (output , tool_type , value , duration_text )
473- if not tool_type or not value then
474- return
475- end
475+ --- @return string
476+ function M ._build_action_line (icon_name , tool_type , value , duration_text )
477+ local icon = icons .get (icon_name )
476478 local detail = value and # value > 0 and (' `' .. value .. ' `' ) or ' '
477479 local duration_suffix = duration_text and (' ' .. duration_text ) or ' '
478- local line = string.format (' **%s** %s%s' , tool_type , detail , duration_suffix )
480+ return string.format (' **%s %s** %s%s' , icon , tool_type , detail , duration_suffix )
481+ end
479482
480- output :add_line (line )
483+ --- @param output Output Output object to write to
484+ --- @param tool_type string Tool type (e.g. , ' run' , ' read' , ' edit' , etc. )
485+ --- @param value string Value associated with the action (e.g. , filename , command )
486+ --- @param duration_text ? string
487+ function M .format_action (output , icon_name , tool_type , value , duration_text )
488+ if not icon_name or not tool_type then
489+ return
490+ end
491+ output :add_line (M ._build_action_line (icon_name , tool_type , value , duration_text ))
481492end
482493
483494--- @param output Output Output object to write to
484495--- @param input BashToolInput data for the tool
485496--- @param metadata BashToolMetadata Metadata for the tool use
486497--- @param duration_text ? string
487498function M ._format_bash_tool (output , input , metadata , duration_text )
488- M .format_action (output , icons . get ( ' run' ) .. ' run' , input and input .description , duration_text )
499+ M .format_action (output , ' run' , ' run' , input and input .description , duration_text )
489500
490501 if not config .ui .output .tools .show_output then
491502 return
@@ -504,22 +515,11 @@ end
504515--- @param metadata FileToolMetadata Metadata for the tool use
505516--- @param duration_text ? string
506517function M ._format_file_tool (output , tool_type , input , metadata , duration_text )
507- local file_name = ' '
508- if input and input .filePath then
509- local cwd = vim .fn .getcwd ()
510- local absolute = vim .fn .fnamemodify (input .filePath , ' :p' )
511-
512- if vim .startswith (absolute , cwd .. ' /' ) then
513- file_name = absolute :sub (# cwd + 2 )
514- else
515- file_name = absolute
516- end
517- end
518+ local file_name = M ._resolve_file_name (input and input .filePath or ' ' )
518519
519520 local file_type = input and util .get_markdown_filetype (input .filePath ) or ' '
520- local tool_action_icons = { read = icons .get (' read' ), edit = icons .get (' edit' ), write = icons .get (' write' ) }
521521
522- M .format_action (output , tool_action_icons [ tool_type ] .. ' ' .. tool_type , file_name , duration_text )
522+ M .format_action (output , tool_type , tool_type , file_name , duration_text )
523523
524524 if not config .ui .output .tools .show_output then
525525 return
537537--- @param duration_text ? string
538538function M ._format_apply_patch_tool (output , metadata , duration_text )
539539 for _ , file in ipairs (metadata .files or {}) do
540- M .format_action (output , icons . get ( ' edit' ) .. ' apply patch' , file .relativePath or file .filePath , duration_text )
540+ M .format_action (output , ' edit' , ' apply patch' , file .relativePath or file .filePath , duration_text )
541541 if config .ui .output .tools .show_output and file .diff then
542542 local file_type = file and util .get_markdown_filetype (file .filePath ) or ' '
543543 M .format_diff (output , file .diff , file_type )
550550--- @param input TodoToolInput
551551--- @param duration_text ? string
552552function M ._format_todo_tool (output , title , input , duration_text )
553- M .format_action (output , icons . get ( ' plan' ) .. ' plan' , (title or ' ' ), duration_text )
553+ M .format_action (output , ' plan' , ' plan' , (title or ' ' ), duration_text )
554554 if not config .ui .output .tools .show_output then
555555 return
556556 end
568568--- @param metadata GlobToolMetadata Metadata for the tool use
569569--- @param duration_text ? string
570570function M ._format_glob_tool (output , input , metadata , duration_text )
571- M .format_action (output , icons . get ( ' search' ) .. ' glob' , input and input .pattern , duration_text )
571+ M .format_action (output , ' search' , ' glob' , input and input .pattern , duration_text )
572572 if not config .ui .output .tools .show_output then
573573 return
574574 end
581581--- @param metadata GrepToolMetadata Metadata for the tool use
582582--- @param duration_text ? string
583583function M ._format_grep_tool (output , input , metadata , duration_text )
584- local grep_str = table.concat (
585- vim .tbl_filter (function (part )
586- return part ~= nil
587- end , { input .path or input .include , input .pattern }),
588- ' ` `'
589- )
590-
591- M .format_action (output , icons .get (' search' ) .. ' grep' , grep_str , duration_text )
584+ local grep_str = M ._resolve_grep_string (input )
585+ M .format_action (output , ' search' , ' grep' , grep_str , duration_text )
592586 if not config .ui .output .tools .show_output then
593587 return
594588 end
602596--- @param input WebFetchToolInput data for the tool
603597--- @param duration_text ? string
604598function M ._format_webfetch_tool (output , input , duration_text )
605- M .format_action (output , icons . get ( ' web' ) .. ' fetch' , input and input .url , duration_text )
599+ M .format_action (output , ' web' , ' fetch' , input and input .url , duration_text )
606600end
607601
608602--- @param output Output Output object to write to
611605--- @param tool_output string
612606--- @param duration_text ? string
613607function M ._format_list_tool (output , input , metadata , tool_output , duration_text )
614- M .format_action (output , icons . get ( ' list' ) .. ' list' , input and input .path or ' ' , duration_text )
608+ M .format_action (output , ' list' , ' list' , input and input .path or ' ' , duration_text )
615609 if not config .ui .output .tools .show_output then
616610 return
617611 end
640634--- @param status string Status of the tool execution
641635--- @param duration_text ? string
642636function M ._format_question_tool (output , input , metadata , status , duration_text )
643- M .format_action (output , icons . get ( ' question' ) .. ' question' , ' ' , duration_text )
637+ M .format_action (output , ' question' , ' question' , ' ' , duration_text )
644638 output :add_empty_line ()
645639 if not config .ui .output .tools .show_output or status ~= ' completed' then
646640 return
@@ -673,9 +667,36 @@ function M._format_question_tool(output, input, metadata, status, duration_text)
673667 end
674668end
675669
670+ function M ._resolve_file_name (file_path )
671+ if not file_path then
672+ return ' '
673+ end
674+ local cwd = vim .fn .getcwd ()
675+ local absolute = vim .fn .fnamemodify (file_path , ' :p' )
676+ if vim .startswith (absolute , cwd .. ' /' ) then
677+ return absolute :sub (# cwd + 2 )
678+ end
679+ return absolute
680+ end
681+
682+ function M ._resolve_grep_string (input )
683+ if not input then
684+ return ' '
685+ end
686+ local path_part = input .path or input .include or ' '
687+ local pattern_part = input .pattern or ' '
688+ return table.concat (
689+ vim .tbl_filter (function (p )
690+ return p ~= nil and p ~= ' '
691+ end , { path_part , pattern_part }),
692+ ' '
693+ )
694+ end
695+
676696--- @param output Output Output object to write to
677697--- @param part OpencodeMessagePart
678- function M ._format_tool (output , part )
698+ --- @param get_child_parts ? fun ( session_id : string ): OpencodeMessagePart[] ?
699+ function M ._format_tool (output , part , get_child_parts )
679700 local tool = part .tool
680701 if not tool or not part .state then
681702 return
@@ -718,7 +739,8 @@ function M._format_tool(output, part)
718739 input --[[ @as TaskToolInput]] ,
719740 metadata --[[ @as TaskToolMetadata]] ,
720741 tool_output ,
721- duration_text
742+ duration_text ,
743+ get_child_parts
722744 )
723745 elseif tool == ' question' then
724746 M ._format_question_tool (
@@ -729,7 +751,7 @@ function M._format_tool(output, part)
729751 duration_text
730752 )
731753 else
732- M .format_action (output , icons . get ( ' tool' ) .. ' tool' , tool , duration_text )
754+ M .format_action (output , ' tool' , ' tool' , tool , duration_text )
733755 end
734756
735757 if part .state .status == ' error' and part .state .error then
@@ -749,12 +771,73 @@ function M._format_tool(output, part)
749771 end
750772end
751773
774+ local tool_summary_handlers = {
775+ bash = function (_ , input )
776+ return ' run' , input .description or ' '
777+ end ,
778+ read = function (_ , input )
779+ return ' read' , M ._resolve_file_name (input .filePath )
780+ end ,
781+ edit = function (_ , input )
782+ return ' edit' , M ._resolve_file_name (input .filePath )
783+ end ,
784+ write = function (_ , input )
785+ return ' write' , M ._resolve_file_name (input .filePath )
786+ end ,
787+ apply_patch = function (_ , metadata )
788+ local file = metadata .files and metadata .files [1 ]
789+ local others_count = metadata .files and # metadata .files - 1 or 0
790+ local suffix = others_count > 0 and string.format (' (+%d more)' , others_count ) or ' '
791+
792+ return ' write' , file and M ._resolve_file_name (file .filePath ) .. suffix or ' '
793+ end ,
794+ todowrite = function (part , _ )
795+ return ' plan' , part .state and part .state .title or ' '
796+ end ,
797+ glob = function (_ , input )
798+ return ' search' , input .pattern or ' '
799+ end ,
800+ webfetch = function (_ , input )
801+ return ' web' , input .url or ' '
802+ end ,
803+ list = function (_ , input )
804+ return ' list' , input .path or ' '
805+ end ,
806+ task = function (_ , input )
807+ return ' task' , input .description or ' '
808+ end ,
809+ grep = function (_ , input )
810+ return ' search' , M ._resolve_grep_string (input )
811+ end ,
812+ tool = function (_ , input )
813+ return ' tool' , input .description or ' '
814+ end ,
815+ }
816+
817+ --- Build the action line string for a part (icon + meaningful value, no duration)
818+ --- Used to show per-tool icon+label in child session activity lists.
819+ --- @param part OpencodeMessagePart
820+ --- @param status string Optional icon name to use for the status (e.g. , ' running' , ' completed' , ' error' ). If not provided , will use the default icon for the tool.
821+ --- @return string
822+ function M ._tool_action_line (part , status )
823+ local tool = part .tool
824+ local input = part .state and part .state .input or {}
825+ local handler = tool_summary_handlers [tool ] or tool_summary_handlers [' tool' ]
826+ local icon_name , tool_value = handler (part , input )
827+ if status ~= ' completed' then
828+ icon_name = status
829+ end
830+
831+ return M ._build_action_line (icon_name , tool or ' tool' , tool_value )
832+ end
833+
752834--- @param output Output Output object to write to
753835--- @param input TaskToolInput data for the tool
754836--- @param metadata TaskToolMetadata Metadata for the tool use
755837--- @param tool_output string
756838--- @param duration_text ? string
757- function M ._format_task_tool (output , input , metadata , tool_output , duration_text )
839+ --- @param get_child_parts ? fun ( session_id : string ): OpencodeMessagePart[] ?
840+ function M ._format_task_tool (output , input , metadata , tool_output , duration_text , get_child_parts )
758841 local start_line = output :get_line_count () + 1
759842
760843 -- Show agent type if available
@@ -764,28 +847,20 @@ function M._format_task_tool(output, input, metadata, tool_output, duration_text
764847 description = string.format (' %s (@%s)' , description , agent_type )
765848 end
766849
767- M .format_action (output , icons . get ( ' task' ) .. ' task' , description , duration_text )
850+ M .format_action (output , ' task' , ' task' , description , duration_text )
768851
769852 if config .ui .output .tools .show_output then
770- -- Show task summary from metadata
771- -- The summary contains items with structure: {id, tool, state: {status, title}}
772- if metadata and metadata .summary and type (metadata .summary ) == ' table' and # metadata .summary > 0 then
773- output :add_empty_line ()
853+ -- Show live tool activity from the child session
854+ local child_session_id = metadata and metadata .sessionId
855+ local child_parts = child_session_id and get_child_parts and get_child_parts (child_session_id )
774856
775- local status_icons = {
776- completed = icons .get (' status_on' ) or ' +' ,
777- running = icons .get (' run' ) or ' >' ,
778- pending = icons .get (' status_off' ) or ' -' ,
779- error = icons .get (' error' ) or ' x' ,
780- }
857+ if child_parts and # child_parts > 0 then
858+ output :add_empty_line ()
781859
782- for _ , item in ipairs (metadata . summary ) do
860+ for _ , item in ipairs (child_parts ) do
783861 if item .tool then
784862 local status = item .state and item .state .status or ' pending'
785- local title = item .state and item .state .title or item .tool
786- local icon = status_icons [status ] or status_icons .pending
787-
788- output :add_line (string.format (' %s %s' , icon , title ))
863+ output :add_line (' ' .. M ._tool_action_line (item , status ))
789864 end
790865 end
791866
@@ -794,8 +869,8 @@ function M._format_task_tool(output, input, metadata, tool_output, duration_text
794869
795870 -- Show tool output text (usually the final summary from the subagent)
796871 if tool_output and tool_output ~= ' ' then
797- -- Strip task_metadata tags from output for cleaner display
798- local clean_output = tool_output :gsub (' <task_metadata>.-</task_metadata> ' , ' ' ):gsub (' %s+$ ' , ' ' )
872+ -- remove the task_result tag, only get the inner content, since the tool output is already visually separated and the tag doesn't add much value in that case
873+ local clean_output = tool_output :gsub (' <task_result> ' , ' ' ):gsub (' </task_result> ' , ' ' )
799874 if clean_output ~= ' ' then
800875 output :add_empty_line ()
801876 output :add_lines (vim .split (clean_output , ' \n ' ))
895970--- @param part OpencodeMessagePart The part to format
896971--- @param message ? OpencodeMessage Optional message object to extract role and mentions from
897972--- @param is_last_part ? boolean Whether this is the last part in the message , used to show an error if there is one
973+ --- @param get_child_parts ? fun ( session_id : string ): OpencodeMessagePart[] ?
898974--- @return Output
899- function M .format_part (part , message , is_last_part )
975+ function M .format_part (part , message , is_last_part , get_child_parts )
900976 local output = Output .new ()
901977
902978 if not message or not message .info or not message .info .role then
@@ -931,7 +1007,7 @@ function M.format_part(part, message, is_last_part)
9311007 M ._format_reasoning (output , part )
9321008 content_added = true
9331009 elseif part .type == ' tool' then
934- M ._format_tool (output , part )
1010+ M ._format_tool (output , part , get_child_parts )
9351011 content_added = true
9361012 elseif part .type == ' patch' and part .hash then
9371013 M ._format_patch (output , part )
0 commit comments