tunacode-cli 0.1.21__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of tunacode-cli might be problematic. Click here for more details.

Files changed (174) hide show
  1. tunacode/__init__.py +0 -0
  2. tunacode/cli/textual_repl.tcss +283 -0
  3. tunacode/configuration/__init__.py +1 -0
  4. tunacode/configuration/defaults.py +45 -0
  5. tunacode/configuration/models.py +147 -0
  6. tunacode/configuration/models_registry.json +1 -0
  7. tunacode/configuration/pricing.py +74 -0
  8. tunacode/configuration/settings.py +35 -0
  9. tunacode/constants.py +227 -0
  10. tunacode/core/__init__.py +6 -0
  11. tunacode/core/agents/__init__.py +39 -0
  12. tunacode/core/agents/agent_components/__init__.py +48 -0
  13. tunacode/core/agents/agent_components/agent_config.py +441 -0
  14. tunacode/core/agents/agent_components/agent_helpers.py +290 -0
  15. tunacode/core/agents/agent_components/message_handler.py +99 -0
  16. tunacode/core/agents/agent_components/node_processor.py +477 -0
  17. tunacode/core/agents/agent_components/response_state.py +129 -0
  18. tunacode/core/agents/agent_components/result_wrapper.py +51 -0
  19. tunacode/core/agents/agent_components/state_transition.py +112 -0
  20. tunacode/core/agents/agent_components/streaming.py +271 -0
  21. tunacode/core/agents/agent_components/task_completion.py +40 -0
  22. tunacode/core/agents/agent_components/tool_buffer.py +44 -0
  23. tunacode/core/agents/agent_components/tool_executor.py +101 -0
  24. tunacode/core/agents/agent_components/truncation_checker.py +37 -0
  25. tunacode/core/agents/delegation_tools.py +109 -0
  26. tunacode/core/agents/main.py +545 -0
  27. tunacode/core/agents/prompts.py +66 -0
  28. tunacode/core/agents/research_agent.py +231 -0
  29. tunacode/core/compaction.py +218 -0
  30. tunacode/core/prompting/__init__.py +27 -0
  31. tunacode/core/prompting/loader.py +66 -0
  32. tunacode/core/prompting/prompting_engine.py +98 -0
  33. tunacode/core/prompting/sections.py +50 -0
  34. tunacode/core/prompting/templates.py +69 -0
  35. tunacode/core/state.py +409 -0
  36. tunacode/exceptions.py +313 -0
  37. tunacode/indexing/__init__.py +5 -0
  38. tunacode/indexing/code_index.py +432 -0
  39. tunacode/indexing/constants.py +86 -0
  40. tunacode/lsp/__init__.py +112 -0
  41. tunacode/lsp/client.py +351 -0
  42. tunacode/lsp/diagnostics.py +19 -0
  43. tunacode/lsp/servers.py +101 -0
  44. tunacode/prompts/default_prompt.md +952 -0
  45. tunacode/prompts/research/sections/agent_role.xml +5 -0
  46. tunacode/prompts/research/sections/constraints.xml +14 -0
  47. tunacode/prompts/research/sections/output_format.xml +57 -0
  48. tunacode/prompts/research/sections/tool_use.xml +23 -0
  49. tunacode/prompts/sections/advanced_patterns.xml +255 -0
  50. tunacode/prompts/sections/agent_role.xml +8 -0
  51. tunacode/prompts/sections/completion.xml +10 -0
  52. tunacode/prompts/sections/critical_rules.xml +37 -0
  53. tunacode/prompts/sections/examples.xml +220 -0
  54. tunacode/prompts/sections/output_style.xml +94 -0
  55. tunacode/prompts/sections/parallel_exec.xml +105 -0
  56. tunacode/prompts/sections/search_pattern.xml +100 -0
  57. tunacode/prompts/sections/system_info.xml +6 -0
  58. tunacode/prompts/sections/tool_use.xml +84 -0
  59. tunacode/prompts/sections/user_instructions.xml +3 -0
  60. tunacode/py.typed +0 -0
  61. tunacode/templates/__init__.py +5 -0
  62. tunacode/templates/loader.py +15 -0
  63. tunacode/tools/__init__.py +10 -0
  64. tunacode/tools/authorization/__init__.py +29 -0
  65. tunacode/tools/authorization/context.py +32 -0
  66. tunacode/tools/authorization/factory.py +20 -0
  67. tunacode/tools/authorization/handler.py +58 -0
  68. tunacode/tools/authorization/notifier.py +35 -0
  69. tunacode/tools/authorization/policy.py +19 -0
  70. tunacode/tools/authorization/requests.py +119 -0
  71. tunacode/tools/authorization/rules.py +72 -0
  72. tunacode/tools/bash.py +222 -0
  73. tunacode/tools/decorators.py +213 -0
  74. tunacode/tools/glob.py +353 -0
  75. tunacode/tools/grep.py +468 -0
  76. tunacode/tools/grep_components/__init__.py +9 -0
  77. tunacode/tools/grep_components/file_filter.py +93 -0
  78. tunacode/tools/grep_components/pattern_matcher.py +158 -0
  79. tunacode/tools/grep_components/result_formatter.py +87 -0
  80. tunacode/tools/grep_components/search_result.py +34 -0
  81. tunacode/tools/list_dir.py +205 -0
  82. tunacode/tools/prompts/bash_prompt.xml +10 -0
  83. tunacode/tools/prompts/glob_prompt.xml +7 -0
  84. tunacode/tools/prompts/grep_prompt.xml +10 -0
  85. tunacode/tools/prompts/list_dir_prompt.xml +7 -0
  86. tunacode/tools/prompts/read_file_prompt.xml +9 -0
  87. tunacode/tools/prompts/todoclear_prompt.xml +12 -0
  88. tunacode/tools/prompts/todoread_prompt.xml +16 -0
  89. tunacode/tools/prompts/todowrite_prompt.xml +28 -0
  90. tunacode/tools/prompts/update_file_prompt.xml +9 -0
  91. tunacode/tools/prompts/web_fetch_prompt.xml +11 -0
  92. tunacode/tools/prompts/write_file_prompt.xml +7 -0
  93. tunacode/tools/react.py +111 -0
  94. tunacode/tools/read_file.py +68 -0
  95. tunacode/tools/todo.py +222 -0
  96. tunacode/tools/update_file.py +62 -0
  97. tunacode/tools/utils/__init__.py +1 -0
  98. tunacode/tools/utils/ripgrep.py +311 -0
  99. tunacode/tools/utils/text_match.py +352 -0
  100. tunacode/tools/web_fetch.py +245 -0
  101. tunacode/tools/write_file.py +34 -0
  102. tunacode/tools/xml_helper.py +34 -0
  103. tunacode/types/__init__.py +166 -0
  104. tunacode/types/base.py +94 -0
  105. tunacode/types/callbacks.py +53 -0
  106. tunacode/types/dataclasses.py +121 -0
  107. tunacode/types/pydantic_ai.py +31 -0
  108. tunacode/types/state.py +122 -0
  109. tunacode/ui/__init__.py +6 -0
  110. tunacode/ui/app.py +542 -0
  111. tunacode/ui/commands/__init__.py +430 -0
  112. tunacode/ui/components/__init__.py +1 -0
  113. tunacode/ui/headless/__init__.py +5 -0
  114. tunacode/ui/headless/output.py +72 -0
  115. tunacode/ui/main.py +252 -0
  116. tunacode/ui/renderers/__init__.py +41 -0
  117. tunacode/ui/renderers/errors.py +197 -0
  118. tunacode/ui/renderers/panels.py +550 -0
  119. tunacode/ui/renderers/search.py +314 -0
  120. tunacode/ui/renderers/tools/__init__.py +21 -0
  121. tunacode/ui/renderers/tools/bash.py +247 -0
  122. tunacode/ui/renderers/tools/diagnostics.py +186 -0
  123. tunacode/ui/renderers/tools/glob.py +226 -0
  124. tunacode/ui/renderers/tools/grep.py +228 -0
  125. tunacode/ui/renderers/tools/list_dir.py +198 -0
  126. tunacode/ui/renderers/tools/read_file.py +226 -0
  127. tunacode/ui/renderers/tools/research.py +294 -0
  128. tunacode/ui/renderers/tools/update_file.py +237 -0
  129. tunacode/ui/renderers/tools/web_fetch.py +182 -0
  130. tunacode/ui/repl_support.py +226 -0
  131. tunacode/ui/screens/__init__.py +16 -0
  132. tunacode/ui/screens/model_picker.py +303 -0
  133. tunacode/ui/screens/session_picker.py +181 -0
  134. tunacode/ui/screens/setup.py +218 -0
  135. tunacode/ui/screens/theme_picker.py +90 -0
  136. tunacode/ui/screens/update_confirm.py +69 -0
  137. tunacode/ui/shell_runner.py +129 -0
  138. tunacode/ui/styles/layout.tcss +98 -0
  139. tunacode/ui/styles/modals.tcss +38 -0
  140. tunacode/ui/styles/panels.tcss +81 -0
  141. tunacode/ui/styles/theme-nextstep.tcss +303 -0
  142. tunacode/ui/styles/widgets.tcss +33 -0
  143. tunacode/ui/styles.py +18 -0
  144. tunacode/ui/widgets/__init__.py +23 -0
  145. tunacode/ui/widgets/command_autocomplete.py +62 -0
  146. tunacode/ui/widgets/editor.py +402 -0
  147. tunacode/ui/widgets/file_autocomplete.py +47 -0
  148. tunacode/ui/widgets/messages.py +46 -0
  149. tunacode/ui/widgets/resource_bar.py +182 -0
  150. tunacode/ui/widgets/status_bar.py +98 -0
  151. tunacode/utils/__init__.py +0 -0
  152. tunacode/utils/config/__init__.py +13 -0
  153. tunacode/utils/config/user_configuration.py +91 -0
  154. tunacode/utils/messaging/__init__.py +10 -0
  155. tunacode/utils/messaging/message_utils.py +34 -0
  156. tunacode/utils/messaging/token_counter.py +77 -0
  157. tunacode/utils/parsing/__init__.py +13 -0
  158. tunacode/utils/parsing/command_parser.py +55 -0
  159. tunacode/utils/parsing/json_utils.py +188 -0
  160. tunacode/utils/parsing/retry.py +146 -0
  161. tunacode/utils/parsing/tool_parser.py +267 -0
  162. tunacode/utils/security/__init__.py +15 -0
  163. tunacode/utils/security/command.py +106 -0
  164. tunacode/utils/system/__init__.py +25 -0
  165. tunacode/utils/system/gitignore.py +155 -0
  166. tunacode/utils/system/paths.py +190 -0
  167. tunacode/utils/ui/__init__.py +9 -0
  168. tunacode/utils/ui/file_filter.py +135 -0
  169. tunacode/utils/ui/helpers.py +24 -0
  170. tunacode_cli-0.1.21.dist-info/METADATA +170 -0
  171. tunacode_cli-0.1.21.dist-info/RECORD +174 -0
  172. tunacode_cli-0.1.21.dist-info/WHEEL +4 -0
  173. tunacode_cli-0.1.21.dist-info/entry_points.txt +2 -0
  174. tunacode_cli-0.1.21.dist-info/licenses/LICENSE +21 -0
tunacode/ui/app.py ADDED
@@ -0,0 +1,542 @@
1
+ """Textual-based REPL shell - Application entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import os
7
+ import time
8
+ from datetime import UTC, datetime
9
+
10
+ from rich.markdown import Markdown
11
+ from rich.panel import Panel
12
+ from rich.syntax import Syntax
13
+ from rich.text import Text
14
+ from textual import events
15
+ from textual.app import App, ComposeResult
16
+ from textual.binding import Binding
17
+ from textual.containers import Container
18
+ from textual.widgets import LoadingIndicator, RichLog, Static
19
+
20
+ from tunacode.constants import (
21
+ RICHLOG_CLASS_PAUSED,
22
+ RICHLOG_CLASS_STREAMING,
23
+ TOOL_PANEL_WIDTH,
24
+ build_nextstep_theme,
25
+ build_tunacode_theme,
26
+ )
27
+ from tunacode.core.agents.main import process_request
28
+ from tunacode.indexing import CodeIndex
29
+ from tunacode.indexing.constants import QUICK_INDEX_THRESHOLD
30
+ from tunacode.types import (
31
+ ModelName,
32
+ StateManager,
33
+ ToolConfirmationRequest,
34
+ ToolConfirmationResponse,
35
+ )
36
+ from tunacode.ui.renderers.errors import render_exception
37
+ from tunacode.ui.renderers.panels import tool_panel_smart
38
+ from tunacode.ui.repl_support import (
39
+ PendingConfirmationState,
40
+ build_textual_tool_callback,
41
+ build_tool_progress_callback,
42
+ build_tool_result_callback,
43
+ build_tool_start_callback,
44
+ format_user_message,
45
+ )
46
+ from tunacode.ui.shell_runner import ShellRunner
47
+ from tunacode.ui.styles import (
48
+ STYLE_ERROR,
49
+ STYLE_HEADING,
50
+ STYLE_MUTED,
51
+ STYLE_PRIMARY,
52
+ STYLE_SUBHEADING,
53
+ STYLE_SUCCESS,
54
+ STYLE_WARNING,
55
+ )
56
+ from tunacode.ui.widgets import (
57
+ CommandAutoComplete,
58
+ Editor,
59
+ EditorSubmitRequested,
60
+ FileAutoComplete,
61
+ ResourceBar,
62
+ StatusBar,
63
+ ToolResultDisplay,
64
+ )
65
+
66
+ # Throttle streaming display updates to reduce visual churn
67
+ STREAM_THROTTLE_MS: float = 100.0
68
+
69
+
70
+ class TextualReplApp(App[None]):
71
+ TITLE = "TunaCode"
72
+ CSS_PATH = [
73
+ "styles/layout.tcss",
74
+ "styles/widgets.tcss",
75
+ "styles/modals.tcss",
76
+ "styles/panels.tcss",
77
+ "styles/theme-nextstep.tcss",
78
+ ]
79
+
80
+ BINDINGS = [
81
+ Binding("ctrl+p", "toggle_pause", "Pause/Resume Stream", show=False, priority=True),
82
+ Binding("escape", "cancel_stream", "Cancel", show=False, priority=True),
83
+ ]
84
+
85
+ def __init__(self, *, state_manager: StateManager, show_setup: bool = False) -> None:
86
+ super().__init__()
87
+ self.state_manager: StateManager = state_manager
88
+ self._show_setup: bool = show_setup
89
+ self.request_queue: asyncio.Queue[str] = asyncio.Queue()
90
+ self.pending_confirmation: PendingConfirmationState | None = None
91
+
92
+ self._streaming_paused: bool = False
93
+ self._streaming_cancelled: bool = False
94
+ self._stream_buffer: list[str] = []
95
+ self.current_stream_text: str = ""
96
+ self._current_request_task: asyncio.Task | None = None
97
+ self._loading_indicator_shown: bool = False
98
+ self._last_display_update: float = 0.0
99
+
100
+ self.shell_runner = ShellRunner(self)
101
+
102
+ self.rich_log: RichLog
103
+ self.editor: Editor
104
+ self.resource_bar: ResourceBar
105
+ self.status_bar: StatusBar
106
+ self.streaming_output: Static
107
+
108
+ def compose(self) -> ComposeResult:
109
+ self.resource_bar = ResourceBar()
110
+ self.rich_log = RichLog(wrap=True, markup=True, highlight=True, auto_scroll=True)
111
+ self.streaming_output = Static("", id="streaming-output")
112
+ self.loading_indicator = LoadingIndicator()
113
+ self.editor = Editor()
114
+ self.status_bar = StatusBar()
115
+
116
+ yield self.resource_bar
117
+ with Container(id="viewport"):
118
+ yield self.rich_log
119
+ yield self.streaming_output
120
+ yield self.loading_indicator
121
+ yield self.editor
122
+ yield FileAutoComplete(self.editor)
123
+ yield CommandAutoComplete(self.editor)
124
+ yield self.status_bar
125
+
126
+ def on_mount(self) -> None:
127
+ tunacode_theme = build_tunacode_theme()
128
+ self.register_theme(tunacode_theme)
129
+ nextstep_theme = build_nextstep_theme()
130
+ self.register_theme(nextstep_theme)
131
+
132
+ user_config = self.state_manager.session.user_config
133
+ saved_theme = user_config.get("settings", {}).get("theme", "dracula")
134
+ self.theme = saved_theme if saved_theme in self.available_themes else "dracula"
135
+
136
+ # Initialize session persistence metadata
137
+ from tunacode.utils.system.paths import get_project_id
138
+
139
+ session = self.state_manager.session
140
+ session.project_id = get_project_id()
141
+ session.working_directory = os.getcwd()
142
+ if not session.created_at:
143
+ session.created_at = datetime.now(UTC).isoformat()
144
+
145
+ if self._show_setup:
146
+ from tunacode.ui.screens import SetupScreen
147
+
148
+ self.push_screen(SetupScreen(self.state_manager), self._on_setup_complete)
149
+ else:
150
+ self._start_repl()
151
+
152
+ async def on_unmount(self) -> None:
153
+ """Save session before app exits."""
154
+ self.state_manager.save_session()
155
+
156
+ def watch_theme(self, old_theme: str, new_theme: str) -> None:
157
+ """Toggle CSS class when theme changes for theme-specific styling."""
158
+ if old_theme:
159
+ self.remove_class(f"theme-{old_theme}")
160
+ if new_theme:
161
+ self.add_class(f"theme-{new_theme}")
162
+
163
+ def _on_setup_complete(self, completed: bool) -> None:
164
+ """Called when setup screen is dismissed."""
165
+ if completed:
166
+ self._update_resource_bar()
167
+ self._start_repl()
168
+
169
+ def _start_repl(self) -> None:
170
+ """Initialize REPL components after setup."""
171
+ self.set_focus(self.editor)
172
+ self.run_worker(self._request_worker, exclusive=False)
173
+ self.run_worker(self._startup_index_worker, exclusive=False)
174
+ self._update_resource_bar()
175
+ self._show_welcome()
176
+
177
+ async def _startup_index_worker(self) -> None:
178
+ """Build startup index with dynamic sizing."""
179
+ import asyncio
180
+
181
+ def do_index() -> tuple[int, int | None, bool]:
182
+ """Returns (indexed_count, total_or_none, is_partial)."""
183
+ index = CodeIndex.get_instance()
184
+ total = index.quick_count()
185
+
186
+ if total < QUICK_INDEX_THRESHOLD:
187
+ index.build_index()
188
+ return len(index._all_files), None, False
189
+ else:
190
+ count = index.build_priority_index()
191
+ return count, total, True
192
+
193
+ loop = asyncio.get_event_loop()
194
+ indexed, total, is_partial = await loop.run_in_executor(None, do_index)
195
+
196
+ if is_partial:
197
+ msg = Text()
198
+ msg.append(
199
+ f"Code cache: {indexed}/{total} files indexed, expanding...",
200
+ style=STYLE_MUTED,
201
+ )
202
+ self.rich_log.write(msg)
203
+
204
+ # Expand in background
205
+ def do_expand() -> int:
206
+ index = CodeIndex.get_instance()
207
+ index.expand_index()
208
+ return len(index._all_files)
209
+
210
+ final_count = await loop.run_in_executor(None, do_expand)
211
+ done_msg = Text()
212
+ done_msg.append(f"Code cache built: {final_count} files indexed ✓", style=STYLE_SUCCESS)
213
+ self.rich_log.write(done_msg)
214
+ else:
215
+ msg = Text()
216
+ msg.append(f"Code cache built: {indexed} files indexed ✓", style=STYLE_SUCCESS)
217
+ self.rich_log.write(msg)
218
+
219
+ def _show_welcome(self) -> None:
220
+ welcome = Text()
221
+ welcome.append("Welcome to TunaCode\n", style=STYLE_HEADING)
222
+ welcome.append("AI coding assistant for your terminal.\n\n", style=STYLE_MUTED)
223
+ welcome.append("Commands:\n", style=STYLE_PRIMARY)
224
+ welcome.append(" /help - Show all commands\n", style="")
225
+ welcome.append(" /clear - Clear conversation\n", style="")
226
+ welcome.append(" /yolo - Toggle auto-confirm\n", style="")
227
+ welcome.append(" /branch - Create git branch\n", style="")
228
+ welcome.append(" /plan - Toggle planning mode\n", style="")
229
+ welcome.append(" /model - Switch model\n", style="")
230
+ welcome.append(" /theme - Switch theme\n", style="")
231
+ welcome.append(" /resume - Load saved session\n", style="")
232
+ welcome.append(" !<cmd> - Run shell command\n", style="")
233
+ self.rich_log.write(welcome)
234
+
235
+ async def _request_worker(self) -> None:
236
+ while True:
237
+ request = await self.request_queue.get()
238
+ try:
239
+ await self._process_request(request)
240
+ except Exception as e:
241
+ error_renderable = render_exception(e)
242
+ self.rich_log.write(error_renderable)
243
+ finally:
244
+ self.request_queue.task_done()
245
+
246
+ async def _process_request(self, message: str) -> None:
247
+ self.current_stream_text = ""
248
+ self._last_display_update = 0.0
249
+ self._streaming_cancelled = False
250
+ self.query_one("#viewport").add_class(RICHLOG_CLASS_STREAMING)
251
+
252
+ self._loading_indicator_shown = True
253
+ self.loading_indicator.add_class("active")
254
+
255
+ try:
256
+ model_name = self.state_manager.session.current_model or "openai/gpt-4o"
257
+
258
+ # Set progress callback on session for subagent progress tracking
259
+ self.state_manager.session.tool_progress_callback = build_tool_progress_callback(self)
260
+
261
+ self._current_request_task = asyncio.create_task(
262
+ process_request(
263
+ message=message,
264
+ model=ModelName(model_name),
265
+ state_manager=self.state_manager,
266
+ tool_callback=build_textual_tool_callback(self, self.state_manager),
267
+ streaming_callback=self.streaming_callback,
268
+ tool_result_callback=build_tool_result_callback(self),
269
+ tool_start_callback=build_tool_start_callback(self),
270
+ )
271
+ )
272
+ await self._current_request_task
273
+ except asyncio.CancelledError:
274
+ from tunacode.core.agents.agent_components import patch_tool_messages
275
+
276
+ patch_tool_messages(
277
+ "Operation cancelled by user",
278
+ state_manager=self.state_manager,
279
+ )
280
+ self.notify("Cancelled")
281
+ except Exception as e:
282
+ from tunacode.core.agents.agent_components import patch_tool_messages
283
+
284
+ patch_tool_messages(
285
+ f"Request failed: {type(e).__name__}",
286
+ state_manager=self.state_manager,
287
+ )
288
+ error_renderable = render_exception(e)
289
+ self.rich_log.write(error_renderable)
290
+ finally:
291
+ self._current_request_task = None
292
+ self._loading_indicator_shown = False
293
+ self.loading_indicator.remove_class("active")
294
+ self.query_one("#viewport").remove_class(RICHLOG_CLASS_STREAMING)
295
+ self.query_one("#viewport").remove_class(RICHLOG_CLASS_PAUSED)
296
+ self.streaming_output.update("")
297
+ self.streaming_output.remove_class("active")
298
+
299
+ if self.current_stream_text and not self._streaming_cancelled:
300
+ self.rich_log.write("")
301
+ self.rich_log.write(Text("agent:", style="accent"))
302
+ self.rich_log.write(Markdown(self.current_stream_text))
303
+
304
+ self.current_stream_text = ""
305
+ self._streaming_cancelled = False
306
+ self._update_resource_bar()
307
+
308
+ # Auto-save session after processing
309
+ self.state_manager.save_session()
310
+
311
+ async def on_editor_submit_requested(self, message: EditorSubmitRequested) -> None:
312
+ from tunacode.ui.commands import handle_command
313
+
314
+ if await handle_command(self, message.text):
315
+ return
316
+
317
+ await self.request_queue.put(message.text)
318
+
319
+ from datetime import datetime
320
+
321
+ timestamp = datetime.now().strftime("%I:%M %p").lstrip("0")
322
+
323
+ self.rich_log.write("")
324
+ render_width = max(1, self.rich_log.size.width - 2)
325
+
326
+ user_block = format_user_message(message.text, STYLE_PRIMARY, width=render_width)
327
+
328
+ user_block.append(f"│ you {timestamp}", style=f"dim {STYLE_PRIMARY}")
329
+ self.rich_log.write(user_block)
330
+
331
+ async def request_tool_confirmation(
332
+ self, request: ToolConfirmationRequest
333
+ ) -> ToolConfirmationResponse:
334
+ if self.pending_confirmation is not None and not self.pending_confirmation.future.done():
335
+ raise RuntimeError("Previous confirmation still pending")
336
+
337
+ future: asyncio.Future[ToolConfirmationResponse] = asyncio.Future()
338
+ self.pending_confirmation = PendingConfirmationState(future=future, request=request)
339
+ self._show_inline_confirmation(request)
340
+ return await future
341
+
342
+ def on_tool_result_display(self, message: ToolResultDisplay) -> None:
343
+ panel = tool_panel_smart(
344
+ name=message.tool_name,
345
+ status=message.status,
346
+ args=message.args,
347
+ result=message.result,
348
+ duration_ms=message.duration_ms,
349
+ )
350
+ self.rich_log.write(panel)
351
+
352
+ def _replay_session_messages(self) -> None:
353
+ """Render loaded session messages to RichLog."""
354
+ from pydantic_ai.messages import ModelRequest, ModelResponse
355
+
356
+ from tunacode.utils.messaging.message_utils import get_message_content
357
+
358
+ for msg in self.state_manager.session.messages:
359
+ if isinstance(msg, dict) and "thought" in msg:
360
+ continue # Skip internal thoughts
361
+
362
+ content = get_message_content(msg)
363
+ if not content:
364
+ continue
365
+
366
+ if isinstance(msg, ModelRequest):
367
+ user_block = Text()
368
+ user_block.append(f"| {content}\n", style=STYLE_PRIMARY)
369
+ user_block.append("| (restored)", style=f"dim {STYLE_PRIMARY}")
370
+ self.rich_log.write(user_block)
371
+ elif isinstance(msg, ModelResponse):
372
+ self.rich_log.write(Text("agent:", style="accent"))
373
+ self.rich_log.write(Markdown(content))
374
+
375
+ async def streaming_callback(self, chunk: str) -> None:
376
+ if self._streaming_paused:
377
+ self._stream_buffer.append(chunk)
378
+ return
379
+
380
+ # Always accumulate immediately
381
+ self.current_stream_text += chunk
382
+
383
+ # Throttle display updates to reduce visual churn
384
+ now = time.monotonic()
385
+ elapsed_ms = (now - self._last_display_update) * 1000
386
+
387
+ if elapsed_ms >= STREAM_THROTTLE_MS:
388
+ self._last_display_update = now
389
+ self.streaming_output.update(Markdown(self.current_stream_text))
390
+ self.streaming_output.add_class("active")
391
+ self.rich_log.scroll_end()
392
+
393
+ def action_toggle_pause(self) -> None:
394
+ if self._streaming_paused:
395
+ self.resume_streaming()
396
+ else:
397
+ self.pause_streaming()
398
+
399
+ def pause_streaming(self) -> None:
400
+ self._streaming_paused = True
401
+ self.query_one("#viewport").add_class(RICHLOG_CLASS_PAUSED)
402
+ self.notify("Streaming paused...")
403
+
404
+ def resume_streaming(self) -> None:
405
+ self._streaming_paused = False
406
+ self.query_one("#viewport").remove_class(RICHLOG_CLASS_PAUSED)
407
+ self.notify("Streaming resumed...")
408
+
409
+ if self._stream_buffer:
410
+ buffered_text = "".join(self._stream_buffer)
411
+ self.current_stream_text += buffered_text
412
+ self._stream_buffer.clear()
413
+
414
+ # Force immediate display update on resume
415
+ self._last_display_update = time.monotonic()
416
+ self.streaming_output.update(Markdown(self.current_stream_text))
417
+
418
+ def action_cancel_stream(self) -> None:
419
+ # If confirmation is pending, Escape rejects it
420
+ if self.pending_confirmation is not None and not self.pending_confirmation.future.done():
421
+ response = ToolConfirmationResponse(approved=False, skip_future=False, abort=True)
422
+ self.pending_confirmation.future.set_result(response)
423
+ self.pending_confirmation = None
424
+ self.rich_log.write(Text("Rejected", style=STYLE_ERROR))
425
+ return
426
+
427
+ # Otherwise, cancel the stream
428
+ if self._current_request_task is not None:
429
+ self._streaming_cancelled = True
430
+ self._stream_buffer.clear()
431
+ self.current_stream_text = ""
432
+ self._current_request_task.cancel()
433
+ return
434
+
435
+ shell_runner = getattr(self, "shell_runner", None)
436
+ if shell_runner is not None and shell_runner.is_running():
437
+ shell_runner.cancel()
438
+ return
439
+
440
+ if self.editor.value or self.editor.has_paste_buffer:
441
+ self.editor.clear_input()
442
+ return
443
+
444
+ return
445
+
446
+ def start_shell_command(self, raw_cmd: str) -> None:
447
+ self.shell_runner.start(raw_cmd)
448
+
449
+ def write_shell_output(self, renderable: Text) -> None:
450
+ self.rich_log.write(renderable)
451
+
452
+ def shell_status_running(self) -> None:
453
+ self.status_bar.update_running_action("shell")
454
+
455
+ def shell_status_last(self) -> None:
456
+ self.status_bar.update_last_action("shell")
457
+
458
+ def _update_resource_bar(self) -> None:
459
+ session = self.state_manager.session
460
+ usage = session.session_total_usage
461
+
462
+ # Use actual context window tokens, not cumulative API usage
463
+ context_tokens = session.total_tokens
464
+
465
+ self.resource_bar.update_stats(
466
+ model=session.current_model or "No model selected",
467
+ tokens=context_tokens,
468
+ max_tokens=session.max_tokens or 200000,
469
+ session_cost=usage.get("cost", 0.0),
470
+ )
471
+
472
+ def _show_inline_confirmation(self, request: ToolConfirmationRequest) -> None:
473
+ """Display inline confirmation prompt in RichLog."""
474
+ content_parts: list[Text | Syntax] = []
475
+
476
+ # Header
477
+ header = Text()
478
+ header.append(f"Confirm: {request.tool_name}\n", style=STYLE_SUBHEADING)
479
+ content_parts.append(header)
480
+
481
+ # Arguments
482
+ args_text = Text()
483
+ for key, value in request.args.items():
484
+ display_value = str(value)
485
+ if len(display_value) > 60:
486
+ display_value = display_value[:57] + "..."
487
+ args_text.append(f" {key}: ", style=STYLE_MUTED)
488
+ args_text.append(f"{display_value}\n")
489
+ content_parts.append(args_text)
490
+
491
+ # Diff Preview (if available)
492
+ if request.diff_content:
493
+ content_parts.append(Text("\nPreview changes:\n", style="bold"))
494
+ content_parts.append(
495
+ Syntax(request.diff_content, "diff", theme="monokai", word_wrap=True)
496
+ )
497
+ content_parts.append(Text("\n"))
498
+
499
+ # Footer Actions
500
+ actions = Text()
501
+ actions.append("\n")
502
+ actions.append("[1]", style=f"bold {STYLE_SUCCESS}")
503
+ actions.append(" Yes ")
504
+ actions.append("[2]", style=f"bold {STYLE_WARNING}")
505
+ actions.append(" Yes + Skip ")
506
+ actions.append("[3]", style=f"bold {STYLE_ERROR}")
507
+ actions.append(" No")
508
+ content_parts.append(actions)
509
+
510
+ # Use Group to stack components vertically
511
+ from rich.console import Group
512
+
513
+ panel = Panel(
514
+ Group(*content_parts),
515
+ border_style=STYLE_PRIMARY,
516
+ padding=(0, 1),
517
+ expand=True,
518
+ width=TOOL_PANEL_WIDTH,
519
+ )
520
+ self.rich_log.write(panel)
521
+
522
+ def on_key(self, event: events.Key) -> None:
523
+ """Handle key events, intercepting confirmation keys when pending."""
524
+ if self.pending_confirmation is None or self.pending_confirmation.future.done():
525
+ return
526
+
527
+ response: ToolConfirmationResponse | None = None
528
+
529
+ if event.key == "1":
530
+ response = ToolConfirmationResponse(approved=True, skip_future=False, abort=False)
531
+ self.rich_log.write(Text("Approved", style=STYLE_SUCCESS))
532
+ elif event.key == "2":
533
+ response = ToolConfirmationResponse(approved=True, skip_future=True, abort=False)
534
+ self.rich_log.write(Text("Approved (skipping future)", style=STYLE_WARNING))
535
+ elif event.key == "3":
536
+ response = ToolConfirmationResponse(approved=False, skip_future=False, abort=True)
537
+ self.rich_log.write(Text("Rejected", style=STYLE_ERROR))
538
+
539
+ if response is not None:
540
+ self.pending_confirmation.future.set_result(response)
541
+ self.pending_confirmation = None
542
+ event.stop()