@@ -16,6 +16,7 @@ import Header from "./components/Header.js"
1616import ChatHistoryItem from "./components/ChatHistoryItem.js"
1717import LoadingText from "./components/LoadingText.js"
1818import ToastDisplay from "./components/ToastDisplay.js"
19+ import TodoDisplay from "./components/TodoDisplay.js"
1920import { useToast } from "./hooks/useToast.js"
2021import {
2122 AutocompleteInput ,
@@ -54,6 +55,7 @@ import type {
5455 SlashCommandResult ,
5556 ModeResult ,
5657 TaskHistoryItem ,
58+ ToolData ,
5759} from "./types.js"
5860import { getGlobalCommand , getGlobalCommandsForAutocomplete } from "../globalCommands.js"
5961
@@ -243,6 +245,9 @@ function AppInner({
243245 const seenMessageIds = useRef < Set < string > > ( new Set ( ) )
244246 const firstTextMessageSkipped = useRef ( false )
245247
248+ // Track pending command for injecting into command_output toolData
249+ const pendingCommandRef = useRef < string | null > ( null )
250+
246251 // Track Ctrl+C presses for "press again to exit" behavior
247252 const [ showExitHint , setShowExitHint ] = useState ( false )
248253 const exitHintTimeout = useRef < NodeJS . Timeout | null > ( null )
@@ -260,6 +265,9 @@ function AppInner({
260265 // Manual focus override: 'scroll' | 'input' | null (null = auto-determine)
261266 const [ manualFocus , setManualFocus ] = useState < "scroll" | "input" | null > ( null )
262267
268+ // State for TODO list viewer (shown via Ctrl+T shortcut)
269+ const [ showTodoViewer , setShowTodoViewer ] = useState ( false )
270+
263271 // Autocomplete picker state (received from AutocompleteInput via callback)
264272 // eslint-disable-next-line @typescript-eslint/no-explicit-any
265273 const [ pickerState , setPickerState ] = useState < AutocompletePickerState < any > > ( {
@@ -430,7 +438,32 @@ function AppInner({
430438 return
431439 }
432440
441+ // Ctrl+T to toggle TODO list viewer
442+ if ( matchesGlobalSequence ( input , key , "ctrl-t" ) ) {
443+ // Close picker if open
444+ if ( pickerState . isOpen ) {
445+ autocompleteRef . current ?. closePicker ( )
446+ followupAutocompleteRef . current ?. closePicker ( )
447+ }
448+ // Toggle TODO viewer
449+ setShowTodoViewer ( ( prev ) => {
450+ const newValue = ! prev
451+ if ( newValue && currentTodos . length === 0 ) {
452+ showInfo ( "No TODO list available" , 2000 )
453+ return false
454+ }
455+ return newValue
456+ } )
457+ return
458+ }
459+
433460 // Escape key to cancel/pause task when loading (streaming)
461+ // Escape key to close TODO viewer
462+ if ( key . escape && showTodoViewer ) {
463+ setShowTodoViewer ( false )
464+ return
465+ }
466+
434467 if ( key . escape && isLoading && hostRef . current ) {
435468 // If picker is open, let the picker handle escape first
436469 if ( pickerState . isOpen ) {
@@ -599,12 +632,23 @@ function AppInner({
599632 let toolName : string | undefined
600633 let toolDisplayName : string | undefined
601634 let toolDisplayOutput : string | undefined
635+ let toolData : ToolData | undefined
602636
603637 if ( say === "command_output" ) {
604638 role = "tool"
605639 toolName = "execute_command"
606640 toolDisplayName = "bash"
607641 toolDisplayOutput = text
642+ // Create toolData for command output, including the pending command if available
643+ const trackedCommand = pendingCommandRef . current
644+ toolInspectorLog ( "say:command_output" , { ts, trackedCommand, outputLength : text ?. length } )
645+ toolData = {
646+ tool : "execute_command" ,
647+ command : trackedCommand || undefined ,
648+ output : text ,
649+ }
650+ // Clear the pending command after using it
651+ pendingCommandRef . current = null
608652 } else if ( say === "tool" ) {
609653 role = "tool"
610654 try {
@@ -621,6 +665,8 @@ function AppInner({
621665 toolName = toolInfo . tool
622666 toolDisplayName = toolInfo . tool
623667 toolDisplayOutput = formatToolOutput ( toolInfo )
668+ // Extract structured toolData for rich rendering
669+ toolData = extractToolData ( toolInfo )
624670
625671 // Special handling for update_todo_list tool
626672 if ( toolName === "update_todo_list" || toolName === "updateTodoList" ) {
@@ -643,6 +689,7 @@ function AppInner({
643689 originalType : say ,
644690 todos,
645691 previousTodos : prevTodos ,
692+ toolData,
646693 } )
647694 return
648695 }
@@ -665,6 +712,7 @@ function AppInner({
665712 toolDisplayOutput,
666713 partial,
667714 originalType : say ,
715+ toolData,
668716 } )
669717 } ,
670718 [ addMessage , verbose , currentTodos , setTodos ] ,
@@ -707,9 +755,52 @@ function AppInner({
707755 seenMessageIds . current . add ( messageId )
708756 setComplete ( true )
709757 setLoading ( false )
758+
759+ // Parse the completion result and add a message for CompletionTool to render
760+ try {
761+ const completionInfo = JSON . parse ( text ) as Record < string , unknown >
762+ const toolData : ToolData = {
763+ tool : "attempt_completion" ,
764+ result : completionInfo . result as string | undefined ,
765+ content : completionInfo . result as string | undefined ,
766+ }
767+
768+ addMessage ( {
769+ id : messageId ,
770+ role : "tool" ,
771+ content : text ,
772+ toolName : "attempt_completion" ,
773+ toolDisplayName : "Task Complete" ,
774+ toolDisplayOutput : formatToolOutput ( { tool : "attempt_completion" , ...completionInfo } ) ,
775+ originalType : ask ,
776+ toolData,
777+ } )
778+ } catch {
779+ // If parsing fails, still add a basic completion message
780+ addMessage ( {
781+ id : messageId ,
782+ role : "tool" ,
783+ content : text || "Task completed" ,
784+ toolName : "attempt_completion" ,
785+ toolDisplayName : "Task Complete" ,
786+ toolDisplayOutput : "✅ Task completed" ,
787+ originalType : ask ,
788+ toolData : {
789+ tool : "attempt_completion" ,
790+ content : text ,
791+ } ,
792+ } )
793+ }
710794 return
711795 }
712796
797+ // Track pending command BEFORE nonInteractive handling
798+ // This ensures we capture the command text for later injection into command_output toolData
799+ if ( ask === "command" ) {
800+ toolInspectorLog ( "ask:command:tracking" , { ts, text } )
801+ pendingCommandRef . current = text
802+ }
803+
713804 if ( nonInteractive && ask !== "followup" ) {
714805 seenMessageIds . current . add ( messageId )
715806
@@ -718,6 +809,9 @@ function AppInner({
718809 let toolDisplayName : string | undefined
719810 let toolDisplayOutput : string | undefined
720811 let formattedContent = text || ""
812+ let toolData : ToolData | undefined
813+ let todos : TodoItem [ ] | undefined
814+ let previousTodos : TodoItem [ ] | undefined
721815
722816 try {
723817 const toolInfo = JSON . parse ( text ) as Record < string , unknown >
@@ -734,6 +828,19 @@ function AppInner({
734828 toolDisplayName = toolInfo . tool as string
735829 toolDisplayOutput = formatToolOutput ( toolInfo )
736830 formattedContent = formatToolAskMessage ( toolInfo )
831+ // Extract structured toolData for rich rendering
832+ toolData = extractToolData ( toolInfo )
833+
834+ // Special handling for update_todo_list tool - extract todos
835+ if ( toolName === "update_todo_list" || toolName === "updateTodoList" ) {
836+ const parsedTodos = parseTodosFromToolInfo ( toolInfo )
837+ if ( parsedTodos && parsedTodos . length > 0 ) {
838+ todos = parsedTodos
839+ // Capture previous todos before updating global state
840+ previousTodos = [ ...currentTodos ]
841+ setTodos ( parsedTodos )
842+ }
843+ }
737844 } catch {
738845 // Use raw text if not valid JSON
739846 }
@@ -746,6 +853,9 @@ function AppInner({
746853 toolDisplayName,
747854 toolDisplayOutput,
748855 originalType : ask ,
856+ toolData,
857+ todos,
858+ previousTodos,
749859 } )
750860 } else {
751861 addMessage ( {
@@ -786,6 +896,7 @@ function AppInner({
786896 // Use raw text if not valid JSON
787897 }
788898 }
899+ // Note: ask === "command" is handled above before the nonInteractive block
789900
790901 seenMessageIds . current . add ( messageId )
791902
@@ -796,7 +907,7 @@ function AppInner({
796907 suggestions,
797908 } )
798909 } ,
799- [ addMessage , setPendingAsk , setComplete , setLoading , nonInteractive ] ,
910+ [ addMessage , setPendingAsk , setComplete , setLoading , nonInteractive , currentTodos , setTodos ] ,
800911 )
801912
802913 // Handle extension messages
@@ -1282,7 +1393,7 @@ function AppInner({
12821393 ) : isScrollAreaActive ? (
12831394 < ScrollIndicator scrollTop = { scrollState . scrollTop } maxScroll = { scrollState . maxScroll } isScrollFocused = { true } />
12841395 ) : isInputAreaActive ? (
1285- < Text color = { theme . dimText } > ? for shortcuts • Ctrl+M mode </ Text >
1396+ < Text color = { theme . dimText } > ? for shortcuts</ Text >
12861397 ) : null
12871398
12881399 // Get render function for picker items based on active trigger
@@ -1428,7 +1539,14 @@ function AppInner({
14281539 prompt = "› "
14291540 />
14301541 < HorizontalLine active = { isInputAreaActive } />
1431- { pickerState . isOpen ? (
1542+ { showTodoViewer ? (
1543+ < Box flexDirection = "column" height = { PICKER_HEIGHT } >
1544+ < TodoDisplay todos = { currentTodos } showProgress = { true } title = "TODO List" />
1545+ < Box height = { 1 } >
1546+ < Text color = { theme . dimText } > Ctrl+T to close</ Text >
1547+ </ Box >
1548+ </ Box >
1549+ ) : pickerState . isOpen ? (
14321550 < Box flexDirection = "column" height = { PICKER_HEIGHT } >
14331551 < PickerSelect
14341552 results = { pickerState . results }
@@ -1464,6 +1582,114 @@ export function App(props: TUIAppProps) {
14641582 )
14651583}
14661584
1585+ /**
1586+ * Extract structured ToolData from parsed tool JSON
1587+ * This provides rich data for tool-specific renderers
1588+ */
1589+ function extractToolData ( toolInfo : Record < string , unknown > ) : ToolData {
1590+ const toolName = ( toolInfo . tool as string ) || "unknown"
1591+
1592+ // Base tool data with common fields
1593+ const toolData : ToolData = {
1594+ tool : toolName ,
1595+ path : toolInfo . path as string | undefined ,
1596+ isOutsideWorkspace : toolInfo . isOutsideWorkspace as boolean | undefined ,
1597+ isProtected : toolInfo . isProtected as boolean | undefined ,
1598+ content : toolInfo . content as string | undefined ,
1599+ reason : toolInfo . reason as string | undefined ,
1600+ }
1601+
1602+ // Extract diff-related fields
1603+ if ( toolInfo . diff !== undefined ) {
1604+ toolData . diff = toolInfo . diff as string
1605+ }
1606+ if ( toolInfo . diffStats !== undefined ) {
1607+ const stats = toolInfo . diffStats as { added ?: number ; removed ?: number }
1608+ if ( typeof stats . added === "number" && typeof stats . removed === "number" ) {
1609+ toolData . diffStats = { added : stats . added , removed : stats . removed }
1610+ }
1611+ }
1612+
1613+ // Extract search-related fields
1614+ if ( toolInfo . regex !== undefined ) {
1615+ toolData . regex = toolInfo . regex as string
1616+ }
1617+ if ( toolInfo . filePattern !== undefined ) {
1618+ toolData . filePattern = toolInfo . filePattern as string
1619+ }
1620+ if ( toolInfo . query !== undefined ) {
1621+ toolData . query = toolInfo . query as string
1622+ }
1623+
1624+ // Extract mode-related fields
1625+ if ( toolInfo . mode !== undefined ) {
1626+ toolData . mode = toolInfo . mode as string
1627+ }
1628+ if ( toolInfo . mode_slug !== undefined ) {
1629+ toolData . mode = toolInfo . mode_slug as string
1630+ }
1631+
1632+ // Extract command-related fields
1633+ if ( toolInfo . command !== undefined ) {
1634+ toolData . command = toolInfo . command as string
1635+ }
1636+ if ( toolInfo . output !== undefined ) {
1637+ toolData . output = toolInfo . output as string
1638+ }
1639+
1640+ // Extract browser-related fields
1641+ if ( toolInfo . action !== undefined ) {
1642+ toolData . action = toolInfo . action as string
1643+ }
1644+ if ( toolInfo . url !== undefined ) {
1645+ toolData . url = toolInfo . url as string
1646+ }
1647+ if ( toolInfo . coordinate !== undefined ) {
1648+ toolData . coordinate = toolInfo . coordinate as string
1649+ }
1650+
1651+ // Extract batch file operations
1652+ if ( Array . isArray ( toolInfo . files ) ) {
1653+ toolData . batchFiles = ( toolInfo . files as Array < Record < string , unknown > > ) . map ( ( f ) => ( {
1654+ path : ( f . path as string ) || "" ,
1655+ lineSnippet : f . lineSnippet as string | undefined ,
1656+ isOutsideWorkspace : f . isOutsideWorkspace as boolean | undefined ,
1657+ key : f . key as string | undefined ,
1658+ content : f . content as string | undefined ,
1659+ } ) )
1660+ }
1661+
1662+ // Extract batch diff operations
1663+ if ( Array . isArray ( toolInfo . batchDiffs ) ) {
1664+ toolData . batchDiffs = ( toolInfo . batchDiffs as Array < Record < string , unknown > > ) . map ( ( d ) => ( {
1665+ path : ( d . path as string ) || "" ,
1666+ changeCount : d . changeCount as number | undefined ,
1667+ key : d . key as string | undefined ,
1668+ content : d . content as string | undefined ,
1669+ diffStats : d . diffStats as { added : number ; removed : number } | undefined ,
1670+ diffs : d . diffs as Array < { content : string ; startLine ?: number } > | undefined ,
1671+ } ) )
1672+ }
1673+
1674+ // Extract question/completion fields
1675+ if ( toolInfo . question !== undefined ) {
1676+ toolData . question = toolInfo . question as string
1677+ }
1678+ if ( toolInfo . result !== undefined ) {
1679+ toolData . result = toolInfo . result as string
1680+ }
1681+
1682+ // Extract additional display hints
1683+ if ( toolInfo . lineNumber !== undefined ) {
1684+ toolData . lineNumber = toolInfo . lineNumber as number
1685+ }
1686+ if ( toolInfo . additionalFileCount !== undefined ) {
1687+ toolData . additionalFileCount = toolInfo . additionalFileCount as number
1688+ }
1689+
1690+ return toolData
1691+ }
1692+
14671693/**
14681694 * Format tool output for display (used in the message body, header shows tool name separately)
14691695 */
0 commit comments