1717local config = require (" codecompanion.config" )
1818
1919local Icons = require (" codecompanion.interactions.chat.ui.icons" )
20+ local Plan = require (" codecompanion.interactions.chat.ui.formatters.plan" )
2021local Reasoning = require (" codecompanion.interactions.chat.ui.formatters.reasoning" )
2122local Standard = require (" codecompanion.interactions.chat.ui.formatters.standard" )
2223local Tools = require (" codecompanion.interactions.chat.ui.formatters.tools" )
2324
2425local api = vim .api
2526
27+ --- Resolve a tool status to its icon and highlight groups
28+ --- Resolve a tool status to IconOpts
29+ --- @param status string
30+ --- @return CodeCompanion.Chat.UI.IconOpts
31+ local function resolve_tool_icon (status )
32+ local icons = config .display .chat .icons
33+ local map = {
34+ pending = {
35+ icon = icons .tool_pending ,
36+ icon_hl_group = " CodeCompanionChatToolPending" ,
37+ line_hl_group = " CodeCompanionChatToolText" ,
38+ },
39+ in_progress = {
40+ icon = icons .tool_in_progress ,
41+ icon_hl_group = " CodeCompanionChatToolInProgress" ,
42+ line_hl_group = " CodeCompanionChatToolText" ,
43+ },
44+ completed = {
45+ icon = icons .tool_success ,
46+ icon_hl_group = " CodeCompanionChatToolSuccessIcon" ,
47+ line_hl_group = " CodeCompanionChatToolText" ,
48+ },
49+ failed = {
50+ icon = icons .tool_failure ,
51+ icon_hl_group = " CodeCompanionChatToolFailureIcon" ,
52+ line_hl_group = " CodeCompanionChatToolText" ,
53+ },
54+ }
55+ return map [status ] or map .pending
56+ end
57+
58+ --- Resolve a plan status to IconOpts
59+ --- @param status string
60+ --- @return CodeCompanion.Chat.UI.IconOpts
61+ local function resolve_plan_icon (status )
62+ local icons = config .display .chat .icons
63+ local map = {
64+ pending = {
65+ icon = icons .tool_pending ,
66+ icon_hl_group = " CodeCompanionChatToolPending" ,
67+ line_hl_group = " CodeCompanionChatPlanPending" ,
68+ },
69+ in_progress = {
70+ icon = icons .tool_in_progress ,
71+ icon_hl_group = " CodeCompanionChatToolInProgress" ,
72+ line_hl_group = " CodeCompanionChatPlanInProgress" ,
73+ },
74+ completed = {
75+ icon = icons .tool_success ,
76+ icon_hl_group = " CodeCompanionChatToolSuccessIcon" ,
77+ line_hl_group = " CodeCompanionChatPlanCompleted" ,
78+ },
79+ failed = {
80+ icon = icons .tool_failure ,
81+ icon_hl_group = " CodeCompanionChatToolFailureIcon" ,
82+ line_hl_group = " CodeCompanionChatPlanFailed" ,
83+ },
84+ }
85+ return map [status ] or map .pending
86+ end
87+
2688local EPHEMERAL_STATE = {
2789 __index = {
2890 update_role = function (self , role )
@@ -32,9 +94,15 @@ local EPHEMERAL_STATE = {
3294 mark_reasoning_complete = function (self )
3395 self .has_reasoning_output = false
3496 end ,
97+ mark_plan_complete = function (self )
98+ self .has_plan_output = false
99+ end ,
35100 mark_reasoning_started = function (self )
36101 self .has_reasoning_output = true
37102 end ,
103+ mark_plan_started = function (self )
104+ self .has_plan_output = true
105+ end ,
38106 update_type = function (self , type )
39107 self .last_type = type
40108 end ,
@@ -76,6 +144,7 @@ function Builder.new(args)
76144 state = {
77145 last_role = args .chat ._last_role ,
78146 last_type = nil ,
147+ has_plan_output = false ,
79148 has_reasoning_output = false ,
80149
81150 -- Block tracking
@@ -99,6 +168,7 @@ function Builder.new(args)
99168 _formatters = {
100169 Tools :new (args .chat ),
101170 Reasoning :new (args .chat ),
171+ Plan :new (args .chat ),
102172 Standard :new (args .chat ),
103173 },
104174 _fmt_state = setmetatable ({}, EPHEMERAL_STATE ),
@@ -114,6 +184,7 @@ local function create_state(out, base_state)
114184
115185 state .last_role = base_state .last_role
116186 state .last_type = base_state .last_type
187+ state .has_plan_output = base_state .has_plan_output
117188 state .has_reasoning_output = base_state .has_reasoning_output
118189
119190 -- Block tracking
@@ -194,6 +265,11 @@ function Builder:add_message(data, opts)
194265 if opts ._icon_info and opts ._icon_info .has_icon and pre_content_lines > 0 then
195266 opts ._icon_info .line_offset = (opts ._icon_info .line_offset or 0 ) + pre_content_lines
196267 end
268+ if opts ._plan_icons and pre_content_lines > 0 then
269+ for _ , entry in ipairs (opts ._plan_icons ) do
270+ entry .line_offset = (entry .line_offset or 0 ) + pre_content_lines
271+ end
272+ end
197273
198274 local insert_line , icon_id
199275 if not vim .tbl_isempty (lines ) then
@@ -288,9 +364,19 @@ function Builder:_write_to_buffer(lines, opts)
288364 local icon_id
289365 if opts ._icon_info and opts ._icon_info .has_icon then
290366 local target_line = insert_line + (opts ._icon_info .line_offset or 0 )
291- icon_id = Icons .apply (self .chat .bufnr , target_line , opts ._icon_info .status , {
292- virt_text_pos = opts .virt_text_pos ,
293- })
367+ local icon_opts = resolve_tool_icon (opts ._icon_info .status )
368+ icon_opts .virt_text_pos = opts .virt_text_pos
369+ icon_id = Icons .apply (self .chat .bufnr , target_line , icon_opts )
370+ end
371+
372+ -- Plan entry icons
373+ if opts ._plan_icons then
374+ for _ , entry in ipairs (opts ._plan_icons ) do
375+ local target_line = insert_line + (entry .line_offset or 0 )
376+ local icon_opts = resolve_plan_icon (entry .status )
377+ icon_opts .virt_text_pos = " inline"
378+ Icons .apply (self .chat .bufnr , target_line , icon_opts )
379+ end
294380 end
295381
296382 -- Record write bounds
@@ -323,6 +409,15 @@ function Builder:_write_to_buffer(lines, opts)
323409 end )
324410 end
325411
412+ -- Plan folds
413+ if self .state .has_plan_output and not state .has_plan_output and config .display .chat .fold_reasoning then
414+ local range_start = self .state .current_section_start or 0
415+ local range_end = insert_line
416+ vim .schedule (function ()
417+ self .chat .ui .folds :create_plan_fold (self .chat , range_start , range_end )
418+ end )
419+ end
420+
326421 if state .last_role ~= config .constants .USER_ROLE then
327422 self .chat .ui :lock_buf ()
328423 end
335430--- Sync formatting state back to builder's persistent state
336431--- @param state table
337432function Builder :_sync_state_from_formatting_state (state )
433+ self .state .has_plan_output = state .has_plan_output
338434 self .state .has_reasoning_output = state .has_reasoning_output
339435 self .state .last_role = state .last_role
340436 self .state .last_type = state .last_type
@@ -382,7 +478,10 @@ function Builder:update_line(line_number, content, opts)
382478 end
383479 -- Also clear by line range as a safety net
384480 Icons .clear_line (self .chat .bufnr , start_line )
385- new_icon_id = Icons .apply (self .chat .bufnr , start_line , opts .status , opts )
481+ local icon_opts = resolve_tool_icon (opts .status )
482+ icon_opts .priority = opts .priority
483+ icon_opts .virt_text_pos = opts .virt_text_pos
484+ new_icon_id = Icons .apply (self .chat .bufnr , start_line , icon_opts )
386485 end
387486
388487 if self .state .last_role ~= config .constants .USER_ROLE then
0 commit comments