@@ -18,6 +18,9 @@ class BackgroundCommandsPanel(Vertical):
1818 the selected command's output is re-rendered only when its stream offset
1919 changes. Reading output is non-destructive so the agent keeps its own
2020 incremental view of the same buffer.
21+
22+ Commands that exceed their timeout and keep running appear here once the
23+ timeout fires, so the user can watch them continue.
2124 """
2225
2326 REFRESH_INTERVAL = 0.5
@@ -112,37 +115,47 @@ def _is_shown(self) -> bool:
112115 return self .parent is None or self .parent .display
113116
114117 def _active_commands (self ) -> Dict [str , dict ]:
115- """Return only running commands started in the background."""
118+ """Return running commands, including timeouts that continued in the background."""
116119 commands = {}
117120
118121 for command_key , info in BackgroundCommandManager .list_background_commands ().items ():
119122 if not info .get ("running" ):
120123 continue
121124
122- if info .get ("persist" ):
125+ # Commands started with background=True are always listed; timeout-managed
126+ # commands only once their timeout elapsed and they kept running.
127+ if info .get ("persist" ) and not info .get ("continued_in_background" ):
123128 continue
124129
125130 commands [command_key ] = info
126131
127132 return commands
128133
129134 def _sync_tabs (self , commands : Dict [str , dict ]) -> None :
130- """Rebuild the tab bar when the set of active commands changes."""
135+ """Add and remove tabs so they match the active command set.
136+
137+ Tabs must be updated incrementally: ``Tabs.clear`` removes its children
138+ asynchronously, so clearing and immediately re-adding the same IDs
139+ races with the pending removal and raises ``DuplicateIds``.
140+ """
131141 tabs = self .query_one ("#bg-command-tabs" , Tabs )
142+ previous_keys = list (self ._keys )
132143 self ._keys = list (commands )
133144
134145 with self .prevent (Tabs .TabActivated ):
135- tabs .clear ()
146+ for command_key in previous_keys :
147+ if command_key not in commands :
148+ tabs .remove_tab (command_key )
136149
137150 for command_key in commands :
138- tabs .add_tab (Tab (command_key , id = command_key , classes = "bg-tab" ))
151+ if command_key not in previous_keys :
152+ tabs .add_tab (Tab (command_key , id = command_key , classes = "bg-tab" ))
139153
140- if self ._active_key in commands :
141- tabs .active = self ._active_key
142- else :
154+ if self ._active_key not in commands :
143155 self ._active_key = next (iter (commands ), None )
144- if self ._active_key is not None :
145- tabs .active = self ._active_key
156+
157+ if self ._active_key is not None :
158+ tabs .active = self ._active_key
146159
147160 def _select_tab (self , command_key : str ) -> None :
148161 """Mark a tab active without relying on a user click."""
@@ -163,6 +176,10 @@ def _set_command_line(self, info: dict) -> None:
163176 """Show the selected command, truncated to the panel width."""
164177 command = " " .join ((info .get ("command" ) or "" ).split ())
165178 text = f"command: { command } " if command else "command: (none)"
179+
180+ if info .get ("continued_in_background" ):
181+ text += " (timed out, still running)"
182+
166183 width = max (20 , self .size .width - 6 )
167184
168185 if len (text ) > width :
0 commit comments