voidx 1.0.0__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.
- voidx/__init__.py +3 -0
- voidx/agent/__init__.py +0 -0
- voidx/agent/agents.py +439 -0
- voidx/agent/attachments.py +235 -0
- voidx/agent/graph.py +463 -0
- voidx/agent/graph_components/__init__.py +1 -0
- voidx/agent/graph_components/compaction.py +268 -0
- voidx/agent/graph_components/permissions.py +139 -0
- voidx/agent/graph_components/run_loop.py +532 -0
- voidx/agent/graph_components/runtime.py +14 -0
- voidx/agent/graph_components/streaming.py +351 -0
- voidx/agent/graph_components/subagent.py +278 -0
- voidx/agent/graph_components/tool_execution.py +208 -0
- voidx/agent/runtime_context.py +368 -0
- voidx/agent/slash.py +466 -0
- voidx/agent/slash_components/__init__.py +1 -0
- voidx/agent/slash_components/code_ide.py +68 -0
- voidx/agent/slash_components/lsp.py +105 -0
- voidx/agent/slash_components/mcp.py +332 -0
- voidx/agent/slash_components/model.py +419 -0
- voidx/agent/slash_components/runtime.py +55 -0
- voidx/agent/slash_components/skills.py +94 -0
- voidx/agent/state.py +32 -0
- voidx/agent/task_state.py +278 -0
- voidx/agent/tool_filters.py +27 -0
- voidx/config.py +707 -0
- voidx/llm/__init__.py +0 -0
- voidx/llm/catalog.py +188 -0
- voidx/llm/compaction.py +267 -0
- voidx/llm/context.py +43 -0
- voidx/llm/instruction.py +220 -0
- voidx/llm/provider.py +312 -0
- voidx/llm/usage.py +341 -0
- voidx/lsp/__init__.py +30 -0
- voidx/lsp/client.py +259 -0
- voidx/lsp/config.py +172 -0
- voidx/lsp/detector.py +512 -0
- voidx/lsp/errors.py +19 -0
- voidx/lsp/manager.py +280 -0
- voidx/lsp/schema.py +179 -0
- voidx/lsp/service.py +103 -0
- voidx/main.py +154 -0
- voidx/mcp/__init__.py +33 -0
- voidx/mcp/client.py +458 -0
- voidx/mcp/manager.py +267 -0
- voidx/mcp/schema.py +112 -0
- voidx/mcp/tool.py +122 -0
- voidx/mcp_servers/__init__.py +1 -0
- voidx/mcp_servers/web.py +104 -0
- voidx/memory/__init__.py +0 -0
- voidx/memory/context_frames.py +188 -0
- voidx/memory/model_profiles.py +98 -0
- voidx/memory/runtime_state.py +240 -0
- voidx/memory/session.py +272 -0
- voidx/memory/store.py +245 -0
- voidx/memory/transcript.py +137 -0
- voidx/permission/__init__.py +28 -0
- voidx/permission/engine.py +430 -0
- voidx/permission/evaluate.py +114 -0
- voidx/permission/sandbox.py +280 -0
- voidx/permission/schema.py +24 -0
- voidx/permission/service.py +314 -0
- voidx/permission/wildcard.py +34 -0
- voidx/skills/__init__.py +18 -0
- voidx/skills/bundled/superpowers/receiving-code-review/SKILL.md +30 -0
- voidx/skills/bundled/superpowers/requesting-code-review/SKILL.md +27 -0
- voidx/skills/bundled/superpowers/systematic-debugging/SKILL.md +36 -0
- voidx/skills/bundled/superpowers/test-driven-development/SKILL.md +33 -0
- voidx/skills/bundled/superpowers/verification-before-completion/SKILL.md +31 -0
- voidx/skills/bundled/superpowers/writing-plans/SKILL.md +27 -0
- voidx/skills/policy.py +97 -0
- voidx/skills/registry.py +162 -0
- voidx/skills/schema.py +47 -0
- voidx/skills/service.py +199 -0
- voidx/tools/__init__.py +0 -0
- voidx/tools/agent.py +81 -0
- voidx/tools/base.py +86 -0
- voidx/tools/bash.py +105 -0
- voidx/tools/file_ops.py +193 -0
- voidx/tools/lsp.py +155 -0
- voidx/tools/registry.py +104 -0
- voidx/tools/repomap.py +238 -0
- voidx/tools/search.py +162 -0
- voidx/tools/task_status.py +57 -0
- voidx/tools/task_tracker.py +81 -0
- voidx/tools/todo.py +82 -0
- voidx/tools/web_content.py +357 -0
- voidx/tools/web_mcp.py +107 -0
- voidx/tools/webfetch.py +155 -0
- voidx/tools/websearch.py +276 -0
- voidx/ui/__init__.py +0 -0
- voidx/ui/app.py +1033 -0
- voidx/ui/app_components/__init__.py +1 -0
- voidx/ui/app_components/clipboard_image.py +245 -0
- voidx/ui/app_components/commands.py +18 -0
- voidx/ui/app_components/controls.py +29 -0
- voidx/ui/app_components/file_picker.py +115 -0
- voidx/ui/app_components/formatting.py +187 -0
- voidx/ui/app_components/git_changes.py +51 -0
- voidx/ui/app_components/rendering.py +1169 -0
- voidx/ui/browse.py +160 -0
- voidx/ui/capture.py +169 -0
- voidx/ui/code_ide.py +251 -0
- voidx/ui/commands.py +83 -0
- voidx/ui/console.py +381 -0
- voidx/ui/console_components/__init__.py +1 -0
- voidx/ui/console_components/formatting.py +96 -0
- voidx/ui/console_components/streaming.py +253 -0
- voidx/ui/diff.py +331 -0
- voidx/ui/dock.py +372 -0
- voidx/ui/dock_components/__init__.py +1 -0
- voidx/ui/dock_components/formatting.py +123 -0
- voidx/ui/dock_components/nodes.py +401 -0
- voidx/ui/dock_components/state.py +51 -0
- voidx/ui/event_components/__init__.py +1 -0
- voidx/ui/event_components/schema.py +249 -0
- voidx/ui/events.py +341 -0
- voidx/ui/session_changes.py +163 -0
- voidx/ui/startup.py +161 -0
- voidx/ui/transcript.py +148 -0
- voidx/ui/tree.py +316 -0
- voidx-1.0.0.dist-info/METADATA +59 -0
- voidx-1.0.0.dist-info/RECORD +126 -0
- voidx-1.0.0.dist-info/WHEEL +5 -0
- voidx-1.0.0.dist-info/entry_points.txt +2 -0
- voidx-1.0.0.dist-info/top_level.txt +1 -0
voidx/ui/app.py
ADDED
|
@@ -0,0 +1,1033 @@
|
|
|
1
|
+
"""prompt_toolkit based full-screen UI for voidx."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any, Awaitable, Callable
|
|
11
|
+
|
|
12
|
+
from prompt_toolkit.application import Application
|
|
13
|
+
from prompt_toolkit.application.current import get_app_or_none
|
|
14
|
+
from prompt_toolkit.clipboard import ClipboardData
|
|
15
|
+
from prompt_toolkit.data_structures import Point
|
|
16
|
+
from prompt_toolkit.filters import Condition
|
|
17
|
+
from prompt_toolkit.formatted_text import AnyFormattedText
|
|
18
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
19
|
+
from prompt_toolkit.layout import HSplit, Layout, VSplit, Window
|
|
20
|
+
from prompt_toolkit.layout.containers import ConditionalContainer, Float, FloatContainer
|
|
21
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
22
|
+
from prompt_toolkit.layout.dimension import Dimension
|
|
23
|
+
from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
|
|
24
|
+
from prompt_toolkit.selection import SelectionType
|
|
25
|
+
from prompt_toolkit.styles import Style
|
|
26
|
+
from prompt_toolkit.widgets import TextArea
|
|
27
|
+
|
|
28
|
+
from voidx.ui.app_components.commands import SlashCommandCompleter
|
|
29
|
+
from voidx.ui.app_components.controls import TranscriptControl, TranscriptScrollbarMargin
|
|
30
|
+
from voidx.ui.app_components.formatting import (
|
|
31
|
+
_args_preview,
|
|
32
|
+
_clip,
|
|
33
|
+
_continuation_prefix,
|
|
34
|
+
_friendly_choice_label,
|
|
35
|
+
_get_ansi_console,
|
|
36
|
+
_lines_to_formatted_text,
|
|
37
|
+
_mcp_status_label,
|
|
38
|
+
_permission_target,
|
|
39
|
+
_rich_to_ansi,
|
|
40
|
+
_visible_text,
|
|
41
|
+
)
|
|
42
|
+
from voidx.ui.app_components.file_picker import attachment_token_text
|
|
43
|
+
from voidx.ui.app_components.clipboard_image import (
|
|
44
|
+
ClipboardImageResult,
|
|
45
|
+
paste_clipboard_image as paste_clipboard_image_from_system,
|
|
46
|
+
)
|
|
47
|
+
from voidx.ui.app_components.rendering import PromptToolkitRenderMixin
|
|
48
|
+
from voidx.ui.dock import dock
|
|
49
|
+
from voidx.ui.dock_components.formatting import _ansi_line, _strip_ansi_trailing_space
|
|
50
|
+
from voidx.ui.events import ErrorAppended, ui_events
|
|
51
|
+
from voidx.ui.session_changes import session_tracker
|
|
52
|
+
from voidx.llm.usage import UsageStats
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
SubmitHandler = Callable[[str], Awaitable[bool]]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class McpServerStatus:
|
|
60
|
+
name: str
|
|
61
|
+
status: str = "configured"
|
|
62
|
+
tool_count: int = 0
|
|
63
|
+
source: str = "Project MCPs"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class UiStatus:
|
|
68
|
+
provider: str
|
|
69
|
+
model: str
|
|
70
|
+
workspace: str
|
|
71
|
+
session_title: str
|
|
72
|
+
context_limit: int
|
|
73
|
+
debug: Callable[[], bool]
|
|
74
|
+
plan_mode: Callable[[], bool]
|
|
75
|
+
interaction_mode: Callable[[], str] = field(default_factory=lambda: lambda: "auto")
|
|
76
|
+
goal_label: Callable[[], str] = field(default_factory=lambda: lambda: "")
|
|
77
|
+
goal_phase: Callable[[], str] = field(default_factory=lambda: lambda: "clarify")
|
|
78
|
+
goal_status: Callable[[], str] = field(default_factory=lambda: lambda: "idle")
|
|
79
|
+
goal_turn_count: Callable[[], int] = field(default_factory=lambda: lambda: 0)
|
|
80
|
+
goal_awaiting_approval: Callable[[], bool] = field(default_factory=lambda: lambda: False)
|
|
81
|
+
reasoning_effort: str = "xhigh"
|
|
82
|
+
permission_label: Callable[[], str] = field(default_factory=lambda: lambda: "default")
|
|
83
|
+
sandbox_label: Callable[[], str] = field(default_factory=lambda: lambda: "w-write")
|
|
84
|
+
approval_label: Callable[[], str] = field(default_factory=lambda: lambda: "on-fail")
|
|
85
|
+
approval_reviewer_label: Callable[[], str] = field(default_factory=lambda: lambda: "user")
|
|
86
|
+
usage_stats: UsageStats = field(default_factory=UsageStats)
|
|
87
|
+
mcp_servers: Callable[[], list[McpServerStatus]] = field(default_factory=lambda: lambda: [])
|
|
88
|
+
mcp_config_path: str = ""
|
|
89
|
+
code_ide: Callable[[], str] = field(default_factory=lambda: lambda: "trae")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class PromptToolkitTui(PromptToolkitRenderMixin):
|
|
93
|
+
"""Scrollable transcript with a fixed bottom input."""
|
|
94
|
+
|
|
95
|
+
COMMAND_OUTPUT_TTL_SECONDS = 5.0
|
|
96
|
+
|
|
97
|
+
def __init__(self, status: UiStatus, commands: list[tuple[str, str]]) -> None:
|
|
98
|
+
self.status = status
|
|
99
|
+
self.commands = commands
|
|
100
|
+
self._queue: asyncio.Queue[str | None] = asyncio.Queue()
|
|
101
|
+
self._quiet_commands: list[str] = []
|
|
102
|
+
self._choice_queue: asyncio.Queue[str | None] = asyncio.Queue()
|
|
103
|
+
self._text_queue: asyncio.Queue[str | None] = asyncio.Queue()
|
|
104
|
+
self._active_choice: list[tuple[str, str, str]] | None = None
|
|
105
|
+
self._active_text_prompt: str | None = None
|
|
106
|
+
self._active_text_default = ""
|
|
107
|
+
self._active_text_secret = False
|
|
108
|
+
self._saved_input_text = ""
|
|
109
|
+
self._saved_input_cursor = 0
|
|
110
|
+
self._choice_prompt: str = ""
|
|
111
|
+
self._choice_selected: int = 0
|
|
112
|
+
self._choice_details: list[dict[str, Any]] = []
|
|
113
|
+
self._choice_anchor = ""
|
|
114
|
+
self._choice_current_value: str = ""
|
|
115
|
+
self._footer_anchor_positions: dict[str, int] = {}
|
|
116
|
+
self._scroll_offset = 0
|
|
117
|
+
self._busy = False
|
|
118
|
+
self._last_error = ""
|
|
119
|
+
self._notice = ""
|
|
120
|
+
self._ctrl_c_armed = False
|
|
121
|
+
self._ctrl_c_deadline = 0.0
|
|
122
|
+
self._exit_requested = False
|
|
123
|
+
self._visible_body_lines: list[str] = []
|
|
124
|
+
self._visible_body_node_ids: list[str | None] = []
|
|
125
|
+
self._visible_row_to_node: dict[int, str | None] = {}
|
|
126
|
+
self._last_body_click: Point | None = None
|
|
127
|
+
self._body_mouse_down: Point | None = None
|
|
128
|
+
self._command_selected = 0
|
|
129
|
+
self._command_panel_suppressed_text = ""
|
|
130
|
+
self._attachment_selected = 0
|
|
131
|
+
self._attachment_panel_suppressed_text = ""
|
|
132
|
+
self._command_output_title = ""
|
|
133
|
+
self._command_output_lines: list[str] = []
|
|
134
|
+
self._command_output_visible = False
|
|
135
|
+
self._command_output_clear_handle: asyncio.TimerHandle | None = None
|
|
136
|
+
self._input_history: list[str] = []
|
|
137
|
+
self._input_history_index: int | None = None
|
|
138
|
+
self._input_history_draft = ""
|
|
139
|
+
self._loading_input_history = False
|
|
140
|
+
self._review_active = False
|
|
141
|
+
self._current_submit_task: asyncio.Task[bool] | None = None
|
|
142
|
+
self._current_submitted_text = ""
|
|
143
|
+
self._submit_cancel_requested = False
|
|
144
|
+
|
|
145
|
+
self.input = TextArea(
|
|
146
|
+
height=Dimension(min=3, preferred=3, max=3),
|
|
147
|
+
multiline=True,
|
|
148
|
+
password=Condition(lambda: self._active_text_secret),
|
|
149
|
+
wrap_lines=True,
|
|
150
|
+
prompt=self._input_prompt,
|
|
151
|
+
style="class:input",
|
|
152
|
+
)
|
|
153
|
+
self.input.buffer.on_text_changed += self._on_input_changed
|
|
154
|
+
self.input.control.mouse_handler = self._ignore_input_mouse
|
|
155
|
+
self.body_control = TranscriptControl(self)
|
|
156
|
+
|
|
157
|
+
kb = KeyBindings()
|
|
158
|
+
|
|
159
|
+
choice_mode = Condition(lambda: self._active_choice is not None)
|
|
160
|
+
text_mode = Condition(lambda: self._active_text_prompt is not None)
|
|
161
|
+
command_mode = Condition(lambda: self._command_panel_active())
|
|
162
|
+
attachment_mode = Condition(lambda: self._attachment_panel_active())
|
|
163
|
+
command_output_mode = Condition(lambda: self._command_output_active())
|
|
164
|
+
compact_choice_mode = Condition(
|
|
165
|
+
lambda: self._active_choice is not None
|
|
166
|
+
)
|
|
167
|
+
has_choice_details = Condition(lambda: bool(self._choice_details))
|
|
168
|
+
footer_choice_mode = compact_choice_mode & ~has_choice_details
|
|
169
|
+
permission_choice_mode = compact_choice_mode & has_choice_details
|
|
170
|
+
has_changes = Condition(lambda: session_tracker.has_changes)
|
|
171
|
+
review_mode = Condition(lambda: self._review_active)
|
|
172
|
+
|
|
173
|
+
@kb.add("escape", filter=choice_mode)
|
|
174
|
+
def _(event) -> None:
|
|
175
|
+
self._finish_choice(None)
|
|
176
|
+
event.app.invalidate()
|
|
177
|
+
|
|
178
|
+
@kb.add("enter", filter=choice_mode)
|
|
179
|
+
def _(event) -> None:
|
|
180
|
+
self._submit_choice_selection()
|
|
181
|
+
event.app.invalidate()
|
|
182
|
+
|
|
183
|
+
@kb.add("up", filter=choice_mode)
|
|
184
|
+
def _(event) -> None:
|
|
185
|
+
self._move_choice_selection(-1)
|
|
186
|
+
event.app.invalidate()
|
|
187
|
+
|
|
188
|
+
@kb.add("down", filter=choice_mode)
|
|
189
|
+
def _(event) -> None:
|
|
190
|
+
self._move_choice_selection(1)
|
|
191
|
+
event.app.invalidate()
|
|
192
|
+
|
|
193
|
+
@kb.add("<any>", filter=choice_mode)
|
|
194
|
+
def _(event) -> None:
|
|
195
|
+
char = event.key_sequence[0].data
|
|
196
|
+
quick_keys = {value: value for _, value, _ in self._active_choice or [] if len(value) == 1}
|
|
197
|
+
if char in quick_keys:
|
|
198
|
+
self._finish_choice(quick_keys[char])
|
|
199
|
+
event.app.invalidate()
|
|
200
|
+
|
|
201
|
+
@kb.add("enter", filter=text_mode)
|
|
202
|
+
def _(event) -> None:
|
|
203
|
+
self._submit_text_prompt()
|
|
204
|
+
event.app.invalidate()
|
|
205
|
+
|
|
206
|
+
@kb.add("escape", filter=text_mode)
|
|
207
|
+
def _(event) -> None:
|
|
208
|
+
self._cancel_text_prompt()
|
|
209
|
+
event.app.invalidate()
|
|
210
|
+
|
|
211
|
+
@kb.add("enter", filter=command_mode)
|
|
212
|
+
def _(event) -> None:
|
|
213
|
+
if self._accept_command_panel_selection():
|
|
214
|
+
event.app.invalidate()
|
|
215
|
+
return
|
|
216
|
+
self._submit_input()
|
|
217
|
+
event.app.invalidate()
|
|
218
|
+
|
|
219
|
+
@kb.add("enter", filter=attachment_mode)
|
|
220
|
+
def _(event) -> None:
|
|
221
|
+
if self._accept_attachment_panel_selection():
|
|
222
|
+
event.app.invalidate()
|
|
223
|
+
return
|
|
224
|
+
self._submit_input()
|
|
225
|
+
event.app.invalidate()
|
|
226
|
+
|
|
227
|
+
@kb.add("up", filter=command_mode)
|
|
228
|
+
def _(event) -> None:
|
|
229
|
+
self._move_command_selection_visual(-1)
|
|
230
|
+
event.app.invalidate()
|
|
231
|
+
|
|
232
|
+
@kb.add("up", filter=attachment_mode)
|
|
233
|
+
def _(event) -> None:
|
|
234
|
+
self._move_attachment_selection(-1)
|
|
235
|
+
event.app.invalidate()
|
|
236
|
+
|
|
237
|
+
@kb.add("down", filter=command_mode)
|
|
238
|
+
def _(event) -> None:
|
|
239
|
+
self._move_command_selection_visual(1)
|
|
240
|
+
event.app.invalidate()
|
|
241
|
+
|
|
242
|
+
@kb.add("down", filter=attachment_mode)
|
|
243
|
+
def _(event) -> None:
|
|
244
|
+
self._move_attachment_selection(1)
|
|
245
|
+
event.app.invalidate()
|
|
246
|
+
|
|
247
|
+
@kb.add("escape", filter=command_mode)
|
|
248
|
+
def _(event) -> None:
|
|
249
|
+
self._command_panel_suppressed_text = self.input.text
|
|
250
|
+
event.app.invalidate()
|
|
251
|
+
|
|
252
|
+
@kb.add("escape", filter=attachment_mode)
|
|
253
|
+
def _(event) -> None:
|
|
254
|
+
self._attachment_panel_suppressed_text = self.input.text
|
|
255
|
+
event.app.invalidate()
|
|
256
|
+
|
|
257
|
+
@kb.add("escape", filter=command_output_mode & ~choice_mode & ~command_mode & ~attachment_mode)
|
|
258
|
+
def _(event) -> None:
|
|
259
|
+
self.hide_command_output()
|
|
260
|
+
event.app.invalidate()
|
|
261
|
+
|
|
262
|
+
@kb.add("escape", filter=review_mode)
|
|
263
|
+
def _(event) -> None:
|
|
264
|
+
self._review_active = False
|
|
265
|
+
event.app.invalidate()
|
|
266
|
+
|
|
267
|
+
@kb.add("enter", filter=~choice_mode & ~text_mode & ~command_mode & ~attachment_mode & ~review_mode)
|
|
268
|
+
def _(event) -> None:
|
|
269
|
+
self._submit_input()
|
|
270
|
+
event.app.invalidate()
|
|
271
|
+
|
|
272
|
+
@kb.add("escape", "enter", filter=~choice_mode & ~text_mode)
|
|
273
|
+
def _(event) -> None:
|
|
274
|
+
self._insert_input_newline()
|
|
275
|
+
event.app.invalidate()
|
|
276
|
+
|
|
277
|
+
@kb.add("c-j")
|
|
278
|
+
def _(event) -> None:
|
|
279
|
+
self._insert_input_newline()
|
|
280
|
+
event.app.invalidate()
|
|
281
|
+
|
|
282
|
+
@kb.add("s-left", filter=~choice_mode)
|
|
283
|
+
def _(event) -> None:
|
|
284
|
+
self._extend_input_selection(-1)
|
|
285
|
+
event.app.invalidate()
|
|
286
|
+
|
|
287
|
+
@kb.add("s-right", filter=~choice_mode)
|
|
288
|
+
def _(event) -> None:
|
|
289
|
+
self._extend_input_selection(1)
|
|
290
|
+
event.app.invalidate()
|
|
291
|
+
|
|
292
|
+
@kb.add("up", filter=~choice_mode & ~text_mode & ~command_mode & ~attachment_mode)
|
|
293
|
+
def _(event) -> None:
|
|
294
|
+
self._previous_input_history()
|
|
295
|
+
event.app.invalidate()
|
|
296
|
+
|
|
297
|
+
@kb.add("down", filter=~choice_mode & ~text_mode & ~command_mode & ~attachment_mode)
|
|
298
|
+
def _(event) -> None:
|
|
299
|
+
self._next_input_history()
|
|
300
|
+
event.app.invalidate()
|
|
301
|
+
|
|
302
|
+
@kb.add("c-c")
|
|
303
|
+
def _(event) -> None:
|
|
304
|
+
if self._copy_input_selection(event):
|
|
305
|
+
event.app.invalidate()
|
|
306
|
+
return
|
|
307
|
+
self._handle_ctrl_c()
|
|
308
|
+
event.app.invalidate()
|
|
309
|
+
|
|
310
|
+
@kb.add("escape", "c", filter=~choice_mode)
|
|
311
|
+
def _(event) -> None:
|
|
312
|
+
self._copy_input_selection(event)
|
|
313
|
+
event.app.invalidate()
|
|
314
|
+
|
|
315
|
+
@kb.add("c-d")
|
|
316
|
+
def _(event) -> None:
|
|
317
|
+
if not self.input.text:
|
|
318
|
+
self._request_exit()
|
|
319
|
+
|
|
320
|
+
@kb.add("c-v", filter=~choice_mode)
|
|
321
|
+
def _(event) -> None:
|
|
322
|
+
result = self.paste_clipboard_image(quiet_no_image=True)
|
|
323
|
+
if not result.ok:
|
|
324
|
+
self.input.buffer.paste_clipboard_data(event.app.clipboard.get_data())
|
|
325
|
+
event.app.invalidate()
|
|
326
|
+
|
|
327
|
+
compact_choice_overlay = ConditionalContainer(
|
|
328
|
+
Window(
|
|
329
|
+
FormattedTextControl(self._render_compact_choice_panel),
|
|
330
|
+
width=self._choice_float_width,
|
|
331
|
+
height=lambda: self._choice_panel_height(),
|
|
332
|
+
dont_extend_width=True,
|
|
333
|
+
dont_extend_height=True,
|
|
334
|
+
style="class:choice.pad",
|
|
335
|
+
),
|
|
336
|
+
filter=footer_choice_mode,
|
|
337
|
+
)
|
|
338
|
+
permission_choice_overlay = ConditionalContainer(
|
|
339
|
+
Window(
|
|
340
|
+
FormattedTextControl(self._render_compact_choice_panel),
|
|
341
|
+
width=lambda: self._choice_menu_width(),
|
|
342
|
+
height=lambda: self._choice_panel_height(),
|
|
343
|
+
dont_extend_width=True,
|
|
344
|
+
dont_extend_height=True,
|
|
345
|
+
style="class:choice.pad",
|
|
346
|
+
),
|
|
347
|
+
filter=permission_choice_mode,
|
|
348
|
+
)
|
|
349
|
+
command_panel = ConditionalContainer(
|
|
350
|
+
Window(
|
|
351
|
+
FormattedTextControl(self._render_command_panel),
|
|
352
|
+
height=lambda: self._command_panel_height(),
|
|
353
|
+
dont_extend_height=True,
|
|
354
|
+
style="class:command",
|
|
355
|
+
),
|
|
356
|
+
filter=command_mode,
|
|
357
|
+
)
|
|
358
|
+
attachment_panel = ConditionalContainer(
|
|
359
|
+
Window(
|
|
360
|
+
FormattedTextControl(self._render_attachment_panel),
|
|
361
|
+
height=lambda: self._command_panel_height(),
|
|
362
|
+
dont_extend_height=True,
|
|
363
|
+
style="class:command",
|
|
364
|
+
),
|
|
365
|
+
filter=attachment_mode,
|
|
366
|
+
)
|
|
367
|
+
changes_bar = ConditionalContainer(
|
|
368
|
+
Window(
|
|
369
|
+
FormattedTextControl(self._render_changes_bar),
|
|
370
|
+
height=1,
|
|
371
|
+
style="class:body",
|
|
372
|
+
),
|
|
373
|
+
filter=has_changes,
|
|
374
|
+
)
|
|
375
|
+
review_panel = ConditionalContainer(
|
|
376
|
+
Window(
|
|
377
|
+
FormattedTextControl(self._render_review_panel),
|
|
378
|
+
height=lambda: self._review_panel_height(),
|
|
379
|
+
dont_extend_height=True,
|
|
380
|
+
style="class:body",
|
|
381
|
+
),
|
|
382
|
+
filter=review_mode,
|
|
383
|
+
)
|
|
384
|
+
command_output_overlay = ConditionalContainer(
|
|
385
|
+
Window(
|
|
386
|
+
FormattedTextControl(self._render_command_output_panel),
|
|
387
|
+
width=self._command_output_float_width,
|
|
388
|
+
height=self._command_output_float_height,
|
|
389
|
+
dont_extend_width=True,
|
|
390
|
+
dont_extend_height=True,
|
|
391
|
+
wrap_lines=True,
|
|
392
|
+
style="class:command-output",
|
|
393
|
+
),
|
|
394
|
+
filter=command_output_mode,
|
|
395
|
+
)
|
|
396
|
+
bottom_bar = VSplit(
|
|
397
|
+
[
|
|
398
|
+
HSplit(
|
|
399
|
+
[
|
|
400
|
+
self.input,
|
|
401
|
+
Window(FormattedTextControl(self._render_footer), height=1, style="class:hints"),
|
|
402
|
+
],
|
|
403
|
+
width=self._input_panel_width,
|
|
404
|
+
height=self._bottom_bar_height(),
|
|
405
|
+
style="class:body",
|
|
406
|
+
),
|
|
407
|
+
Window(char="│", width=1, style="class:rule"),
|
|
408
|
+
Window(
|
|
409
|
+
FormattedTextControl(self._render_detail_status_panel),
|
|
410
|
+
width=self._detail_status_width,
|
|
411
|
+
height=self._bottom_bar_height(),
|
|
412
|
+
style="class:status",
|
|
413
|
+
),
|
|
414
|
+
],
|
|
415
|
+
height=self._bottom_bar_height(),
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
left = HSplit(
|
|
419
|
+
[
|
|
420
|
+
Window(
|
|
421
|
+
self.body_control,
|
|
422
|
+
right_margins=[TranscriptScrollbarMargin(self)],
|
|
423
|
+
wrap_lines=True,
|
|
424
|
+
dont_extend_height=False,
|
|
425
|
+
style="class:body",
|
|
426
|
+
get_line_prefix=self._body_line_prefix,
|
|
427
|
+
),
|
|
428
|
+
Window(height=self._transcript_bottom_gap_height, style="class:body"),
|
|
429
|
+
Window(char="─", height=1, style="class:rule"),
|
|
430
|
+
command_panel,
|
|
431
|
+
attachment_panel,
|
|
432
|
+
review_panel,
|
|
433
|
+
changes_bar,
|
|
434
|
+
bottom_bar,
|
|
435
|
+
Window(char="─", height=1, style="class:rule"),
|
|
436
|
+
]
|
|
437
|
+
)
|
|
438
|
+
self._compact_choice_float = Float(
|
|
439
|
+
content=compact_choice_overlay,
|
|
440
|
+
left=0,
|
|
441
|
+
bottom=2,
|
|
442
|
+
width=self._choice_float_width,
|
|
443
|
+
height=self._choice_panel_height,
|
|
444
|
+
transparent=True,
|
|
445
|
+
z_index=20,
|
|
446
|
+
)
|
|
447
|
+
self._permission_choice_float = Float(
|
|
448
|
+
content=permission_choice_overlay,
|
|
449
|
+
left=0,
|
|
450
|
+
bottom=self.BOTTOM_BAR_HEIGHT + 1,
|
|
451
|
+
width=lambda: self._choice_menu_width(),
|
|
452
|
+
height=self._choice_panel_height,
|
|
453
|
+
transparent=True,
|
|
454
|
+
z_index=21,
|
|
455
|
+
)
|
|
456
|
+
root = FloatContainer(
|
|
457
|
+
content=left,
|
|
458
|
+
floats=[
|
|
459
|
+
self._compact_choice_float,
|
|
460
|
+
self._permission_choice_float,
|
|
461
|
+
Float(
|
|
462
|
+
content=command_output_overlay,
|
|
463
|
+
top=1,
|
|
464
|
+
right=2,
|
|
465
|
+
width=self._command_output_float_width,
|
|
466
|
+
height=self._command_output_float_height,
|
|
467
|
+
transparent=True,
|
|
468
|
+
z_index=10,
|
|
469
|
+
)
|
|
470
|
+
],
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
self.app: Application = Application(
|
|
474
|
+
layout=Layout(root, focused_element=self.input),
|
|
475
|
+
key_bindings=kb,
|
|
476
|
+
full_screen=True,
|
|
477
|
+
mouse_support=True,
|
|
478
|
+
refresh_interval=None,
|
|
479
|
+
style=_STYLE,
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
async def run(self, on_submit: SubmitHandler) -> None:
|
|
483
|
+
dock.set_refresh_callback(self.invalidate)
|
|
484
|
+
dock.set_width_provider(self._main_width)
|
|
485
|
+
consumer = asyncio.create_task(self._consume(on_submit))
|
|
486
|
+
try:
|
|
487
|
+
await self.app.run_async()
|
|
488
|
+
finally:
|
|
489
|
+
dock.set_refresh_callback(None)
|
|
490
|
+
dock.set_width_provider(None)
|
|
491
|
+
self._cancel_command_output_clear()
|
|
492
|
+
consumer.cancel()
|
|
493
|
+
try:
|
|
494
|
+
await consumer
|
|
495
|
+
except asyncio.CancelledError:
|
|
496
|
+
pass
|
|
497
|
+
|
|
498
|
+
def invalidate(self) -> None:
|
|
499
|
+
app = get_app_or_none()
|
|
500
|
+
if app is not None:
|
|
501
|
+
app.invalidate()
|
|
502
|
+
else:
|
|
503
|
+
self.app.invalidate()
|
|
504
|
+
|
|
505
|
+
async def _consume(self, on_submit: SubmitHandler) -> None:
|
|
506
|
+
while True:
|
|
507
|
+
item = await self._queue.get()
|
|
508
|
+
if item is None:
|
|
509
|
+
if not self._exit_requested:
|
|
510
|
+
self._exit_requested = True
|
|
511
|
+
self._exit_app()
|
|
512
|
+
return
|
|
513
|
+
self._reset_ctrl_c()
|
|
514
|
+
self._busy = True
|
|
515
|
+
self._current_submitted_text = item
|
|
516
|
+
self._submit_cancel_requested = False
|
|
517
|
+
self._current_submit_task = asyncio.create_task(on_submit(item))
|
|
518
|
+
self.invalidate()
|
|
519
|
+
try:
|
|
520
|
+
keep_running = await self._current_submit_task
|
|
521
|
+
except asyncio.CancelledError:
|
|
522
|
+
if not self._submit_cancel_requested:
|
|
523
|
+
raise
|
|
524
|
+
keep_running = True
|
|
525
|
+
except Exception as exc:
|
|
526
|
+
self._last_error = str(exc)
|
|
527
|
+
if dock.active and ui_events.is_running:
|
|
528
|
+
ui_events.emit_nowait(ErrorAppended(message=str(exc)))
|
|
529
|
+
else:
|
|
530
|
+
dock.append_error(str(exc))
|
|
531
|
+
keep_running = True
|
|
532
|
+
finally:
|
|
533
|
+
self._busy = False
|
|
534
|
+
self._current_submit_task = None
|
|
535
|
+
self._current_submitted_text = ""
|
|
536
|
+
self._submit_cancel_requested = False
|
|
537
|
+
self.invalidate()
|
|
538
|
+
if not keep_running:
|
|
539
|
+
self._exit_requested = True
|
|
540
|
+
self._exit_app()
|
|
541
|
+
return
|
|
542
|
+
|
|
543
|
+
def _on_input_changed(self, _) -> None:
|
|
544
|
+
if self.input.text:
|
|
545
|
+
self._reset_ctrl_c()
|
|
546
|
+
if not self._loading_input_history:
|
|
547
|
+
self._input_history_index = None
|
|
548
|
+
self._input_history_draft = ""
|
|
549
|
+
if self.input.text != self._command_panel_suppressed_text:
|
|
550
|
+
self._command_panel_suppressed_text = ""
|
|
551
|
+
if self.input.text != self._attachment_panel_suppressed_text:
|
|
552
|
+
self._attachment_panel_suppressed_text = ""
|
|
553
|
+
self._clamp_command_selection()
|
|
554
|
+
self._clamp_attachment_selection()
|
|
555
|
+
|
|
556
|
+
def _ignore_input_mouse(self, mouse_event: MouseEvent) -> None:
|
|
557
|
+
return None
|
|
558
|
+
|
|
559
|
+
def _input_prompt(self) -> AnyFormattedText:
|
|
560
|
+
if self._active_text_prompt is not None:
|
|
561
|
+
return [("class:input.prompt", f"{self._active_text_prompt}: ")]
|
|
562
|
+
return [("class:input.prompt", "❯ ")]
|
|
563
|
+
|
|
564
|
+
def _handle_body_mouse(self, mouse_event: MouseEvent) -> None:
|
|
565
|
+
event_type = mouse_event.event_type
|
|
566
|
+
if event_type == MouseEventType.SCROLL_UP:
|
|
567
|
+
self._scroll_by(3)
|
|
568
|
+
self.invalidate()
|
|
569
|
+
return None
|
|
570
|
+
if event_type == MouseEventType.SCROLL_DOWN:
|
|
571
|
+
self._scroll_by(-3)
|
|
572
|
+
self.invalidate()
|
|
573
|
+
return None
|
|
574
|
+
if event_type in (MouseEventType.MOUSE_DOWN, MouseEventType.MOUSE_UP):
|
|
575
|
+
self._last_body_click = mouse_event.position
|
|
576
|
+
if event_type == MouseEventType.MOUSE_DOWN:
|
|
577
|
+
self._body_mouse_down = mouse_event.position
|
|
578
|
+
else:
|
|
579
|
+
down = self._body_mouse_down
|
|
580
|
+
self._body_mouse_down = None
|
|
581
|
+
if down is None or down.y == mouse_event.position.y:
|
|
582
|
+
self._toggle_body_node_at(mouse_event.position.y)
|
|
583
|
+
self.invalidate()
|
|
584
|
+
return None
|
|
585
|
+
if event_type == MouseEventType.MOUSE_MOVE:
|
|
586
|
+
return None
|
|
587
|
+
return None
|
|
588
|
+
|
|
589
|
+
def _handle_ctrl_c(self) -> None:
|
|
590
|
+
if self._busy and self._current_submit_task is not None:
|
|
591
|
+
if not self._current_submit_task.done():
|
|
592
|
+
self._submit_cancel_requested = True
|
|
593
|
+
self._current_submit_task.cancel()
|
|
594
|
+
self._restore_interrupted_input()
|
|
595
|
+
self._reset_ctrl_c()
|
|
596
|
+
self._notice = "Interrupted. Restored last message for editing."
|
|
597
|
+
return
|
|
598
|
+
|
|
599
|
+
if self.input.text:
|
|
600
|
+
self.input.text = ""
|
|
601
|
+
self._reset_ctrl_c()
|
|
602
|
+
self._notice = "Input cleared. Press Ctrl-C twice on empty input to exit."
|
|
603
|
+
return
|
|
604
|
+
|
|
605
|
+
now = time.monotonic()
|
|
606
|
+
if not self._ctrl_c_armed or now > self._ctrl_c_deadline:
|
|
607
|
+
self._ctrl_c_armed = True
|
|
608
|
+
self._ctrl_c_deadline = now + 3.0
|
|
609
|
+
self._notice = "Press Ctrl-C again to exit"
|
|
610
|
+
return
|
|
611
|
+
|
|
612
|
+
self._notice = ""
|
|
613
|
+
self._request_exit()
|
|
614
|
+
|
|
615
|
+
def _reset_ctrl_c(self) -> None:
|
|
616
|
+
self._ctrl_c_armed = False
|
|
617
|
+
self._ctrl_c_deadline = 0.0
|
|
618
|
+
self._notice = ""
|
|
619
|
+
|
|
620
|
+
def _restore_interrupted_input(self) -> None:
|
|
621
|
+
text = self._current_submitted_text
|
|
622
|
+
if not text:
|
|
623
|
+
return
|
|
624
|
+
if self.input.buffer.selection_state is not None:
|
|
625
|
+
self.input.buffer.exit_selection()
|
|
626
|
+
self.input.text = text
|
|
627
|
+
self.input.buffer.cursor_position = len(text)
|
|
628
|
+
self._command_panel_suppressed_text = ""
|
|
629
|
+
self._attachment_panel_suppressed_text = ""
|
|
630
|
+
|
|
631
|
+
def _request_exit(self) -> None:
|
|
632
|
+
if self._exit_requested:
|
|
633
|
+
return
|
|
634
|
+
self._exit_requested = True
|
|
635
|
+
if self.app.is_running:
|
|
636
|
+
self._exit_app()
|
|
637
|
+
else:
|
|
638
|
+
self._queue.put_nowait(None)
|
|
639
|
+
|
|
640
|
+
def _exit_app(self) -> None:
|
|
641
|
+
if not self.app.is_running:
|
|
642
|
+
return
|
|
643
|
+
try:
|
|
644
|
+
self.app.exit()
|
|
645
|
+
except Exception as exc:
|
|
646
|
+
if "Return value already set" not in str(exc):
|
|
647
|
+
raise
|
|
648
|
+
|
|
649
|
+
def _insert_input_newline(self) -> None:
|
|
650
|
+
self._reset_ctrl_c()
|
|
651
|
+
self.input.buffer.insert_text("\n", fire_event=False)
|
|
652
|
+
|
|
653
|
+
def _extend_input_selection(self, amount: int) -> None:
|
|
654
|
+
if amount == 0:
|
|
655
|
+
return
|
|
656
|
+
buffer = self.input.buffer
|
|
657
|
+
if buffer.selection_state is None:
|
|
658
|
+
buffer.start_selection(selection_type=SelectionType.CHARACTERS)
|
|
659
|
+
if amount < 0:
|
|
660
|
+
buffer.cursor_left(count=abs(amount))
|
|
661
|
+
else:
|
|
662
|
+
buffer.cursor_right(count=amount)
|
|
663
|
+
self._reset_ctrl_c()
|
|
664
|
+
|
|
665
|
+
def _input_selection_text(self) -> str:
|
|
666
|
+
buffer = self.input.buffer
|
|
667
|
+
selection = buffer.selection_state
|
|
668
|
+
if selection is None:
|
|
669
|
+
return ""
|
|
670
|
+
start = min(selection.original_cursor_position, buffer.cursor_position)
|
|
671
|
+
end = max(selection.original_cursor_position, buffer.cursor_position)
|
|
672
|
+
return buffer.text[start:end]
|
|
673
|
+
|
|
674
|
+
def _copy_input_selection(self, event: Any | None = None) -> bool:
|
|
675
|
+
text = self._input_selection_text()
|
|
676
|
+
if not text:
|
|
677
|
+
return False
|
|
678
|
+
data = ClipboardData(text)
|
|
679
|
+
|
|
680
|
+
app = getattr(event, "app", None)
|
|
681
|
+
clipboard = getattr(app, "clipboard", None)
|
|
682
|
+
if clipboard is not None:
|
|
683
|
+
try:
|
|
684
|
+
clipboard.set_data(data)
|
|
685
|
+
except AttributeError:
|
|
686
|
+
clipboard.set_text(data.text)
|
|
687
|
+
self._copy_text_to_system_clipboard(data.text)
|
|
688
|
+
self._reset_ctrl_c()
|
|
689
|
+
self._notice = "Copied selection"
|
|
690
|
+
return True
|
|
691
|
+
|
|
692
|
+
def _copy_text_to_system_clipboard(self, text: str) -> None:
|
|
693
|
+
if not text or sys.platform != "darwin":
|
|
694
|
+
return
|
|
695
|
+
try:
|
|
696
|
+
subprocess.run(["pbcopy"], input=text, text=True, timeout=1, check=False)
|
|
697
|
+
except Exception:
|
|
698
|
+
return
|
|
699
|
+
|
|
700
|
+
def _record_input_history(self, text: str) -> None:
|
|
701
|
+
if not text.strip():
|
|
702
|
+
return
|
|
703
|
+
if not self._input_history or self._input_history[-1] != text:
|
|
704
|
+
self._input_history.append(text)
|
|
705
|
+
if len(self._input_history) > 200:
|
|
706
|
+
self._input_history = self._input_history[-200:]
|
|
707
|
+
self._input_history_index = None
|
|
708
|
+
self._input_history_draft = ""
|
|
709
|
+
|
|
710
|
+
def _set_input_from_history(self, text: str) -> None:
|
|
711
|
+
buffer = self.input.buffer
|
|
712
|
+
if buffer.selection_state is not None:
|
|
713
|
+
buffer.exit_selection()
|
|
714
|
+
self._loading_input_history = True
|
|
715
|
+
try:
|
|
716
|
+
self.input.text = text
|
|
717
|
+
self.input.buffer.cursor_position = len(text)
|
|
718
|
+
finally:
|
|
719
|
+
self._loading_input_history = False
|
|
720
|
+
self._review_active = False
|
|
721
|
+
self._reset_ctrl_c()
|
|
722
|
+
|
|
723
|
+
def _previous_input_history(self) -> None:
|
|
724
|
+
if not self._input_history:
|
|
725
|
+
return
|
|
726
|
+
if self._input_history_index is None:
|
|
727
|
+
self._input_history_draft = self.input.text
|
|
728
|
+
self._input_history_index = len(self._input_history) - 1
|
|
729
|
+
else:
|
|
730
|
+
self._input_history_index = max(0, self._input_history_index - 1)
|
|
731
|
+
self._set_input_from_history(self._input_history[self._input_history_index])
|
|
732
|
+
|
|
733
|
+
def _next_input_history(self) -> None:
|
|
734
|
+
if self._input_history_index is None:
|
|
735
|
+
return
|
|
736
|
+
if self._input_history_index < len(self._input_history) - 1:
|
|
737
|
+
self._input_history_index += 1
|
|
738
|
+
self._set_input_from_history(self._input_history[self._input_history_index])
|
|
739
|
+
return
|
|
740
|
+
draft = self._input_history_draft
|
|
741
|
+
self._input_history_index = None
|
|
742
|
+
self._input_history_draft = ""
|
|
743
|
+
self._set_input_from_history(draft)
|
|
744
|
+
|
|
745
|
+
def _choice_initial_index(self, choices: list[tuple[str, str, str]]) -> int:
|
|
746
|
+
if not self._choice_current_value:
|
|
747
|
+
return 0
|
|
748
|
+
cv = self._choice_current_value
|
|
749
|
+
for i, (label, value, _desc) in enumerate(choices):
|
|
750
|
+
if cv == value or cv == label:
|
|
751
|
+
return i
|
|
752
|
+
return 0
|
|
753
|
+
|
|
754
|
+
async def ask_choice(
|
|
755
|
+
self,
|
|
756
|
+
prompt: str,
|
|
757
|
+
choices: list[tuple[str, str, str]],
|
|
758
|
+
details: list[dict[str, Any]] | None = None,
|
|
759
|
+
) -> str | None:
|
|
760
|
+
self._active_choice = choices
|
|
761
|
+
self._choice_prompt = prompt
|
|
762
|
+
self._choice_selected = self._choice_initial_index(choices)
|
|
763
|
+
self._choice_details = details or []
|
|
764
|
+
if not self._choice_anchor:
|
|
765
|
+
self._choice_anchor = self._choice_anchor_for_prompt(prompt)
|
|
766
|
+
self.invalidate()
|
|
767
|
+
try:
|
|
768
|
+
return await self._choice_queue.get()
|
|
769
|
+
finally:
|
|
770
|
+
self._active_choice = None
|
|
771
|
+
self._choice_details = []
|
|
772
|
+
self._choice_anchor = ""
|
|
773
|
+
|
|
774
|
+
def _finish_choice(self, value: str | None) -> None:
|
|
775
|
+
if self._active_choice is None:
|
|
776
|
+
return
|
|
777
|
+
self._choice_queue.put_nowait(value)
|
|
778
|
+
self._active_choice = None
|
|
779
|
+
self._choice_details = []
|
|
780
|
+
self._choice_anchor = ""
|
|
781
|
+
|
|
782
|
+
def _move_choice_selection(self, amount: int) -> None:
|
|
783
|
+
choices = self._active_choice or []
|
|
784
|
+
if not choices:
|
|
785
|
+
return
|
|
786
|
+
self._choice_selected = (self._choice_selected + amount) % len(choices)
|
|
787
|
+
|
|
788
|
+
def _submit_choice_selection(self) -> None:
|
|
789
|
+
choices = self._active_choice or []
|
|
790
|
+
if not choices:
|
|
791
|
+
return
|
|
792
|
+
index = max(0, min(self._choice_selected, len(choices) - 1))
|
|
793
|
+
self._finish_choice(choices[index][1])
|
|
794
|
+
|
|
795
|
+
async def ask_text(self, prompt: str, default: str = "", secret: bool = False) -> str | None:
|
|
796
|
+
if self._active_text_prompt is not None:
|
|
797
|
+
raise RuntimeError("Text prompt is already active")
|
|
798
|
+
self._saved_input_text = self.input.text
|
|
799
|
+
self._saved_input_cursor = self.input.buffer.cursor_position
|
|
800
|
+
self._active_text_prompt = prompt
|
|
801
|
+
self._active_text_default = default
|
|
802
|
+
self._active_text_secret = secret
|
|
803
|
+
self.input.text = ""
|
|
804
|
+
self.input.buffer.cursor_position = 0
|
|
805
|
+
self._command_panel_suppressed_text = ""
|
|
806
|
+
self._attachment_panel_suppressed_text = ""
|
|
807
|
+
self.invalidate()
|
|
808
|
+
try:
|
|
809
|
+
result = await self._text_queue.get()
|
|
810
|
+
if result is None:
|
|
811
|
+
return None
|
|
812
|
+
return result if result else default
|
|
813
|
+
finally:
|
|
814
|
+
if self._active_text_prompt is not None:
|
|
815
|
+
self._restore_text_prompt()
|
|
816
|
+
|
|
817
|
+
def set_notice(self, text: str) -> None:
|
|
818
|
+
self._notice = text
|
|
819
|
+
self.invalidate()
|
|
820
|
+
|
|
821
|
+
def show_transient_output(self, text: str, title: str = "") -> None:
|
|
822
|
+
self.begin_command_output(title)
|
|
823
|
+
self.append_command_output(text)
|
|
824
|
+
|
|
825
|
+
def begin_command_output(self, title: str) -> None:
|
|
826
|
+
self._command_output_title = title
|
|
827
|
+
self._command_output_lines = []
|
|
828
|
+
self._command_output_visible = False
|
|
829
|
+
self._cancel_command_output_clear()
|
|
830
|
+
self.invalidate()
|
|
831
|
+
|
|
832
|
+
def append_command_output(self, text: str) -> None:
|
|
833
|
+
if not text.strip():
|
|
834
|
+
return
|
|
835
|
+
for line in text.rstrip("\n").splitlines():
|
|
836
|
+
cleaned = _strip_ansi_trailing_space(line)
|
|
837
|
+
self._command_output_lines.append(_ansi_line(cleaned))
|
|
838
|
+
if len(self._command_output_lines) > 500:
|
|
839
|
+
self._command_output_lines = self._command_output_lines[-500:]
|
|
840
|
+
self._command_output_visible = True
|
|
841
|
+
self._schedule_command_output_clear()
|
|
842
|
+
self.invalidate()
|
|
843
|
+
|
|
844
|
+
def hide_command_output(self) -> None:
|
|
845
|
+
self._command_output_visible = False
|
|
846
|
+
self._cancel_command_output_clear()
|
|
847
|
+
self.invalidate()
|
|
848
|
+
|
|
849
|
+
def clear_command_output(self) -> None:
|
|
850
|
+
self._command_output_title = ""
|
|
851
|
+
self._command_output_lines = []
|
|
852
|
+
self._command_output_visible = False
|
|
853
|
+
self._cancel_command_output_clear()
|
|
854
|
+
self.invalidate()
|
|
855
|
+
|
|
856
|
+
def _schedule_command_output_clear(self) -> None:
|
|
857
|
+
self._cancel_command_output_clear()
|
|
858
|
+
try:
|
|
859
|
+
loop = asyncio.get_running_loop()
|
|
860
|
+
except RuntimeError:
|
|
861
|
+
return
|
|
862
|
+
self._command_output_clear_handle = loop.call_later(
|
|
863
|
+
self.COMMAND_OUTPUT_TTL_SECONDS,
|
|
864
|
+
self.clear_command_output,
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
def _cancel_command_output_clear(self) -> None:
|
|
868
|
+
handle = self._command_output_clear_handle
|
|
869
|
+
self._command_output_clear_handle = None
|
|
870
|
+
if handle is not None and not handle.cancelled():
|
|
871
|
+
handle.cancel()
|
|
872
|
+
|
|
873
|
+
def queue_quiet_command(self, command: str) -> None:
|
|
874
|
+
command = command.strip()
|
|
875
|
+
if not command:
|
|
876
|
+
return
|
|
877
|
+
self._quiet_commands.append(command)
|
|
878
|
+
self._queue.put_nowait(command)
|
|
879
|
+
|
|
880
|
+
def consume_quiet_command(self, command: str) -> bool:
|
|
881
|
+
command = command.strip()
|
|
882
|
+
try:
|
|
883
|
+
index = self._quiet_commands.index(command)
|
|
884
|
+
except ValueError:
|
|
885
|
+
return False
|
|
886
|
+
del self._quiet_commands[index]
|
|
887
|
+
return True
|
|
888
|
+
|
|
889
|
+
def command_output_width(self) -> int:
|
|
890
|
+
return self._command_output_float_width()
|
|
891
|
+
|
|
892
|
+
def _submit_text_prompt(self) -> None:
|
|
893
|
+
value = self.input.text
|
|
894
|
+
self._text_queue.put_nowait(value)
|
|
895
|
+
self._restore_text_prompt()
|
|
896
|
+
|
|
897
|
+
def _cancel_text_prompt(self) -> None:
|
|
898
|
+
self._text_queue.put_nowait(None)
|
|
899
|
+
self._restore_text_prompt()
|
|
900
|
+
|
|
901
|
+
def _restore_text_prompt(self) -> None:
|
|
902
|
+
saved_text = self._saved_input_text
|
|
903
|
+
saved_cursor = max(0, min(self._saved_input_cursor, len(saved_text)))
|
|
904
|
+
self._active_text_prompt = None
|
|
905
|
+
self._active_text_default = ""
|
|
906
|
+
self._active_text_secret = False
|
|
907
|
+
self._saved_input_text = ""
|
|
908
|
+
self._saved_input_cursor = 0
|
|
909
|
+
self.input.text = saved_text
|
|
910
|
+
self.input.buffer.cursor_position = saved_cursor
|
|
911
|
+
self.invalidate()
|
|
912
|
+
|
|
913
|
+
def _submit_input(self) -> None:
|
|
914
|
+
text = self.input.text
|
|
915
|
+
stripped = text.strip()
|
|
916
|
+
if stripped == "/paste":
|
|
917
|
+
self._record_input_history(text)
|
|
918
|
+
self.input.text = ""
|
|
919
|
+
self._scroll_offset = 0
|
|
920
|
+
self._command_panel_suppressed_text = ""
|
|
921
|
+
self._attachment_panel_suppressed_text = ""
|
|
922
|
+
self._reset_ctrl_c()
|
|
923
|
+
self.paste_clipboard_image()
|
|
924
|
+
return
|
|
925
|
+
|
|
926
|
+
self.input.text = ""
|
|
927
|
+
self._scroll_offset = 0
|
|
928
|
+
self._command_panel_suppressed_text = ""
|
|
929
|
+
self._attachment_panel_suppressed_text = ""
|
|
930
|
+
self._reset_ctrl_c()
|
|
931
|
+
if stripped and not stripped.startswith("/"):
|
|
932
|
+
self.clear_command_output()
|
|
933
|
+
if stripped:
|
|
934
|
+
self._record_input_history(text)
|
|
935
|
+
self._queue.put_nowait(text)
|
|
936
|
+
|
|
937
|
+
def paste_clipboard_image(self, *, quiet_no_image: bool = False) -> ClipboardImageResult:
|
|
938
|
+
result = paste_clipboard_image_from_system(self.status.workspace)
|
|
939
|
+
if result.ok:
|
|
940
|
+
self._insert_attachment_token(result.rel_path)
|
|
941
|
+
self.clear_command_output()
|
|
942
|
+
if result.ok or not quiet_no_image:
|
|
943
|
+
self._notice = result.message
|
|
944
|
+
return result
|
|
945
|
+
|
|
946
|
+
def _insert_attachment_token(self, rel_path: str) -> None:
|
|
947
|
+
token = attachment_token_text(rel_path) + " "
|
|
948
|
+
text = self.input.text
|
|
949
|
+
cursor = max(0, min(self.input.buffer.cursor_position, len(text)))
|
|
950
|
+
prefix = " " if cursor > 0 and not text[cursor - 1].isspace() else ""
|
|
951
|
+
new_text = text[:cursor] + prefix + token + text[cursor:]
|
|
952
|
+
self.input.text = new_text
|
|
953
|
+
self.input.buffer.cursor_position = cursor + len(prefix) + len(token)
|
|
954
|
+
self._attachment_panel_suppressed_text = ""
|
|
955
|
+
self._command_panel_suppressed_text = ""
|
|
956
|
+
|
|
957
|
+
def _accept_attachment_panel_selection(self) -> bool:
|
|
958
|
+
token = self._attachment_token()
|
|
959
|
+
if token is None:
|
|
960
|
+
return False
|
|
961
|
+
matches = self._attachment_matches()
|
|
962
|
+
if not matches:
|
|
963
|
+
return False
|
|
964
|
+
selected = matches[min(self._attachment_selected, len(matches) - 1)]
|
|
965
|
+
replacement = attachment_token_text(selected.rel_path) + " "
|
|
966
|
+
text = self.input.text
|
|
967
|
+
new_text = text[:token.start] + replacement + text[token.end:]
|
|
968
|
+
self.input.text = new_text
|
|
969
|
+
new_cursor = token.start + len(replacement)
|
|
970
|
+
self.input.buffer.cursor_position = new_cursor
|
|
971
|
+
self._attachment_panel_suppressed_text = ""
|
|
972
|
+
self._attachment_selected = 0
|
|
973
|
+
return True
|
|
974
|
+
|
|
975
|
+
_STYLE = Style.from_dict(
|
|
976
|
+
{
|
|
977
|
+
"body": "#ECEFF4 bg:#000000",
|
|
978
|
+
"rule": "#4C566A",
|
|
979
|
+
"input": "#ECEFF4 bg:#000000",
|
|
980
|
+
"input.prompt": "bold #ECEFF4 bg:#000000",
|
|
981
|
+
"hints": "#8FBCBB",
|
|
982
|
+
"hints.click": "#D8DEE9",
|
|
983
|
+
"footer.permission": "#A3BE8C bg:#000000",
|
|
984
|
+
"footer.model": "#88C0D0 bg:#000000",
|
|
985
|
+
"footer.reasoning": "#EBCB8B bg:#000000",
|
|
986
|
+
"dim": "#D8DEE9",
|
|
987
|
+
"status": "#8FBCBB bg:#000000",
|
|
988
|
+
"status.label": "#8FBCBB bg:#000000",
|
|
989
|
+
"status.value": "#D8DEE9 bg:#000000",
|
|
990
|
+
"status.dim": "#9AA1AD bg:#000000",
|
|
991
|
+
"choice": "#ECEFF4 bg:#000000",
|
|
992
|
+
"choice.pad": "bg:#000000",
|
|
993
|
+
"choice.selected": "bold #EBCB8B bg:#000000",
|
|
994
|
+
"choice.prompt": "bold #ECEFF4",
|
|
995
|
+
"choice.tool": "bold #8FBCBB",
|
|
996
|
+
"choice.dim": "#A7B0BE",
|
|
997
|
+
"command": "bg:#000000 #D8DEE9",
|
|
998
|
+
"command.divider": "#B7C1FF bg:#000000",
|
|
999
|
+
"command.title": "bold #B7C1FF bg:#000000",
|
|
1000
|
+
"command.group": "bold #ECEFF4 bg:#000000",
|
|
1001
|
+
"command.name": "#ECEFF4 bg:#000000",
|
|
1002
|
+
"command.selected": "bold #EBCB8B bg:#000000",
|
|
1003
|
+
"command.marker": "bold #EBCB8B bg:#000000",
|
|
1004
|
+
"command.dim": "#9AA1AD bg:#000000",
|
|
1005
|
+
"command.ok": "#5FD27A bg:#000000",
|
|
1006
|
+
"command.error": "#BF616A bg:#000000",
|
|
1007
|
+
"command-output": "#D8DEE9 bg:#000000",
|
|
1008
|
+
"permission": "bg:#000000 #D8DEE9",
|
|
1009
|
+
"permission.border": "#5E81AC bg:#000000",
|
|
1010
|
+
"permission.title": "bold #EBCB8B bg:#000000",
|
|
1011
|
+
"permission.prompt": "bold #ECEFF4 bg:#000000",
|
|
1012
|
+
"permission.tool": "bold #8FBCBB bg:#000000",
|
|
1013
|
+
"permission.dim": "#A7B0BE bg:#000000",
|
|
1014
|
+
"permission.marker": "bold #EBCB8B bg:#000000",
|
|
1015
|
+
"permission.choice": "#D8DEE9 bg:#000000",
|
|
1016
|
+
"permission.choice.selected": "bold #EBCB8B",
|
|
1017
|
+
"permission.key": "#88C0D0 bg:#000000",
|
|
1018
|
+
"scrollbar.background": "bg:#000000",
|
|
1019
|
+
"scrollbar.button": "bg:#4C566A",
|
|
1020
|
+
"changes": "#D8DEE9 bg:#253340",
|
|
1021
|
+
"changes.label": "#D8DEE9 bg:#253340",
|
|
1022
|
+
"changes.dim": "#9AA1AD bg:#253340",
|
|
1023
|
+
"changes.added": "#A3BE8C bg:#253340",
|
|
1024
|
+
"changes.removed": "#BF616A bg:#253340",
|
|
1025
|
+
"changes.review": "bold #8FBCBB bg:#253340",
|
|
1026
|
+
"changes.rollback": "bold #EBCB8B bg:#253340",
|
|
1027
|
+
"review": "#D8DEE9 bg:#253340",
|
|
1028
|
+
"review.file": "bold #5E81AC bg:#253340",
|
|
1029
|
+
"review.added": "#A3BE8C bg:#253340",
|
|
1030
|
+
"review.removed": "#BF616A bg:#253340",
|
|
1031
|
+
"review.dim": "#9AA1AD bg:#253340",
|
|
1032
|
+
}
|
|
1033
|
+
)
|