@@ -8,12 +8,10 @@ local snapshot = require('opencode.snapshot')
88local mention = require (' opencode.ui.mention' )
99local permission_window = require (' opencode.ui.permission_window' )
1010local tool_formatters = require (' opencode.ui.formatter.tools' )
11- local tool_helpers = require (' opencode.ui.formatter.tools.helpers ' )
11+ local format_utils = require (' opencode.ui.formatter.utils ' )
1212
1313local M = {}
1414
15- --- @note child-session parts are requested from the renderer at format time
16-
1715M .separator = {
1816 ' ----' ,
1917 ' ' ,
@@ -35,7 +33,7 @@ function M._format_reasoning(output, part)
3533 end
3634 end
3735
38- M .format_action (output , ' reasoning' , title , ' ' )
36+ format_utils .format_action (output , icons . get ( ' reasoning' ) , title , ' ' )
3937
4038 if config .ui .output .tools .show_reasoning_output and text ~= ' ' then
4139 output :add_empty_line ()
@@ -53,69 +51,13 @@ function M._format_reasoning(output, part)
5351 end
5452end
5553
56- --- Calculate statistics for reverted messages and tool calls
57- --- @param messages { info : MessageInfo , parts : OpencodeMessagePart[] } [] All messages in the session
58- --- @param revert_index number Index of the message where revert occurred
59- --- @param revert_info SessionRevertInfo Revert information
60- --- @return { messages : number , tool_calls : number , files : table<string , { additions : number , deletions : number } >}
61- function M ._calculate_revert_stats (messages , revert_index , revert_info )
62- local stats = {
63- messages = 0 ,
64- tool_calls = 0 ,
65- files = {}, -- { [filename] = { additions = n, deletions = m } }
66- }
67-
68- for i = revert_index , # messages do
69- local msg = messages [i ]
70- if msg .info .role == ' user' then
71- stats .messages = stats .messages + 1
72- end
73- if msg .parts then
74- for _ , part in ipairs (msg .parts ) do
75- if part .type == ' tool' then
76- stats .tool_calls = stats .tool_calls + 1
77- end
78- end
79- end
80- end
81-
82- if revert_info .diff then
83- local current_file = nil
84- for line in revert_info .diff :gmatch (' [^\r\n ]+' ) do
85- local file_a = line :match (' ^%-%-%- ([ab]/.+)' )
86- local file_b = line :match (' ^%+%+%+ ([ab]/.+)' )
87- if file_b then
88- current_file = file_b :gsub (' ^[ab]/' , ' ' )
89- if not stats .files [current_file ] then
90- stats .files [current_file ] = { additions = 0 , deletions = 0 }
91- end
92- elseif file_a then
93- current_file = file_a :gsub (' ^[ab]/' , ' ' )
94- if not stats .files [current_file ] then
95- stats .files [current_file ] = { additions = 0 , deletions = 0 }
96- end
97- elseif line :sub (1 , 1 ) == ' +' and not line :match (' ^%+%+%+' ) then
98- if current_file then
99- stats .files [current_file ].additions = stats .files [current_file ].additions + 1
100- end
101- elseif line :sub (1 , 1 ) == ' -' and not line :match (' ^%-%-%-' ) then
102- if current_file then
103- stats .files [current_file ].deletions = stats .files [current_file ].deletions + 1
104- end
105- end
106- end
107- end
108-
109- return stats
110- end
111-
11254--- Format the revert callout with statistics
11355--- @param session_data OpencodeMessage[] All messages in the session
11456--- @param start_idx number Index of the message where revert occurred
11557--- @return Output output object representing the lines , extmarks , and actions
11658function M ._format_revert_message (session_data , start_idx )
11759 local output = Output .new ()
118- local stats = M . _calculate_revert_stats (session_data , start_idx , state .active_session .revert )
60+ local stats = format_utils . calculate_revert_stats (session_data , start_idx , state .active_session .revert )
11961 local message_text = stats .messages == 1 and ' message' or ' messages'
12062 local tool_text = stats .tool_calls == 1 and ' tool call' or ' tool calls'
12163
@@ -177,7 +119,7 @@ function M._format_patch(output, part)
177119 end
178120
179121 local restore_points = snapshot .get_restore_points_by_parent (part .hash ) or {}
180- M .format_action (output , ' snapshot' , ' Created Snapshot' , vim .trim (part .hash :sub (1 , 8 )))
122+ format_utils .format_action (output , icons . get ( ' snapshot' ) , ' Created Snapshot' , vim .trim (part .hash :sub (1 , 8 )))
181123
182124 -- Anchor all snapshot-level actions to the snapshot header line
183125 add_action (output , ' [R]evert file' , ' diff_revert_selected_file' , { part .hash }, ' R' )
@@ -469,88 +411,19 @@ function M._format_assistant_message(output, text)
469411 output :add_lines (vim .split (result , ' \n ' ))
470412end
471413
472- --- Build the formatted action line string without writing to output
473- --- @param icon_name string Name of the icon to fetch with ` icons.get`
474- --- @param tool_type string Tool type (e.g. , ' run' , ' read' , ' edit' , etc. )
475- --- @param value string Value associated with the action (e.g. , filename , command )
476- --- @param duration_text ? string
477- --- @return string
478- function M ._build_action_line (icon_name , tool_type , value , duration_text )
479- local icon = icons .get (icon_name )
480- local detail = value and # value > 0 and (' `' .. value .. ' `' ) or ' '
481- local duration_suffix = duration_text and (' ' .. duration_text ) or ' '
482- return string.format (' **%s %s** %s%s' , icon , tool_type , detail , duration_suffix )
483- end
484-
485- --- @param output Output Output object to write to
486- --- @param tool_type string Tool type (e.g. , ' run' , ' read' , ' edit' , etc. )
487- --- @param value string Value associated with the action (e.g. , filename , command )
488- --- @param duration_text ? string
489- function M .format_action (output , icon_name , tool_type , value , duration_text )
490- if not icon_name or not tool_type then
491- return
492- end
493- output :add_line (M ._build_action_line (icon_name , tool_type , value , duration_text ))
494- end
495-
496- function M ._resolve_file_name (file_path )
497- return tool_helpers .resolve_file_name (file_path )
498- end
499-
500- --- @param file_path string
501- --- @param tool_output ? string
502- --- @return boolean
503- function M ._is_directory_path (file_path , tool_output )
504- return tool_helpers .is_directory_path (file_path , tool_output )
505- end
506-
507- --- @param file_path string
508- --- @param tool_output ? string
509- --- @return string
510- function M ._resolve_display_file_name (file_path , tool_output )
511- return tool_helpers .resolve_display_file_name (file_path , tool_output )
512- end
513-
514- function M ._resolve_grep_string (input )
515- return tool_helpers .resolve_grep_string (input )
516- end
517-
518414--- @param output Output Output object to write to
519415--- @param part OpencodeMessagePart
520416--- @param get_child_parts ? fun ( session_id : string ): OpencodeMessagePart[] ?
521- function M ._format_tool (output , part , get_child_parts )
417+ function M .format_tool (output , part , get_child_parts )
522418 local tool = part .tool
523419 if not tool or not part .state then
524420 return
525421 end
526422
527423 local start_line = output :get_line_count () + 1
528- local input = part .state .input or {}
529- local metadata = part .state .metadata or {}
530- local tool_output = part .state .output or ' '
531- local tool_time = part .state .time or {}
532- local tool_status = part .state .status
533- local should_show_duration = tool ~= ' question' and tool_status ~= ' pending'
534- local duration_text = should_show_duration and util .format_duration_seconds (tool_time .start , tool_time [' end' ]) or nil
535424
536425 local formatter = tool_formatters [tool ] or tool_formatters .tool
537- formatter .format ({
538- output = output ,
539- tool_type = tool ,
540- input = input ,
541- metadata = metadata ,
542- tool_output = tool_output ,
543- title = part .state .title ,
544- status = part .state .status ,
545- duration_text = duration_text ,
546- config = config ,
547- format_action = M .format_action ,
548- format_diff = M .format_diff ,
549- format_code = M ._format_code ,
550- build_action_line = M ._build_action_line ,
551- get_child_parts = get_child_parts ,
552- tool_summary_handlers = M ._tool_summary_handlers ,
553- })
426+ formatter .format (output , part , get_child_parts )
554427
555428 if part .state .status == ' error' and part .state .error then
556429 output :add_line (' ' )
@@ -569,155 +442,6 @@ function M._format_tool(output, part, get_child_parts)
569442 end
570443end
571444
572- M ._tool_summary_handlers = {
573- bash = function (part , input , metadata )
574- return tool_formatters .bash .summary (part , input , metadata )
575- end ,
576- read = function (part , input , metadata )
577- return tool_formatters .read .summary (part , input , metadata )
578- end ,
579- edit = function (part , input , metadata )
580- return tool_formatters .edit .summary (part , input , metadata )
581- end ,
582- write = function (part , input , metadata )
583- return tool_formatters .write .summary (part , input , metadata )
584- end ,
585- apply_patch = function (part , input , metadata )
586- return tool_formatters .apply_patch .summary (part , input , metadata )
587- end ,
588- todowrite = function (part , input , metadata )
589- return tool_formatters .todowrite .summary (part , input , metadata )
590- end ,
591- glob = function (part , input , metadata )
592- return tool_formatters .glob .summary (part , input , metadata )
593- end ,
594- webfetch = function (part , input , metadata )
595- return tool_formatters .webfetch .summary (part , input , metadata )
596- end ,
597- list = function (part , input , metadata )
598- return tool_formatters .list .summary (part , input , metadata )
599- end ,
600- task = function (part , input , metadata )
601- return tool_formatters .task .summary (part , input , metadata )
602- end ,
603- grep = function (part , input , metadata )
604- return tool_formatters .grep .summary (part , input , metadata )
605- end ,
606- tool = function (part , input , metadata )
607- return tool_formatters .tool .summary (part , input , metadata )
608- end ,
609- }
610-
611- --- @param output Output Output object to write to
612- --- @param lines string[]
613- --- @param language string
614- function M ._format_code (output , lines , language )
615- output :add_empty_line ()
616- --- NOTE: use longer code fence because lines could contain ```
617- output :add_line (' `````' .. (language or ' ' ))
618- output :add_lines (util .sanitize_lines (lines ))
619- output :add_line (' `````' )
620- end
621-
622- --- @param lines string[]
623- local function parse_diff_line_numbers (lines )
624- local numbered_lines = {}
625- local old_line
626- local new_line
627- local max_line_number = 0
628-
629- for idx , line in ipairs (lines ) do
630- local old_start , new_start = line :match (' ^@@ %-(%d+),?%d* %+(%d+),?%d* @@' )
631-
632- if old_start and new_start then
633- old_line = tonumber (old_start )
634- new_line = tonumber (new_start )
635- elseif old_line and new_line then
636- local first_char = line :sub (1 , 1 )
637-
638- if first_char == ' ' then
639- numbered_lines [idx ] = { old = old_line , new = new_line }
640- max_line_number = math.max (max_line_number , old_line , new_line )
641- old_line = old_line + 1
642- new_line = new_line + 1
643- elseif first_char == ' +' and not line :match (' ^%+%+%+%s' ) then
644- numbered_lines [idx ] = { old = nil , new = new_line }
645- max_line_number = math.max (max_line_number , new_line )
646- new_line = new_line + 1
647- elseif first_char == ' -' and not line :match (' ^%-%-%-%s' ) then
648- numbered_lines [idx ] = { old = old_line , new = nil }
649- max_line_number = math.max (max_line_number , old_line )
650- old_line = old_line + 1
651- end
652- end
653- end
654-
655- return numbered_lines , math.max (# tostring (max_line_number ), 4 )
656- end
657-
658- local function build_diff_gutter (line_numbers , width )
659- local line_number = line_numbers .new or line_numbers .old
660- return string.format (' %-' .. width .. ' s' , line_number and tostring (line_number ) or ' ' )
661- end
662-
663- local function add_diff_line (output , line , line_numbers , width )
664- local first_char = line :sub (1 , 1 )
665- local line_hl = first_char == ' +' and ' OpencodeDiffAdd' or first_char == ' -' and ' OpencodeDiffDelete' or nil
666- local gutter_hl = first_char == ' +' and ' OpencodeDiffAddGutter'
667- or first_char == ' -' and ' OpencodeDiffDeleteGutter'
668- or ' OpencodeDiffGutter'
669- local sign_hl = gutter_hl
670- local gutter = build_diff_gutter (line_numbers , width )
671- local gutter_width = # gutter + 2
672-
673- output :add_line (string.rep (' ' , gutter_width ) .. line :sub (2 ))
674-
675- local line_idx = output :get_line_count ()
676- local extmark = {
677- end_col = 0 ,
678- end_row = line_idx ,
679- virt_text = {
680- { gutter , gutter_hl },
681- { first_char , sign_hl },
682- { ' ' , gutter_hl },
683- },
684- priority = 5000 ,
685- right_gravity = true ,
686- end_right_gravity = false ,
687- virt_text_hide = false ,
688- virt_text_pos = ' overlay' ,
689- virt_text_repeat_linebreak = false ,
690- }
691-
692- if line_hl then
693- extmark .hl_group = line_hl
694- extmark .hl_eol = true
695- end
696-
697- output :add_extmark (line_idx - 1 , extmark --[[ @as OutputExtmark]] )
698- end
699-
700- function M .format_diff (output , code , file_type )
701- output :add_empty_line ()
702-
703- --- NOTE: use longer code fence because code could contain ```
704- output :add_line (' `````' .. file_type )
705- local full_lines = vim .split (code , ' \n ' )
706- local numbered_lines , line_number_width = parse_diff_line_numbers (full_lines )
707- local first_visible_line = # full_lines > 5 and 6 or 1
708- local lines = first_visible_line > 1 and vim .list_slice (full_lines , first_visible_line ) or full_lines
709-
710- for idx , line in ipairs (lines ) do
711- local source_idx = first_visible_line + idx - 1
712- if numbered_lines [source_idx ] then
713- add_diff_line (output , line , numbered_lines [source_idx ], line_number_width )
714- else
715- output :add_line (line )
716- end
717- end
718- output :add_line (' `````' )
719- end
720-
721445--- @param output Output Output object to write to
722446--- @param start_line number
723447--- @param end_line number
@@ -783,7 +507,7 @@ function M.format_part(part, message, is_last_part, get_child_parts)
783507 M ._format_reasoning (output , part )
784508 content_added = true
785509 elseif part .type == ' tool' then
786- M ._format_tool (output , part , get_child_parts )
510+ M .format_tool (output , part , get_child_parts )
787511 content_added = true
788512 elseif part .type == ' patch' and part .hash then
789513 M ._format_patch (output , part )
0 commit comments