indent 0.1.8__py3-none-any.whl → 0.1.9__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 indent might be problematic. Click here for more details.
- exponent/__init__.py +21 -1
- exponent/cli.py +8 -10
- exponent/commands/config_commands.py +10 -10
- exponent/commands/run_commands.py +1 -1
- exponent/commands/upgrade.py +3 -3
- exponent/commands/utils.py +0 -90
- exponent/commands/workflow_commands.py +1 -1
- exponent/core/config.py +2 -0
- exponent/core/remote_execution/client.py +57 -1
- {indent-0.1.8.dist-info → indent-0.1.9.dist-info}/METADATA +1 -1
- {indent-0.1.8.dist-info → indent-0.1.9.dist-info}/RECORD +13 -17
- exponent/commands/github_app_commands.py +0 -211
- exponent/commands/listen_commands.py +0 -96
- exponent/commands/shell_commands.py +0 -2840
- exponent/commands/theme.py +0 -246
- {indent-0.1.8.dist-info → indent-0.1.9.dist-info}/WHEEL +0 -0
- {indent-0.1.8.dist-info → indent-0.1.9.dist-info}/entry_points.txt +0 -0
|
@@ -1,2840 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import difflib
|
|
3
|
-
import os
|
|
4
|
-
import re
|
|
5
|
-
import select
|
|
6
|
-
import shutil
|
|
7
|
-
import sys
|
|
8
|
-
import time
|
|
9
|
-
from asyncio.events import AbstractEventLoop
|
|
10
|
-
from collections.abc import Callable, Coroutine, Iterable
|
|
11
|
-
|
|
12
|
-
from exponent.commands.theme import Theme, bg_color_seq, fg_color_seq, get_theme
|
|
13
|
-
from exponent.core.remote_execution.types import ChatSource
|
|
14
|
-
from exponent.core.remote_execution.utils import safe_read_file
|
|
15
|
-
from exponent.utils.version import check_exponent_version_and_upgrade
|
|
16
|
-
|
|
17
|
-
# Import termios and tty conditionally for Windows compatibility
|
|
18
|
-
try:
|
|
19
|
-
import termios
|
|
20
|
-
import tty
|
|
21
|
-
|
|
22
|
-
POSIX_TERMINAL = True
|
|
23
|
-
except ImportError:
|
|
24
|
-
POSIX_TERMINAL = False
|
|
25
|
-
|
|
26
|
-
import logging
|
|
27
|
-
from concurrent.futures import Future
|
|
28
|
-
from datetime import datetime
|
|
29
|
-
from enum import Enum
|
|
30
|
-
from typing import IO, Any
|
|
31
|
-
|
|
32
|
-
import click
|
|
33
|
-
import questionary
|
|
34
|
-
from prompt_toolkit import PromptSession
|
|
35
|
-
from prompt_toolkit.completion import (
|
|
36
|
-
CompleteEvent,
|
|
37
|
-
Completer,
|
|
38
|
-
Completion,
|
|
39
|
-
FuzzyCompleter,
|
|
40
|
-
)
|
|
41
|
-
from prompt_toolkit.document import Document
|
|
42
|
-
from prompt_toolkit.formatted_text import FormattedText
|
|
43
|
-
from prompt_toolkit.key_binding import KeyBindings, merge_key_bindings
|
|
44
|
-
from prompt_toolkit.key_binding.defaults import load_key_bindings
|
|
45
|
-
from prompt_toolkit.key_binding.key_processor import KeyPress
|
|
46
|
-
from prompt_toolkit.keys import Keys
|
|
47
|
-
from prompt_toolkit.lexers import Lexer
|
|
48
|
-
from prompt_toolkit.styles import Style
|
|
49
|
-
from rich.console import Console
|
|
50
|
-
from rich.syntax import Syntax
|
|
51
|
-
|
|
52
|
-
from exponent.commands.common import (
|
|
53
|
-
check_inside_git_repo,
|
|
54
|
-
check_running_from_home_directory,
|
|
55
|
-
check_ssl,
|
|
56
|
-
create_chat,
|
|
57
|
-
inside_ssh_session,
|
|
58
|
-
redirect_to_login,
|
|
59
|
-
start_client,
|
|
60
|
-
)
|
|
61
|
-
from exponent.commands.settings import use_settings
|
|
62
|
-
from exponent.commands.types import (
|
|
63
|
-
StrategyChoice,
|
|
64
|
-
StrategyOption,
|
|
65
|
-
exponent_cli_group,
|
|
66
|
-
)
|
|
67
|
-
from exponent.commands.utils import (
|
|
68
|
-
ConnectionTracker,
|
|
69
|
-
Spinner,
|
|
70
|
-
ThinkingSpinner,
|
|
71
|
-
start_background_event_loop,
|
|
72
|
-
)
|
|
73
|
-
from exponent.core.config import Environment, Settings
|
|
74
|
-
from exponent.core.graphql.client import GraphQLClient
|
|
75
|
-
from exponent.core.graphql.mutations import HALT_CHAT_STREAM_MUTATION
|
|
76
|
-
from exponent.core.graphql.queries import EVENTS_FOR_CHAT_QUERY
|
|
77
|
-
from exponent.core.graphql.subscriptions import (
|
|
78
|
-
CONFIRM_AND_CONTINUE_SUBSCRIPTION,
|
|
79
|
-
INDENT_EVENTS_SUBSCRIPTION,
|
|
80
|
-
)
|
|
81
|
-
from exponent.core.remote_execution.exceptions import ExponentError, RateLimitError
|
|
82
|
-
from exponent.core.remote_execution.files import FileCache
|
|
83
|
-
from exponent.core.types.generated.strategy_info import (
|
|
84
|
-
ENABLED_STRATEGY_INFO_LIST,
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
from .utils import (
|
|
88
|
-
ask_for_quit_confirmation,
|
|
89
|
-
get_short_git_commit_hash,
|
|
90
|
-
launch_exponent_browser,
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
logger = logging.getLogger(__name__)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
SLASH_COMMANDS = {
|
|
97
|
-
"/help": "Show available commands",
|
|
98
|
-
"/autorun": "Toggle between autorun modes",
|
|
99
|
-
"/web": "Move chat to a web browser",
|
|
100
|
-
"/cmd": "Execute a terminal command directly and give the result as context",
|
|
101
|
-
# Thinking is slow + not making chats better, disabling for now
|
|
102
|
-
# "/thinking": "Toggle thinking mode to show/hide AI's thinking process",
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
class AutoConfirmMode(str, Enum):
|
|
107
|
-
OFF = "off"
|
|
108
|
-
READ_ONLY = "read_only"
|
|
109
|
-
ALL = "all"
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
ATTACHMENT_PATH_PATTERN = re.compile(r"(^|\s)@[^\s]+")
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
@exponent_cli_group()
|
|
116
|
-
def shell_cli() -> None:
|
|
117
|
-
pass
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
@shell_cli.command()
|
|
121
|
-
@click.option(
|
|
122
|
-
"--model",
|
|
123
|
-
help="LLM model",
|
|
124
|
-
required=True,
|
|
125
|
-
default="CLAUDE_4_SONNET",
|
|
126
|
-
)
|
|
127
|
-
@click.option(
|
|
128
|
-
"--strategy",
|
|
129
|
-
prompt=True,
|
|
130
|
-
prompt_required=False,
|
|
131
|
-
type=StrategyChoice(ENABLED_STRATEGY_INFO_LIST),
|
|
132
|
-
cls=StrategyOption,
|
|
133
|
-
default="NATURAL_EDIT_CLAUDE_3_7_XML",
|
|
134
|
-
)
|
|
135
|
-
@click.option(
|
|
136
|
-
"--autorun",
|
|
137
|
-
is_flag=True,
|
|
138
|
-
help="Enable autorun mode",
|
|
139
|
-
)
|
|
140
|
-
@click.option(
|
|
141
|
-
"--depth",
|
|
142
|
-
type=click.IntRange(1, 30, clamp=True),
|
|
143
|
-
help="Depth limit of the chat if autorun mode is enabled",
|
|
144
|
-
default=5,
|
|
145
|
-
)
|
|
146
|
-
@click.option(
|
|
147
|
-
"--chat-id",
|
|
148
|
-
help="ID of an existing chat session to reconnect",
|
|
149
|
-
required=False,
|
|
150
|
-
)
|
|
151
|
-
@click.option(
|
|
152
|
-
"--prompt",
|
|
153
|
-
help="Initial prompt",
|
|
154
|
-
)
|
|
155
|
-
@click.option(
|
|
156
|
-
"--headless",
|
|
157
|
-
is_flag=True,
|
|
158
|
-
help="Run single prompt in headless mode",
|
|
159
|
-
)
|
|
160
|
-
@use_settings
|
|
161
|
-
def shell(
|
|
162
|
-
settings: Settings,
|
|
163
|
-
model: str,
|
|
164
|
-
strategy: str,
|
|
165
|
-
chat_id: str | None = None,
|
|
166
|
-
autorun: bool = False,
|
|
167
|
-
depth: int = 0,
|
|
168
|
-
prompt: str | None = None,
|
|
169
|
-
headless: bool = False,
|
|
170
|
-
) -> None:
|
|
171
|
-
"""Start an Exponent session in your current shell."""
|
|
172
|
-
|
|
173
|
-
check_exponent_version_and_upgrade(settings)
|
|
174
|
-
|
|
175
|
-
if not headless and not sys.stdin.isatty():
|
|
176
|
-
print("Terminal not available, running in headless mode")
|
|
177
|
-
headless = True
|
|
178
|
-
|
|
179
|
-
if headless and not prompt:
|
|
180
|
-
print("Error: --prompt option is required with headless mode")
|
|
181
|
-
sys.exit(1)
|
|
182
|
-
|
|
183
|
-
if not settings.api_key:
|
|
184
|
-
redirect_to_login(settings)
|
|
185
|
-
return
|
|
186
|
-
|
|
187
|
-
is_running_from_home_directory = check_running_from_home_directory(
|
|
188
|
-
# We don't require a confirmation for exponent shell because it's more likely
|
|
189
|
-
# there's a legitimate reason for running Exponent from home directory when using shell
|
|
190
|
-
require_confirmation=False
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
loop = start_background_event_loop()
|
|
194
|
-
|
|
195
|
-
if not is_running_from_home_directory: # Prevent double warnings from being shown
|
|
196
|
-
asyncio.run_coroutine_threadsafe(check_inside_git_repo(settings), loop).result()
|
|
197
|
-
|
|
198
|
-
check_ssl()
|
|
199
|
-
|
|
200
|
-
api_key = settings.api_key
|
|
201
|
-
base_api_url = settings.get_base_api_url()
|
|
202
|
-
base_ws_url = settings.get_base_ws_url()
|
|
203
|
-
gql_client = GraphQLClient(api_key, base_api_url, base_ws_url)
|
|
204
|
-
parent_event_uuid: str | None = None
|
|
205
|
-
checkpoints: list[dict[str, Any]] = []
|
|
206
|
-
|
|
207
|
-
if chat_id is None:
|
|
208
|
-
chat_uuid = asyncio.run_coroutine_threadsafe(
|
|
209
|
-
create_chat(api_key, base_api_url, base_ws_url, ChatSource.CLI_SHELL), loop
|
|
210
|
-
).result()
|
|
211
|
-
else:
|
|
212
|
-
chat_uuid = chat_id
|
|
213
|
-
|
|
214
|
-
# events = asyncio.run_coroutine_threadsafe(
|
|
215
|
-
# _get_events_for_chat(gql_client, chat_uuid), loop
|
|
216
|
-
# ).result()
|
|
217
|
-
# parent_event_uuid = _get_parent_event_uuid(events)
|
|
218
|
-
# checkpoints = _get_checkpoints(events)
|
|
219
|
-
|
|
220
|
-
if chat_uuid is None:
|
|
221
|
-
sys.exit(1)
|
|
222
|
-
|
|
223
|
-
working_directory = os.getcwd()
|
|
224
|
-
file_cache = FileCache(working_directory)
|
|
225
|
-
connection_tracker = ConnectionTracker()
|
|
226
|
-
|
|
227
|
-
client_coro = start_client(
|
|
228
|
-
api_key,
|
|
229
|
-
settings.base_url,
|
|
230
|
-
base_api_url,
|
|
231
|
-
base_ws_url,
|
|
232
|
-
chat_uuid,
|
|
233
|
-
file_cache=file_cache,
|
|
234
|
-
connection_tracker=connection_tracker,
|
|
235
|
-
)
|
|
236
|
-
|
|
237
|
-
theme = get_theme(settings.options.use_default_colors)
|
|
238
|
-
|
|
239
|
-
if headless:
|
|
240
|
-
assert prompt is not None
|
|
241
|
-
|
|
242
|
-
chat = Chat(
|
|
243
|
-
chat_uuid,
|
|
244
|
-
parent_event_uuid,
|
|
245
|
-
settings.base_url,
|
|
246
|
-
working_directory,
|
|
247
|
-
gql_client,
|
|
248
|
-
model,
|
|
249
|
-
strategy,
|
|
250
|
-
autorun,
|
|
251
|
-
depth,
|
|
252
|
-
StaticView(theme),
|
|
253
|
-
checkpoints=checkpoints,
|
|
254
|
-
thinking=False,
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
client_task = loop.create_task(client_coro)
|
|
258
|
-
turn_task = loop.create_task(chat.send_prompt(prompt))
|
|
259
|
-
|
|
260
|
-
asyncio.run_coroutine_threadsafe(
|
|
261
|
-
asyncio.wait({client_task, turn_task}, return_when=asyncio.FIRST_COMPLETED),
|
|
262
|
-
loop,
|
|
263
|
-
).result()
|
|
264
|
-
else:
|
|
265
|
-
chat = Chat(
|
|
266
|
-
chat_uuid,
|
|
267
|
-
parent_event_uuid,
|
|
268
|
-
settings.base_url,
|
|
269
|
-
working_directory,
|
|
270
|
-
gql_client,
|
|
271
|
-
model,
|
|
272
|
-
strategy,
|
|
273
|
-
autorun,
|
|
274
|
-
depth,
|
|
275
|
-
LiveView(theme),
|
|
276
|
-
checkpoints=checkpoints,
|
|
277
|
-
thinking=False,
|
|
278
|
-
)
|
|
279
|
-
|
|
280
|
-
client_fut = asyncio.run_coroutine_threadsafe(client_coro, loop)
|
|
281
|
-
input_handler = InputHandler(theme, loop, file_cache)
|
|
282
|
-
_print_welcome_message(theme)
|
|
283
|
-
|
|
284
|
-
shell = Shell(
|
|
285
|
-
prompt, loop, input_handler, chat, connection_tracker, settings.environment
|
|
286
|
-
)
|
|
287
|
-
|
|
288
|
-
shell.run()
|
|
289
|
-
client_fut.cancel()
|
|
290
|
-
|
|
291
|
-
print("Jump back into this chat by running:")
|
|
292
|
-
print(f" exponent shell --chat-id {chat_uuid}")
|
|
293
|
-
print()
|
|
294
|
-
print("Or continue in a web browser at:")
|
|
295
|
-
print(f" {chat.url()}")
|
|
296
|
-
print()
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
async def _get_events_for_chat(
|
|
300
|
-
gql_client: GraphQLClient, chat_uuid: str
|
|
301
|
-
) -> list[dict[str, Any]]:
|
|
302
|
-
result = await gql_client.execute(EVENTS_FOR_CHAT_QUERY, {"chatUuid": chat_uuid})
|
|
303
|
-
return result.get("eventsForChat", {}).get("events", []) # type: ignore
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
def _get_parent_event_uuid(events: list[dict[str, Any]]) -> str | None:
|
|
307
|
-
if len(events) > 0:
|
|
308
|
-
uuid = events[-1]["eventUuid"]
|
|
309
|
-
assert isinstance(uuid, str)
|
|
310
|
-
|
|
311
|
-
return uuid
|
|
312
|
-
|
|
313
|
-
return None
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
def _get_checkpoints(events: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
317
|
-
return [
|
|
318
|
-
event for event in events if event.get("__typename") == "CheckpointCreatedEvent"
|
|
319
|
-
]
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
def _print_welcome_message(theme: Theme) -> None:
|
|
323
|
-
print("╭─────────────────────────────────╮")
|
|
324
|
-
print(
|
|
325
|
-
f"│ ✨ Welcome to {bold_seq()}{fg_color_seq(theme.exponent_green)}Indent \x1b[4mSHELL{reset_attrs_seq()} ✨ │"
|
|
326
|
-
)
|
|
327
|
-
|
|
328
|
-
print("╰─────────────────────────────────╯")
|
|
329
|
-
print()
|
|
330
|
-
print(
|
|
331
|
-
f" Enter {bold_seq()}/help{not_bold_seq()} to see available commands and keyboard shortcuts"
|
|
332
|
-
)
|
|
333
|
-
print(
|
|
334
|
-
f" Enter {bold_seq()}q{not_bold_seq()}, {bold_seq()}exit{not_bold_seq()} or press {bold_seq()}<ctrl+c>{not_bold_seq()} to quit"
|
|
335
|
-
)
|
|
336
|
-
print()
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
def pause_spinner(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
340
|
-
def wrapper(self: "LiveView", *args: Any, **kwargs: Any) -> Any:
|
|
341
|
-
self.spinner.hide()
|
|
342
|
-
self.spinner = self.default_spinner
|
|
343
|
-
result = func(self, *args, **kwargs)
|
|
344
|
-
self.spinner.show()
|
|
345
|
-
return result
|
|
346
|
-
|
|
347
|
-
return wrapper
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
def stop_spinner(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
351
|
-
def wrapper(self: "LiveView", *args: Any, **kwargs: Any) -> Any:
|
|
352
|
-
self.spinner.hide()
|
|
353
|
-
self.spinner = self.default_spinner
|
|
354
|
-
result = func(self, *args, **kwargs)
|
|
355
|
-
return result
|
|
356
|
-
|
|
357
|
-
return wrapper
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
class BaseView:
|
|
361
|
-
def __init__(self, theme: Theme) -> None:
|
|
362
|
-
self.theme = theme
|
|
363
|
-
|
|
364
|
-
def render_event_summary(self, kind: str, event: dict[str, Any]) -> None:
|
|
365
|
-
summary = event["messageData"]["__typename"]
|
|
366
|
-
# if kind == "FileWriteEvent":
|
|
367
|
-
# summary = "Writing file..."
|
|
368
|
-
|
|
369
|
-
# elif kind == "FileWriteResultEvent":
|
|
370
|
-
# summary = "File written"
|
|
371
|
-
|
|
372
|
-
# elif kind == "CodeBlockEvent":
|
|
373
|
-
# summary = "Executing code..."
|
|
374
|
-
|
|
375
|
-
# elif kind == "CodeExecutionEvent":
|
|
376
|
-
# summary = "Code executed"
|
|
377
|
-
|
|
378
|
-
# elif kind == "CommandEvent":
|
|
379
|
-
# if event["data"]["type"] == "FILE_READ":
|
|
380
|
-
# summary = f"Reading file {event['data']['filePath']}..."
|
|
381
|
-
# elif event["data"]["type"] == "FILE_OPEN":
|
|
382
|
-
# summary = f"Opening file {event['data']['filePath']}..."
|
|
383
|
-
# else:
|
|
384
|
-
# summary = "Executing command..."
|
|
385
|
-
|
|
386
|
-
# else:
|
|
387
|
-
# return
|
|
388
|
-
|
|
389
|
-
seqs = [
|
|
390
|
-
clear_line_seq(),
|
|
391
|
-
self._render_block_footer(summary, status="running", nl=False),
|
|
392
|
-
]
|
|
393
|
-
render(seqs)
|
|
394
|
-
|
|
395
|
-
def _render_block_header(self, text: str) -> list[str]:
|
|
396
|
-
return [
|
|
397
|
-
bg_color_seq(self.theme.block_header_bg),
|
|
398
|
-
erase_line_seq(),
|
|
399
|
-
bold_seq(),
|
|
400
|
-
"\r", # Return to start of line
|
|
401
|
-
f" {text} ",
|
|
402
|
-
reset_attrs_seq(),
|
|
403
|
-
"\n",
|
|
404
|
-
]
|
|
405
|
-
|
|
406
|
-
def _render_block_footer(
|
|
407
|
-
self, text: str, status: str | None = None, nl: bool = True
|
|
408
|
-
) -> list[str]:
|
|
409
|
-
seqs: list[Any] = [
|
|
410
|
-
bg_color_seq(self.theme.block_footer_bg),
|
|
411
|
-
erase_line_seq(),
|
|
412
|
-
italic_seq(),
|
|
413
|
-
]
|
|
414
|
-
|
|
415
|
-
if status == "completed":
|
|
416
|
-
seqs.append(
|
|
417
|
-
[
|
|
418
|
-
fg_color_seq(self.theme.green),
|
|
419
|
-
bold_seq(),
|
|
420
|
-
" ✓ ",
|
|
421
|
-
not_bold_seq(),
|
|
422
|
-
]
|
|
423
|
-
)
|
|
424
|
-
elif status == "rejected":
|
|
425
|
-
seqs.append(
|
|
426
|
-
[
|
|
427
|
-
fg_color_seq(self.theme.red),
|
|
428
|
-
bold_seq(),
|
|
429
|
-
" 𐄂 ",
|
|
430
|
-
not_bold_seq(),
|
|
431
|
-
]
|
|
432
|
-
)
|
|
433
|
-
elif status == "running":
|
|
434
|
-
seqs.append(
|
|
435
|
-
[
|
|
436
|
-
fg_color_seq(self.theme.blue),
|
|
437
|
-
bold_seq(),
|
|
438
|
-
" ",
|
|
439
|
-
not_bold_seq(),
|
|
440
|
-
]
|
|
441
|
-
)
|
|
442
|
-
else:
|
|
443
|
-
seqs.append(
|
|
444
|
-
[
|
|
445
|
-
bold_seq(),
|
|
446
|
-
" ",
|
|
447
|
-
not_bold_seq(),
|
|
448
|
-
]
|
|
449
|
-
)
|
|
450
|
-
|
|
451
|
-
seqs.append([fg_color_seq(self.theme.block_footer_fg), text, reset_attrs_seq()])
|
|
452
|
-
|
|
453
|
-
if nl:
|
|
454
|
-
seqs.append(["\n"])
|
|
455
|
-
|
|
456
|
-
return seqs
|
|
457
|
-
|
|
458
|
-
def _render_natural_edit_diff(self, write: dict[str, Any]) -> list[Any]:
|
|
459
|
-
diff = generate_diff(write.get("originalFile") or "", write["newFile"]).strip()
|
|
460
|
-
|
|
461
|
-
highlighted_diff = highlight_code(diff, "udiff", self.theme)
|
|
462
|
-
|
|
463
|
-
return [
|
|
464
|
-
self._render_block_header("Diff"),
|
|
465
|
-
bg_color_seq(self.theme.block_body_bg),
|
|
466
|
-
erase_line_seq(),
|
|
467
|
-
"\n",
|
|
468
|
-
highlighted_diff,
|
|
469
|
-
"\n",
|
|
470
|
-
]
|
|
471
|
-
|
|
472
|
-
def _render_natural_edit_error(self, error: str) -> list[str]:
|
|
473
|
-
return [
|
|
474
|
-
fg_color_seq(1),
|
|
475
|
-
f"\nError: {error.strip()}\n\n",
|
|
476
|
-
reset_attrs_seq(),
|
|
477
|
-
]
|
|
478
|
-
|
|
479
|
-
def _render_code_block_start(self, lang: str) -> list[Any]:
|
|
480
|
-
return [
|
|
481
|
-
self._render_block_header(lang.capitalize()),
|
|
482
|
-
]
|
|
483
|
-
|
|
484
|
-
def _render_code_block_content(self, content: str, lang: str) -> str:
|
|
485
|
-
return highlight_code(content.strip(), lang, self.theme)
|
|
486
|
-
|
|
487
|
-
def _render_code_block_output(self, content: str) -> list[Any]:
|
|
488
|
-
content = re.sub("\n+$", "", content)
|
|
489
|
-
|
|
490
|
-
if content.strip() == "":
|
|
491
|
-
result = [
|
|
492
|
-
[
|
|
493
|
-
" ",
|
|
494
|
-
fg_color_seq(self.theme.dimmed_text_fg),
|
|
495
|
-
bg_color_seq(self.theme.block_body_bg),
|
|
496
|
-
erase_line_seq(),
|
|
497
|
-
"-- no output --\n",
|
|
498
|
-
]
|
|
499
|
-
]
|
|
500
|
-
else:
|
|
501
|
-
lines = pad_left(content, " ").split("\n")
|
|
502
|
-
|
|
503
|
-
result = [
|
|
504
|
-
[bg_color_seq(self.theme.block_body_bg), erase_line_seq(), line, "\n"]
|
|
505
|
-
for line in lines
|
|
506
|
-
]
|
|
507
|
-
|
|
508
|
-
return [
|
|
509
|
-
self._render_block_header("Output"),
|
|
510
|
-
bg_color_seq(self.theme.block_body_bg),
|
|
511
|
-
erase_line_seq(),
|
|
512
|
-
"\n",
|
|
513
|
-
erase_line_seq(),
|
|
514
|
-
result,
|
|
515
|
-
bg_color_seq(self.theme.block_body_bg),
|
|
516
|
-
erase_line_seq(),
|
|
517
|
-
reset_attrs_seq(),
|
|
518
|
-
"\n",
|
|
519
|
-
]
|
|
520
|
-
|
|
521
|
-
def _render_file_write_block_start(self, path: str) -> list[Any]:
|
|
522
|
-
return [
|
|
523
|
-
self._render_block_header(f"Editing file {path}"),
|
|
524
|
-
]
|
|
525
|
-
|
|
526
|
-
def _render_file_write_block_content(self, content: str, lang: str) -> str:
|
|
527
|
-
return highlight_code(
|
|
528
|
-
content.strip(),
|
|
529
|
-
lang,
|
|
530
|
-
self.theme,
|
|
531
|
-
)
|
|
532
|
-
|
|
533
|
-
def _render_file_write_block_result(self, result: str | None) -> list[Any]:
|
|
534
|
-
result = result or "Edit applied"
|
|
535
|
-
return [self._render_block_footer(f"{result}", status="completed")]
|
|
536
|
-
|
|
537
|
-
def _render_command_block_start(self, type_: str) -> list[Any]:
|
|
538
|
-
return [
|
|
539
|
-
self._render_block_header(type_),
|
|
540
|
-
]
|
|
541
|
-
|
|
542
|
-
def _render_command_block_content(self, data: dict[str, Any]) -> list[Any]:
|
|
543
|
-
if data["type"] == "PROTOTYPE":
|
|
544
|
-
content = data["contentRendered"]
|
|
545
|
-
else:
|
|
546
|
-
content = data["filePath"].strip()
|
|
547
|
-
|
|
548
|
-
lines = pad_left(content, " ").split("\n")
|
|
549
|
-
seqs: list[Any] = [bg_color_seq(self.theme.block_body_bg)]
|
|
550
|
-
seqs.append([[erase_line_seq(), line, "\n"] for line in lines])
|
|
551
|
-
|
|
552
|
-
return seqs
|
|
553
|
-
|
|
554
|
-
def _render_command_block_output(
|
|
555
|
-
self, content: str, type_: str, lang: str
|
|
556
|
-
) -> list[Any]:
|
|
557
|
-
if content.strip() == "":
|
|
558
|
-
result = [
|
|
559
|
-
" ",
|
|
560
|
-
fg_color_seq(self.theme.dimmed_text_fg),
|
|
561
|
-
bg_color_seq(self.theme.block_body_bg),
|
|
562
|
-
erase_line_seq(),
|
|
563
|
-
"-- no output --\n",
|
|
564
|
-
]
|
|
565
|
-
else:
|
|
566
|
-
result = [
|
|
567
|
-
highlight_code(
|
|
568
|
-
content.strip(),
|
|
569
|
-
lang,
|
|
570
|
-
self.theme,
|
|
571
|
-
)
|
|
572
|
-
]
|
|
573
|
-
|
|
574
|
-
return [
|
|
575
|
-
self._render_block_header("Output"),
|
|
576
|
-
bg_color_seq(self.theme.block_body_bg),
|
|
577
|
-
erase_line_seq(),
|
|
578
|
-
"\n",
|
|
579
|
-
result,
|
|
580
|
-
bg_color_seq(self.theme.block_body_bg),
|
|
581
|
-
erase_line_seq(),
|
|
582
|
-
"\n",
|
|
583
|
-
self._render_block_footer(
|
|
584
|
-
f"{bold_seq()}{type_}{not_bold_seq()} command executed",
|
|
585
|
-
status="completed",
|
|
586
|
-
),
|
|
587
|
-
]
|
|
588
|
-
|
|
589
|
-
def _render_checkpoint_created_block_start(self) -> list[Any]:
|
|
590
|
-
header_text = "".join(
|
|
591
|
-
[
|
|
592
|
-
bold_seq(),
|
|
593
|
-
"✓ ",
|
|
594
|
-
not_bold_seq(),
|
|
595
|
-
"checkpoint created",
|
|
596
|
-
]
|
|
597
|
-
)
|
|
598
|
-
return [
|
|
599
|
-
self._render_block_header(header_text),
|
|
600
|
-
]
|
|
601
|
-
|
|
602
|
-
def _render_checkpoint_content(self, content: str) -> list[Any]:
|
|
603
|
-
return [
|
|
604
|
-
highlight_code(
|
|
605
|
-
content.strip(),
|
|
606
|
-
"text",
|
|
607
|
-
self.theme,
|
|
608
|
-
),
|
|
609
|
-
]
|
|
610
|
-
|
|
611
|
-
def _render_checkpoint_created_block(self, event: dict[str, Any]) -> list[Any]:
|
|
612
|
-
content = f"{get_short_git_commit_hash(event['commitHash'])}: {event['commitMessage']}"
|
|
613
|
-
|
|
614
|
-
return [
|
|
615
|
-
self._render_checkpoint_created_block_start(),
|
|
616
|
-
self._render_checkpoint_content(content),
|
|
617
|
-
"\n",
|
|
618
|
-
"\n",
|
|
619
|
-
]
|
|
620
|
-
|
|
621
|
-
def _render_checkpoint_rollback_block_start(self) -> list[Any]:
|
|
622
|
-
header_text = "".join(
|
|
623
|
-
[
|
|
624
|
-
bold_seq(),
|
|
625
|
-
"✓ ",
|
|
626
|
-
not_bold_seq(),
|
|
627
|
-
"checkpoint rollback",
|
|
628
|
-
]
|
|
629
|
-
)
|
|
630
|
-
return [
|
|
631
|
-
self._render_block_header(header_text),
|
|
632
|
-
]
|
|
633
|
-
|
|
634
|
-
def _render_checkpoint_rollback_block(self, event: dict[str, Any]) -> list[Any]:
|
|
635
|
-
pretty_date = datetime.fromisoformat(
|
|
636
|
-
event["gitMetadata"]["commit_date"]
|
|
637
|
-
).strftime("%Y-%m-%d %H:%M:%S")
|
|
638
|
-
content = (
|
|
639
|
-
f"{get_short_git_commit_hash(event['commitHash'])}: {event['commitMessage']}\n"
|
|
640
|
-
f"{event['gitMetadata']['author_name']} <{event['gitMetadata']['author_email']}> {pretty_date}"
|
|
641
|
-
)
|
|
642
|
-
|
|
643
|
-
return [
|
|
644
|
-
self._render_checkpoint_rollback_block_start(),
|
|
645
|
-
self._render_checkpoint_content(content),
|
|
646
|
-
"\n",
|
|
647
|
-
"\n",
|
|
648
|
-
]
|
|
649
|
-
|
|
650
|
-
def _render_checkpoint_error_block_start(self) -> list[Any]:
|
|
651
|
-
return [
|
|
652
|
-
self._render_block_header(
|
|
653
|
-
f"{bold_seq()}𐄂 {not_bold_seq()}checkpoint error"
|
|
654
|
-
),
|
|
655
|
-
]
|
|
656
|
-
|
|
657
|
-
def _render_checkpoint_error_block(self, event: dict[str, Any]) -> list[Any]:
|
|
658
|
-
return [
|
|
659
|
-
self._render_checkpoint_error_block_start(),
|
|
660
|
-
self._render_checkpoint_content(event["message"]),
|
|
661
|
-
"\n",
|
|
662
|
-
"\n",
|
|
663
|
-
]
|
|
664
|
-
|
|
665
|
-
def _render_step_block_start(self, title: str) -> list[Any]:
|
|
666
|
-
return [
|
|
667
|
-
self._render_block_header(f"Step: {title}"),
|
|
668
|
-
]
|
|
669
|
-
|
|
670
|
-
def _render_step_block_content(self, content: str) -> list[str]:
|
|
671
|
-
"""Render step content with proper formatting and background color.
|
|
672
|
-
Returns a list of formatted lines ready for rendering.
|
|
673
|
-
"""
|
|
674
|
-
# First reset any previous formatting
|
|
675
|
-
reset_seq = reset_attrs_seq()
|
|
676
|
-
erase_line_seq_str = erase_line_seq()
|
|
677
|
-
|
|
678
|
-
# Apply padding and wrap lines with proper escape sequences
|
|
679
|
-
formatted_lines = []
|
|
680
|
-
lines = content.strip().split("\n")
|
|
681
|
-
|
|
682
|
-
for line in lines:
|
|
683
|
-
# erase_line_seq fills the entire line with the background color
|
|
684
|
-
# We need to return to start of line first with \r
|
|
685
|
-
formatted_line = f"\r{erase_line_seq_str} {line}{reset_seq}\n"
|
|
686
|
-
formatted_lines.append(formatted_line)
|
|
687
|
-
|
|
688
|
-
return formatted_lines
|
|
689
|
-
|
|
690
|
-
def _render_step_result_content(self, content: str) -> list[Any]:
|
|
691
|
-
formatted_content = pad_left(content, " ")
|
|
692
|
-
return [
|
|
693
|
-
self._render_block_header("Step result"),
|
|
694
|
-
formatted_content,
|
|
695
|
-
"\n",
|
|
696
|
-
]
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
class LiveView(BaseView):
|
|
700
|
-
def __init__(self, theme: Theme, render_user_messages: bool = False) -> None:
|
|
701
|
-
super().__init__(theme)
|
|
702
|
-
self.buffer: Buffer = NullBuffer()
|
|
703
|
-
self.command_data: dict[str, Any] | None = None
|
|
704
|
-
self.tool_uses: dict[str, dict[str, Any]] = {}
|
|
705
|
-
self.confirmation_required: bool = True
|
|
706
|
-
self.default_spinner = Spinner("Exponent is working...")
|
|
707
|
-
self.msg_gen_spinner = Spinner("Exponent is thinking real hard...")
|
|
708
|
-
(r, g, b) = theme.thinking_spinner_fg.rgb
|
|
709
|
-
rgb = (round(r * 255), round(g * 255), round(b * 255))
|
|
710
|
-
self.thinking_spinner = ThinkingSpinner(fg_color=rgb)
|
|
711
|
-
self.step_spinner = Spinner("Exponent is working on a step...")
|
|
712
|
-
self.thinking_start_time: float | None = None
|
|
713
|
-
self.exec_spinner = Spinner(
|
|
714
|
-
"Exponent is waiting for the code to finish running..."
|
|
715
|
-
)
|
|
716
|
-
self.diff_spinner = Spinner("Exponent is generating file diff...")
|
|
717
|
-
self.spinner = self.default_spinner
|
|
718
|
-
self.pending_event: dict[str, Any] | None = None
|
|
719
|
-
self.pending_event_list: list[Any] = []
|
|
720
|
-
self.render_user_messages = render_user_messages
|
|
721
|
-
|
|
722
|
-
def render_event(self, kind: str, event: dict[str, Any]) -> None:
|
|
723
|
-
message_type = event["messageData"]["__typename"]
|
|
724
|
-
if kind == "UserEvent" and message_type == "TextMessage":
|
|
725
|
-
self._handle_user_message_event(event)
|
|
726
|
-
if (
|
|
727
|
-
kind == "UserEvent" and message_type == "ToolResultMessage"
|
|
728
|
-
) or message_type == "PartialToolResultMessage":
|
|
729
|
-
self._handle_command_result_event(event)
|
|
730
|
-
elif kind == "AssistantEvent" and message_type == "TextMessage":
|
|
731
|
-
self._handle_message_event(event)
|
|
732
|
-
elif kind == "AssistantEvent" and message_type == "ToolCallMessage":
|
|
733
|
-
self._handle_tool_call_event(event)
|
|
734
|
-
elif kind == "SystemEvent" and message_type == "ToolPermissionStatusMessage":
|
|
735
|
-
if event["messageData"]["permissionStatus"] == "requested":
|
|
736
|
-
self._handle_confirmation_request_event(event)
|
|
737
|
-
else:
|
|
738
|
-
self._handle_confirmation_response_event(event)
|
|
739
|
-
elif kind == "Error":
|
|
740
|
-
print(event)
|
|
741
|
-
# print(json.dumps(event))
|
|
742
|
-
# elif kind == "CodeBlockEvent":
|
|
743
|
-
# self._handle_code_block_event(event)
|
|
744
|
-
# elif (
|
|
745
|
-
# kind == "CommandChunkEvent"
|
|
746
|
-
# and event.get("data", {}).get("__typename") == "ThinkingCommandData"
|
|
747
|
-
# ):
|
|
748
|
-
# # Start the thinking animation and record start time
|
|
749
|
-
# if self.spinner != self.thinking_spinner:
|
|
750
|
-
# if self.spinner:
|
|
751
|
-
# self.spinner.hide()
|
|
752
|
-
# self.spinner = self.thinking_spinner
|
|
753
|
-
# # Record the start time before showing spinner
|
|
754
|
-
# self.thinking_start_time = time.time()
|
|
755
|
-
# # Pass the start time to spinner
|
|
756
|
-
# self.thinking_spinner.start_time = self.thinking_start_time
|
|
757
|
-
# self.spinner.show()
|
|
758
|
-
# elif (
|
|
759
|
-
# kind == "CommandEvent"
|
|
760
|
-
# and event.get("data", {}).get("__typename") == "ThinkingCommandData"
|
|
761
|
-
# ):
|
|
762
|
-
# # Treat CommandEvent with ThinkingCommandData as ThinkingEvent
|
|
763
|
-
# self._handle_thinking_event(
|
|
764
|
-
# {
|
|
765
|
-
# "eventUuid": event["uuid"],
|
|
766
|
-
# "parentUuid": event.get("parentUuid"),
|
|
767
|
-
# "turnUuid": event.get("turnUuid"),
|
|
768
|
-
# "content": event["data"]["content"],
|
|
769
|
-
# }
|
|
770
|
-
# )
|
|
771
|
-
# elif kind == "StepChunkEvent":
|
|
772
|
-
# self._handle_step_chunk_event(event)
|
|
773
|
-
# elif kind == "StepEvent":
|
|
774
|
-
# self._handle_step_event(event)
|
|
775
|
-
# elif kind == "StepConfirmationEvent":
|
|
776
|
-
# self._handle_step_confirmation_event(event)
|
|
777
|
-
# elif kind == "StepExecutionStartEvent":
|
|
778
|
-
# self._handle_step_execution_start_event(event)
|
|
779
|
-
# elif kind == "StepExecutionResultEvent":
|
|
780
|
-
# self._handle_step_execution_result_event(event)
|
|
781
|
-
# elif kind == "FileWriteChunkEvent":
|
|
782
|
-
# self._handle_file_write_chunk_event(event)
|
|
783
|
-
# elif kind == "FileWriteEvent":
|
|
784
|
-
# self._handle_file_write_event(event)
|
|
785
|
-
# elif kind == "FileWriteConfirmationEvent":
|
|
786
|
-
# self._handle_file_write_confirmation_event(event)
|
|
787
|
-
# elif kind == "FileWriteStartEvent":
|
|
788
|
-
# self._handle_file_write_start_event(event)
|
|
789
|
-
# elif kind == "FileWriteResultEvent":
|
|
790
|
-
# self._handle_file_write_result_event(event)
|
|
791
|
-
# elif kind == "CodeBlockChunkEvent":
|
|
792
|
-
# self._handle_code_block_chunk_event(event)
|
|
793
|
-
# elif kind == "CodeBlockEvent":
|
|
794
|
-
# self._handle_code_block_event(event)
|
|
795
|
-
# elif kind == "CodeBlockConfirmationEvent":
|
|
796
|
-
# self._handle_code_block_confirmation_event(event)
|
|
797
|
-
# elif kind == "CodeExecutionStartEvent":
|
|
798
|
-
# self._handle_code_execution_start_event(event)
|
|
799
|
-
# elif kind == "CodeExecutionEvent":
|
|
800
|
-
# self._handle_code_execution_event(event)
|
|
801
|
-
# elif kind == "CommandChunkEvent":
|
|
802
|
-
# if event["data"]["type"] in ["FILE_READ", "FILE_OPEN", "PROTOTYPE"]:
|
|
803
|
-
# self._handle_command_chunk_event(event)
|
|
804
|
-
# elif kind == "CommandEvent":
|
|
805
|
-
# if event["data"]["type"] in ["FILE_READ", "FILE_OPEN", "PROTOTYPE"]:
|
|
806
|
-
# self._handle_tool_call_event(event)
|
|
807
|
-
# elif kind == "CommandConfirmationEvent":
|
|
808
|
-
# self._handle_command_confirmation_event(event)
|
|
809
|
-
# elif kind == "CommandStartEvent":
|
|
810
|
-
# self._handle_command_start_event(event)
|
|
811
|
-
# elif kind == "CommandResultEvent":
|
|
812
|
-
# self._handle_command_result_event(event)
|
|
813
|
-
# elif kind == "Error" or kind == "ContextLimitExceededError":
|
|
814
|
-
# error_message = event["message"]
|
|
815
|
-
# raise ExponentError(error_message)
|
|
816
|
-
# elif kind == "RateLimitError":
|
|
817
|
-
# error_message = event["message"]
|
|
818
|
-
# raise RateLimitError(error_message)
|
|
819
|
-
# elif kind == "CheckpointCreatedEvent":
|
|
820
|
-
# self._handle_checkpoint_created_event(event)
|
|
821
|
-
# elif kind == "CheckpointRollbackEvent":
|
|
822
|
-
# self._handle_checkpoint_rollback_event(event)
|
|
823
|
-
# elif kind == "CheckpointError":
|
|
824
|
-
# self._handle_checkpoint_error_event(event)
|
|
825
|
-
|
|
826
|
-
def start_turn(self) -> None:
|
|
827
|
-
self.spinner.show()
|
|
828
|
-
|
|
829
|
-
def end_turn(self) -> None:
|
|
830
|
-
self.spinner.hide()
|
|
831
|
-
|
|
832
|
-
@pause_spinner
|
|
833
|
-
def _handle_message_start_event(self, event: dict[str, Any]) -> None:
|
|
834
|
-
self.spinner = self.msg_gen_spinner
|
|
835
|
-
|
|
836
|
-
@pause_spinner
|
|
837
|
-
def _handle_thinking_start_event(self, event: dict[str, Any]) -> None:
|
|
838
|
-
# Use ThinkingSpinner for a specialized thinking animation
|
|
839
|
-
self.spinner = self.thinking_spinner
|
|
840
|
-
self.spinner.show()
|
|
841
|
-
# Record the start time
|
|
842
|
-
self.thinking_start_time = time.time()
|
|
843
|
-
|
|
844
|
-
@pause_spinner
|
|
845
|
-
def _handle_thinking_event(self, event: dict[str, Any]) -> None:
|
|
846
|
-
# This handles the thinking event completion
|
|
847
|
-
if self.spinner == self.thinking_spinner:
|
|
848
|
-
self.spinner.hide()
|
|
849
|
-
self.spinner = self.default_spinner
|
|
850
|
-
|
|
851
|
-
# Calculate thinking duration
|
|
852
|
-
if self.thinking_start_time is not None:
|
|
853
|
-
duration = time.time() - self.thinking_start_time
|
|
854
|
-
# Format duration nicely
|
|
855
|
-
if duration < 1:
|
|
856
|
-
duration_str = f"{int(duration * 1000)}ms"
|
|
857
|
-
elif duration < 60:
|
|
858
|
-
# Round to nearest second to match spinner display
|
|
859
|
-
duration_str = f"{int(duration)}s"
|
|
860
|
-
else:
|
|
861
|
-
mins = int(duration // 60)
|
|
862
|
-
secs = int(duration % 60)
|
|
863
|
-
duration_str = f"{mins}m {secs}s"
|
|
864
|
-
|
|
865
|
-
# Improved message with more natural language and decorations
|
|
866
|
-
# Use render() instead of print() to ensure proper terminal rendering
|
|
867
|
-
render(
|
|
868
|
-
f"\r {fg_color_seq(self.theme.dimmed_text_fg)}Exponent thought for {duration_str}{reset_attrs_seq()}\n\n"
|
|
869
|
-
)
|
|
870
|
-
|
|
871
|
-
# Reset the start time
|
|
872
|
-
self.thinking_start_time = None
|
|
873
|
-
|
|
874
|
-
@stop_spinner
|
|
875
|
-
def _handle_message_chunk_event(self, event: dict[str, Any]) -> None:
|
|
876
|
-
event_uuid = event["uuid"]
|
|
877
|
-
seqs = []
|
|
878
|
-
|
|
879
|
-
if self.buffer.event_uuid != event_uuid:
|
|
880
|
-
self.buffer = CharBuffer(event_uuid, get_term_width())
|
|
881
|
-
|
|
882
|
-
assert isinstance(self.buffer, CharBuffer)
|
|
883
|
-
|
|
884
|
-
seqs.append(self.buffer.render_new_chars(event["content"]))
|
|
885
|
-
render(seqs)
|
|
886
|
-
|
|
887
|
-
@pause_spinner
|
|
888
|
-
def _handle_message_event(self, event: dict[str, Any]) -> None:
|
|
889
|
-
event_uuid = event["uuid"]
|
|
890
|
-
seqs = []
|
|
891
|
-
|
|
892
|
-
if self.buffer.event_uuid != event_uuid:
|
|
893
|
-
self.buffer = CharBuffer(event_uuid, get_term_width())
|
|
894
|
-
|
|
895
|
-
assert isinstance(self.buffer, CharBuffer)
|
|
896
|
-
|
|
897
|
-
seqs.append(self.buffer.render_new_chars(event["messageData"]["text"]))
|
|
898
|
-
seqs.append(["\n\n"])
|
|
899
|
-
render(seqs)
|
|
900
|
-
|
|
901
|
-
def _handle_user_message_event(self, event: dict[str, Any]) -> None:
|
|
902
|
-
event_uuid = event["uuid"]
|
|
903
|
-
seqs = []
|
|
904
|
-
|
|
905
|
-
if self.buffer.event_uuid != event_uuid:
|
|
906
|
-
self.buffer = CharBuffer(event_uuid, get_term_width())
|
|
907
|
-
|
|
908
|
-
assert isinstance(self.buffer, CharBuffer)
|
|
909
|
-
|
|
910
|
-
seqs.append(self.buffer.render_new_chars(f"> {event['messageData']['text']}"))
|
|
911
|
-
seqs.append(["\n\n"])
|
|
912
|
-
render(seqs)
|
|
913
|
-
|
|
914
|
-
@stop_spinner
|
|
915
|
-
def _handle_file_write_chunk_event(self, event: dict[str, Any]) -> None:
|
|
916
|
-
event_uuid = event["uuid"]
|
|
917
|
-
seqs: list[Any] = []
|
|
918
|
-
|
|
919
|
-
if self.buffer.event_uuid != event_uuid:
|
|
920
|
-
self.buffer = LineBuffer(event_uuid)
|
|
921
|
-
|
|
922
|
-
seqs.append(
|
|
923
|
-
[
|
|
924
|
-
self._render_block_header(f"Editing file {event['filePath']}"),
|
|
925
|
-
bg_color_seq(self.theme.block_body_bg),
|
|
926
|
-
erase_line_seq(),
|
|
927
|
-
"\n",
|
|
928
|
-
]
|
|
929
|
-
)
|
|
930
|
-
|
|
931
|
-
assert isinstance(self.buffer, LineBuffer)
|
|
932
|
-
|
|
933
|
-
write = event["writeContent"]
|
|
934
|
-
content = write.get("naturalEdit") or write.get("content") or event["content"]
|
|
935
|
-
|
|
936
|
-
formatted_content = self._render_file_write_block_content(
|
|
937
|
-
content, event["language"]
|
|
938
|
-
)
|
|
939
|
-
|
|
940
|
-
seqs.append(self.buffer.render_new_lines(formatted_content, self.theme))
|
|
941
|
-
|
|
942
|
-
if (
|
|
943
|
-
"intermediateEdit" in write and write["intermediateEdit"] is not None
|
|
944
|
-
): # natural edit
|
|
945
|
-
# when intermediateEdit is present in writeContent dict
|
|
946
|
-
# it indicates the server is generating original/new file pair
|
|
947
|
-
seqs.append(["\n", "\n"])
|
|
948
|
-
render(seqs)
|
|
949
|
-
self.spinner = self.diff_spinner
|
|
950
|
-
self.spinner.show()
|
|
951
|
-
else:
|
|
952
|
-
render(seqs)
|
|
953
|
-
|
|
954
|
-
@pause_spinner
|
|
955
|
-
def _handle_file_write_event(self, event: dict[str, Any]) -> None:
|
|
956
|
-
event_uuid = event["uuid"]
|
|
957
|
-
seqs: list[Any] = []
|
|
958
|
-
|
|
959
|
-
if self.buffer.event_uuid != event_uuid:
|
|
960
|
-
self.buffer = LineBuffer(event_uuid)
|
|
961
|
-
seqs.append(self._render_file_write_block_start(event["filePath"]))
|
|
962
|
-
|
|
963
|
-
assert isinstance(self.buffer, LineBuffer)
|
|
964
|
-
|
|
965
|
-
write = event["writeContent"]
|
|
966
|
-
|
|
967
|
-
if "newFile" in write: # natural edit
|
|
968
|
-
seqs.append(
|
|
969
|
-
[
|
|
970
|
-
"\r",
|
|
971
|
-
move_cursor_up_seq(2),
|
|
972
|
-
erase_display_seq(),
|
|
973
|
-
]
|
|
974
|
-
)
|
|
975
|
-
|
|
976
|
-
content = write.get("naturalEdit") or write.get("content") or event["content"]
|
|
977
|
-
|
|
978
|
-
formatted_content = self._render_file_write_block_content(
|
|
979
|
-
content, event["language"]
|
|
980
|
-
)
|
|
981
|
-
|
|
982
|
-
seqs.append(
|
|
983
|
-
[
|
|
984
|
-
self.buffer.render_new_lines(formatted_content, self.theme),
|
|
985
|
-
bg_color_seq(self.theme.block_body_bg),
|
|
986
|
-
erase_line_seq(),
|
|
987
|
-
"\n",
|
|
988
|
-
]
|
|
989
|
-
)
|
|
990
|
-
|
|
991
|
-
error = write.get("errorContent")
|
|
992
|
-
|
|
993
|
-
if error is None:
|
|
994
|
-
if write.get("newFile") is not None:
|
|
995
|
-
seqs.append(self._render_natural_edit_diff(write))
|
|
996
|
-
|
|
997
|
-
if event["requireConfirmation"]:
|
|
998
|
-
seqs.append(
|
|
999
|
-
[
|
|
1000
|
-
self._render_block_footer(
|
|
1001
|
-
f"Confirm edit with {bold_seq()}<ctrl+y>{not_bold_seq()}, send a new message to dismiss code changes."
|
|
1002
|
-
),
|
|
1003
|
-
"\n",
|
|
1004
|
-
]
|
|
1005
|
-
)
|
|
1006
|
-
else:
|
|
1007
|
-
seqs.append(self._render_natural_edit_error(error))
|
|
1008
|
-
|
|
1009
|
-
render(seqs)
|
|
1010
|
-
self.confirmation_required = event["requireConfirmation"]
|
|
1011
|
-
|
|
1012
|
-
@pause_spinner
|
|
1013
|
-
def _handle_file_write_confirmation_event(self, event: dict[str, Any]) -> None:
|
|
1014
|
-
if self.buffer.event_uuid != event["fileWriteUuid"]:
|
|
1015
|
-
return
|
|
1016
|
-
|
|
1017
|
-
seqs = []
|
|
1018
|
-
|
|
1019
|
-
if event["accepted"]:
|
|
1020
|
-
seqs.append(
|
|
1021
|
-
[
|
|
1022
|
-
move_cursor_up_seq(2),
|
|
1023
|
-
self._render_block_footer("Applying edit...", status="running"),
|
|
1024
|
-
"\n",
|
|
1025
|
-
]
|
|
1026
|
-
)
|
|
1027
|
-
elif self.confirmation_required:
|
|
1028
|
-
# user entered new prompt, cursor moved down
|
|
1029
|
-
# therefore we need to move it up, redraw status, and move it
|
|
1030
|
-
# back where it was
|
|
1031
|
-
|
|
1032
|
-
seqs.append(
|
|
1033
|
-
[
|
|
1034
|
-
move_cursor_up_seq(4),
|
|
1035
|
-
self._render_block_footer("Edit dismissed", status="rejected"),
|
|
1036
|
-
"\n\n\n",
|
|
1037
|
-
]
|
|
1038
|
-
)
|
|
1039
|
-
|
|
1040
|
-
render(seqs)
|
|
1041
|
-
|
|
1042
|
-
@pause_spinner
|
|
1043
|
-
def _handle_file_write_start_event(self, event: dict[str, Any]) -> None:
|
|
1044
|
-
return
|
|
1045
|
-
|
|
1046
|
-
@pause_spinner
|
|
1047
|
-
def _handle_file_write_result_event(self, event: dict[str, Any]) -> None:
|
|
1048
|
-
if self.buffer.event_uuid != event["fileWriteUuid"]:
|
|
1049
|
-
return
|
|
1050
|
-
|
|
1051
|
-
seqs: list[Any] = []
|
|
1052
|
-
|
|
1053
|
-
if self.confirmation_required:
|
|
1054
|
-
seqs.append([move_cursor_up_seq(2)])
|
|
1055
|
-
|
|
1056
|
-
seqs.append([self._render_file_write_block_result(event["content"]), "\n"])
|
|
1057
|
-
|
|
1058
|
-
render(seqs)
|
|
1059
|
-
|
|
1060
|
-
@stop_spinner
|
|
1061
|
-
def _handle_command_chunk_event(self, event: dict[str, Any]) -> None:
|
|
1062
|
-
data = event["data"]
|
|
1063
|
-
|
|
1064
|
-
# Ignore thinking chunks
|
|
1065
|
-
if data["type"] == "THINKING":
|
|
1066
|
-
return
|
|
1067
|
-
|
|
1068
|
-
if (
|
|
1069
|
-
data["type"] == "PROTOTYPE"
|
|
1070
|
-
and data["commandName"] in ["search_files", "glob"]
|
|
1071
|
-
) or data["type"] == "FILE_READ":
|
|
1072
|
-
# We want to render the _final_ regex in the header, so we don't render any chunk,
|
|
1073
|
-
# instead we'll render the header below in _handle_command_event.
|
|
1074
|
-
return
|
|
1075
|
-
|
|
1076
|
-
type_ = self._get_command_type(data)
|
|
1077
|
-
|
|
1078
|
-
event_uuid = event["uuid"]
|
|
1079
|
-
seqs = []
|
|
1080
|
-
|
|
1081
|
-
if self.buffer.event_uuid != event_uuid:
|
|
1082
|
-
self.buffer = CommandBuffer(event_uuid)
|
|
1083
|
-
|
|
1084
|
-
seqs.append(
|
|
1085
|
-
[
|
|
1086
|
-
self._render_block_header(type_),
|
|
1087
|
-
bg_color_seq(self.theme.block_body_bg),
|
|
1088
|
-
erase_line_seq(),
|
|
1089
|
-
"\n",
|
|
1090
|
-
]
|
|
1091
|
-
)
|
|
1092
|
-
|
|
1093
|
-
assert isinstance(self.buffer, CommandBuffer)
|
|
1094
|
-
|
|
1095
|
-
# NOTE: not rendering the contentRendered here incrementally
|
|
1096
|
-
# because it's changing non-incrementally and breaks the layout
|
|
1097
|
-
|
|
1098
|
-
render(seqs)
|
|
1099
|
-
|
|
1100
|
-
@pause_spinner
|
|
1101
|
-
def _handle_confirmation_request_event(self, event: dict[str, Any]) -> None:
|
|
1102
|
-
self.pending_event = event
|
|
1103
|
-
self.pending_event_list.append(event)
|
|
1104
|
-
render(
|
|
1105
|
-
[
|
|
1106
|
-
self._render_block_footer(
|
|
1107
|
-
f"Confirm with {bold_seq()}<ctrl+y>{not_bold_seq()}, send a new message to dismiss code changes."
|
|
1108
|
-
),
|
|
1109
|
-
"\n",
|
|
1110
|
-
]
|
|
1111
|
-
)
|
|
1112
|
-
|
|
1113
|
-
@pause_spinner
|
|
1114
|
-
def _handle_confirmation_response_event(self, event: dict[str, Any]) -> None:
|
|
1115
|
-
self.pending_event_list.pop()
|
|
1116
|
-
|
|
1117
|
-
@pause_spinner
|
|
1118
|
-
def _handle_tool_call_event(self, event: dict[str, Any]) -> None:
|
|
1119
|
-
data = event["messageData"]
|
|
1120
|
-
event_uuid = event["uuid"]
|
|
1121
|
-
command_name = data["toolName"]
|
|
1122
|
-
|
|
1123
|
-
if self.buffer.event_uuid != event_uuid:
|
|
1124
|
-
if command_name == "read_file":
|
|
1125
|
-
self.buffer = CommandBuffer(event_uuid)
|
|
1126
|
-
self._handle_read_file_event(event)
|
|
1127
|
-
return
|
|
1128
|
-
elif command_name == "bash":
|
|
1129
|
-
self.buffer = LineBuffer(event_uuid)
|
|
1130
|
-
self._handle_bash_event(event)
|
|
1131
|
-
return
|
|
1132
|
-
|
|
1133
|
-
@pause_spinner
|
|
1134
|
-
def _handle_command_confirmation_event(self, event: dict[str, Any]) -> None:
|
|
1135
|
-
if self.buffer.event_uuid != event["commandUuid"]:
|
|
1136
|
-
return
|
|
1137
|
-
|
|
1138
|
-
seqs = []
|
|
1139
|
-
|
|
1140
|
-
if event["accepted"]:
|
|
1141
|
-
seqs.append(
|
|
1142
|
-
[
|
|
1143
|
-
move_cursor_up_seq(2),
|
|
1144
|
-
self._render_block_footer("Executing command...", status="running"),
|
|
1145
|
-
"\n",
|
|
1146
|
-
]
|
|
1147
|
-
)
|
|
1148
|
-
else:
|
|
1149
|
-
# user entered new prompt, cursor moved down
|
|
1150
|
-
# therefore we need to move it up, redraw status, and move it
|
|
1151
|
-
# back where it was
|
|
1152
|
-
|
|
1153
|
-
seqs.append(
|
|
1154
|
-
[
|
|
1155
|
-
move_cursor_up_seq(4),
|
|
1156
|
-
self._render_block_footer(
|
|
1157
|
-
"Command did not execute", status="rejected"
|
|
1158
|
-
),
|
|
1159
|
-
"\n\n\n",
|
|
1160
|
-
]
|
|
1161
|
-
)
|
|
1162
|
-
|
|
1163
|
-
render(seqs)
|
|
1164
|
-
|
|
1165
|
-
def _handle_command_start_event(self, event: dict[str, Any]) -> None:
|
|
1166
|
-
return
|
|
1167
|
-
|
|
1168
|
-
@pause_spinner
|
|
1169
|
-
def _handle_command_result_event(self, event: dict[str, Any]) -> None:
|
|
1170
|
-
if self.command_data is None:
|
|
1171
|
-
return
|
|
1172
|
-
|
|
1173
|
-
seqs: list[Any] = []
|
|
1174
|
-
|
|
1175
|
-
# For FILE_READ commands, only show that the command ran successfully, not the file contents
|
|
1176
|
-
if self.command_data["toolName"] == "read_file":
|
|
1177
|
-
seqs.append(
|
|
1178
|
-
[
|
|
1179
|
-
self._render_block_footer(
|
|
1180
|
-
"File read successfully", status="completed"
|
|
1181
|
-
),
|
|
1182
|
-
"\n",
|
|
1183
|
-
]
|
|
1184
|
-
)
|
|
1185
|
-
else:
|
|
1186
|
-
seqs.append(
|
|
1187
|
-
[
|
|
1188
|
-
self._render_command_block_output(
|
|
1189
|
-
event["messageData"]["text"],
|
|
1190
|
-
self.command_data["toolName"],
|
|
1191
|
-
"bash",
|
|
1192
|
-
),
|
|
1193
|
-
"\n",
|
|
1194
|
-
]
|
|
1195
|
-
)
|
|
1196
|
-
|
|
1197
|
-
render(seqs)
|
|
1198
|
-
self.command_data = None
|
|
1199
|
-
|
|
1200
|
-
@stop_spinner
|
|
1201
|
-
def _handle_code_block_chunk_event(self, event: dict[str, Any]) -> None:
|
|
1202
|
-
event_uuid = event["uuid"]
|
|
1203
|
-
seqs = []
|
|
1204
|
-
|
|
1205
|
-
if self.buffer.event_uuid != event_uuid:
|
|
1206
|
-
self.buffer = LineBuffer(event_uuid)
|
|
1207
|
-
|
|
1208
|
-
seqs.append(
|
|
1209
|
-
[
|
|
1210
|
-
self._render_block_header(event["language"].capitalize()),
|
|
1211
|
-
bg_color_seq(self.theme.block_body_bg),
|
|
1212
|
-
erase_line_seq(),
|
|
1213
|
-
"\n",
|
|
1214
|
-
]
|
|
1215
|
-
)
|
|
1216
|
-
|
|
1217
|
-
assert isinstance(self.buffer, LineBuffer)
|
|
1218
|
-
|
|
1219
|
-
formatted_content = self._render_code_block_content(
|
|
1220
|
-
event["content"], event["language"]
|
|
1221
|
-
)
|
|
1222
|
-
|
|
1223
|
-
seqs.append(self.buffer.render_new_lines(formatted_content, self.theme))
|
|
1224
|
-
render(seqs)
|
|
1225
|
-
|
|
1226
|
-
@pause_spinner
|
|
1227
|
-
def _handle_read_file_event(self, event: dict[str, Any]) -> None:
|
|
1228
|
-
data = event["messageData"]
|
|
1229
|
-
tool_input = data["toolInput"]
|
|
1230
|
-
seqs: list[Any] = []
|
|
1231
|
-
|
|
1232
|
-
path = tool_input["filePath"]
|
|
1233
|
-
seqs.append(self._render_command_block_start(f"Read file: {path}"))
|
|
1234
|
-
|
|
1235
|
-
assert isinstance(self.buffer, CommandBuffer)
|
|
1236
|
-
|
|
1237
|
-
render(seqs)
|
|
1238
|
-
self.command_data = data
|
|
1239
|
-
# self.tool_uses[tool_input["tool_use_id"]] =
|
|
1240
|
-
|
|
1241
|
-
@pause_spinner
|
|
1242
|
-
def _handle_bash_event(self, event: dict[str, Any]) -> None:
|
|
1243
|
-
data = event["messageData"]
|
|
1244
|
-
event_uuid = event["uuid"]
|
|
1245
|
-
seqs: list[Any] = []
|
|
1246
|
-
|
|
1247
|
-
if self.buffer.event_uuid != event_uuid:
|
|
1248
|
-
self.buffer = LineBuffer(event_uuid)
|
|
1249
|
-
seqs.append(self._render_code_block_start("bash"))
|
|
1250
|
-
|
|
1251
|
-
assert isinstance(self.buffer, LineBuffer)
|
|
1252
|
-
|
|
1253
|
-
formatted_content = self._render_code_block_content(
|
|
1254
|
-
event["messageData"]["toolInput"]["command"], "bash"
|
|
1255
|
-
)
|
|
1256
|
-
|
|
1257
|
-
seqs.append(
|
|
1258
|
-
[
|
|
1259
|
-
self.buffer.render_new_lines(formatted_content, self.theme),
|
|
1260
|
-
bg_color_seq(self.theme.block_body_bg),
|
|
1261
|
-
erase_line_seq(),
|
|
1262
|
-
"\n",
|
|
1263
|
-
]
|
|
1264
|
-
)
|
|
1265
|
-
|
|
1266
|
-
render(seqs)
|
|
1267
|
-
self.command_data = data
|
|
1268
|
-
|
|
1269
|
-
@pause_spinner
|
|
1270
|
-
def _handle_code_block_event(self, event: dict[str, Any]) -> None:
|
|
1271
|
-
event_uuid = event["uuid"]
|
|
1272
|
-
seqs: list[Any] = []
|
|
1273
|
-
|
|
1274
|
-
if self.buffer.event_uuid != event_uuid:
|
|
1275
|
-
self.buffer = LineBuffer(event_uuid)
|
|
1276
|
-
seqs.append(self._render_code_block_start(event["language"]))
|
|
1277
|
-
|
|
1278
|
-
assert isinstance(self.buffer, LineBuffer)
|
|
1279
|
-
|
|
1280
|
-
formatted_content = self._render_code_block_content(
|
|
1281
|
-
event["content"], event["language"]
|
|
1282
|
-
)
|
|
1283
|
-
|
|
1284
|
-
seqs.append(
|
|
1285
|
-
[
|
|
1286
|
-
self.buffer.render_new_lines(formatted_content, self.theme),
|
|
1287
|
-
bg_color_seq(self.theme.block_body_bg),
|
|
1288
|
-
erase_line_seq(),
|
|
1289
|
-
"\n",
|
|
1290
|
-
]
|
|
1291
|
-
)
|
|
1292
|
-
|
|
1293
|
-
if event["requireConfirmation"]:
|
|
1294
|
-
seqs.append(
|
|
1295
|
-
[
|
|
1296
|
-
self._render_block_footer(
|
|
1297
|
-
f"Run it with {bold_seq()}<ctrl+y>{not_bold_seq()}, send a new message to cancel this request."
|
|
1298
|
-
),
|
|
1299
|
-
"\n",
|
|
1300
|
-
]
|
|
1301
|
-
)
|
|
1302
|
-
|
|
1303
|
-
render(seqs)
|
|
1304
|
-
self.confirmation_required = event["requireConfirmation"]
|
|
1305
|
-
|
|
1306
|
-
@pause_spinner
|
|
1307
|
-
def _handle_code_block_confirmation_event(self, event: dict[str, Any]) -> None:
|
|
1308
|
-
if self.buffer.event_uuid != event["codeBlockUuid"]:
|
|
1309
|
-
return
|
|
1310
|
-
|
|
1311
|
-
seqs = []
|
|
1312
|
-
|
|
1313
|
-
if event["accepted"]:
|
|
1314
|
-
seqs.append(
|
|
1315
|
-
[
|
|
1316
|
-
move_cursor_up_seq(2),
|
|
1317
|
-
self._render_block_footer("Running code...", status="running"),
|
|
1318
|
-
"\n",
|
|
1319
|
-
]
|
|
1320
|
-
)
|
|
1321
|
-
else:
|
|
1322
|
-
# user entered new prompt, cursor moved down
|
|
1323
|
-
# therefore we need to move it up, redraw status, and move it
|
|
1324
|
-
# back where it was
|
|
1325
|
-
|
|
1326
|
-
seqs.append(
|
|
1327
|
-
[
|
|
1328
|
-
move_cursor_up_seq(4),
|
|
1329
|
-
self._render_block_footer(
|
|
1330
|
-
"Code did not execute", status="rejected"
|
|
1331
|
-
),
|
|
1332
|
-
"\n\n\n",
|
|
1333
|
-
]
|
|
1334
|
-
)
|
|
1335
|
-
|
|
1336
|
-
render(seqs)
|
|
1337
|
-
|
|
1338
|
-
@pause_spinner
|
|
1339
|
-
def _handle_code_execution_start_event(self, event: dict[str, Any]) -> None:
|
|
1340
|
-
self.spinner = self.exec_spinner
|
|
1341
|
-
|
|
1342
|
-
@pause_spinner
|
|
1343
|
-
def _handle_code_execution_event(self, event: dict[str, Any]) -> None:
|
|
1344
|
-
if self.buffer.event_uuid != event["codeBlockUuid"]:
|
|
1345
|
-
return
|
|
1346
|
-
|
|
1347
|
-
seqs: list[Any] = []
|
|
1348
|
-
|
|
1349
|
-
if self.confirmation_required:
|
|
1350
|
-
seqs.append([move_cursor_up_seq(2)])
|
|
1351
|
-
|
|
1352
|
-
seqs.append([self._render_code_block_output(event["content"]), "\n"])
|
|
1353
|
-
|
|
1354
|
-
render(seqs)
|
|
1355
|
-
|
|
1356
|
-
def _get_command_type(self, data: dict[str, Any]) -> str:
|
|
1357
|
-
if data["type"] == "PROTOTYPE":
|
|
1358
|
-
return " ".join(
|
|
1359
|
-
[word.capitalize() for word in data["commandName"].split("_")]
|
|
1360
|
-
)
|
|
1361
|
-
elif data["type"] == "FILE_READ":
|
|
1362
|
-
return "Read File"
|
|
1363
|
-
elif data["type"] == "FILE_OPEN":
|
|
1364
|
-
return "Open File"
|
|
1365
|
-
else:
|
|
1366
|
-
return str(data["type"]).title().replace("_", " ")
|
|
1367
|
-
|
|
1368
|
-
def _handle_checkpoint_created_event(self, event: dict[str, Any]) -> None:
|
|
1369
|
-
render(self._render_checkpoint_created_block(event))
|
|
1370
|
-
|
|
1371
|
-
def _handle_checkpoint_rollback_event(self, event: dict[str, Any]) -> None:
|
|
1372
|
-
render(self._render_checkpoint_rollback_block(event))
|
|
1373
|
-
|
|
1374
|
-
def _handle_checkpoint_error_event(self, event: dict[str, Any]) -> None:
|
|
1375
|
-
render(self._render_checkpoint_error_block(event))
|
|
1376
|
-
|
|
1377
|
-
@pause_spinner
|
|
1378
|
-
def _handle_step_chunk_event(self, event: dict[str, Any]) -> None:
|
|
1379
|
-
"""Handle streaming chunks for step events.
|
|
1380
|
-
|
|
1381
|
-
Simply shows a spinner while streaming step chunks.
|
|
1382
|
-
We'll let the final StepEvent handle the full rendering.
|
|
1383
|
-
"""
|
|
1384
|
-
# Just show a spinner for step chunks coming in
|
|
1385
|
-
# Don't render partial content to avoid duplication
|
|
1386
|
-
self.spinner = self.step_spinner
|
|
1387
|
-
self.spinner.show()
|
|
1388
|
-
|
|
1389
|
-
@pause_spinner
|
|
1390
|
-
def _handle_step_event(self, event: dict[str, Any]) -> None:
|
|
1391
|
-
"""Handle a step event by rendering it with proper ANSI formatting.
|
|
1392
|
-
|
|
1393
|
-
This is modeled after the file_write_event handler for consistency.
|
|
1394
|
-
"""
|
|
1395
|
-
event_uuid = event["uuid"]
|
|
1396
|
-
seqs = []
|
|
1397
|
-
|
|
1398
|
-
self.buffer = LineBuffer(event_uuid)
|
|
1399
|
-
|
|
1400
|
-
# Get title with fallback
|
|
1401
|
-
title = event.get("stepTitle") or "Step"
|
|
1402
|
-
seqs.append(self._render_block_header(f"Step: {title}"))
|
|
1403
|
-
|
|
1404
|
-
# Get content with fallbacks and format it
|
|
1405
|
-
content = (
|
|
1406
|
-
event.get("stepContent")
|
|
1407
|
-
or event.get("stepDescription", "")
|
|
1408
|
-
or event.get("description", "")
|
|
1409
|
-
)
|
|
1410
|
-
|
|
1411
|
-
if content:
|
|
1412
|
-
# Format lines with proper background color
|
|
1413
|
-
lines = content.strip().split("\n")
|
|
1414
|
-
formatted_lines = []
|
|
1415
|
-
for line in lines:
|
|
1416
|
-
formatted_lines.append(
|
|
1417
|
-
[
|
|
1418
|
-
erase_line_seq(),
|
|
1419
|
-
" " + line,
|
|
1420
|
-
reset_attrs_seq(),
|
|
1421
|
-
"\n",
|
|
1422
|
-
]
|
|
1423
|
-
)
|
|
1424
|
-
seqs.extend(formatted_lines)
|
|
1425
|
-
|
|
1426
|
-
seqs.append(["\n"])
|
|
1427
|
-
|
|
1428
|
-
# Check if this step requires confirmation
|
|
1429
|
-
require_confirmation = event.get("requireConfirmation", False)
|
|
1430
|
-
if require_confirmation:
|
|
1431
|
-
seqs.append(
|
|
1432
|
-
[
|
|
1433
|
-
*self._render_block_footer(
|
|
1434
|
-
"Confirm step with <ctrl+y>. Sending a new message will cancel this step."
|
|
1435
|
-
),
|
|
1436
|
-
"\n",
|
|
1437
|
-
]
|
|
1438
|
-
)
|
|
1439
|
-
|
|
1440
|
-
# Render it all at once
|
|
1441
|
-
render(seqs)
|
|
1442
|
-
|
|
1443
|
-
# Store confirmation state and step event for later
|
|
1444
|
-
self.confirmation_required = require_confirmation
|
|
1445
|
-
if require_confirmation:
|
|
1446
|
-
self.pending_event = event
|
|
1447
|
-
|
|
1448
|
-
@pause_spinner
|
|
1449
|
-
def _handle_step_confirmation_event(self, event: dict[str, Any]) -> None:
|
|
1450
|
-
"""Handle confirmation events for steps.
|
|
1451
|
-
|
|
1452
|
-
Updates the UI to show that a step has been confirmed or rejected.
|
|
1453
|
-
"""
|
|
1454
|
-
# Copy exactly the file_write_confirmation_event handler pattern
|
|
1455
|
-
# This pattern is known to work correctly
|
|
1456
|
-
if self.buffer.event_uuid != event["stepUuid"]:
|
|
1457
|
-
return
|
|
1458
|
-
|
|
1459
|
-
seqs = []
|
|
1460
|
-
|
|
1461
|
-
if event["accepted"]:
|
|
1462
|
-
seqs.append(
|
|
1463
|
-
[
|
|
1464
|
-
move_cursor_up_seq(2),
|
|
1465
|
-
self._render_block_footer("Executing step...", status="running"),
|
|
1466
|
-
"\n",
|
|
1467
|
-
]
|
|
1468
|
-
)
|
|
1469
|
-
else:
|
|
1470
|
-
# user entered new prompt, cursor moved down
|
|
1471
|
-
# therefore we need to move it up, redraw status, and move it
|
|
1472
|
-
# back where it was
|
|
1473
|
-
seqs.append(
|
|
1474
|
-
[
|
|
1475
|
-
move_cursor_up_seq(4),
|
|
1476
|
-
self._render_block_footer(
|
|
1477
|
-
"Step did not execute", status="rejected"
|
|
1478
|
-
),
|
|
1479
|
-
"\n\n\n",
|
|
1480
|
-
]
|
|
1481
|
-
)
|
|
1482
|
-
|
|
1483
|
-
render(seqs)
|
|
1484
|
-
|
|
1485
|
-
# If the step was rejected, clear pending state
|
|
1486
|
-
if not event["accepted"]:
|
|
1487
|
-
self.pending_event = None
|
|
1488
|
-
self.confirmation_required = False
|
|
1489
|
-
|
|
1490
|
-
@pause_spinner
|
|
1491
|
-
def _handle_step_execution_start_event(self, event: dict[str, Any]) -> None:
|
|
1492
|
-
"""Handle the start of step execution.
|
|
1493
|
-
|
|
1494
|
-
Sets the appropriate spinner and does basic validation.
|
|
1495
|
-
"""
|
|
1496
|
-
try:
|
|
1497
|
-
# Log the event UUID for debugging
|
|
1498
|
-
step_uuid = event.get("stepUuid")
|
|
1499
|
-
|
|
1500
|
-
# Only change spinner if we have a step UUID
|
|
1501
|
-
if step_uuid and self.buffer.event_uuid.startswith("step-"):
|
|
1502
|
-
self.spinner = self.step_spinner
|
|
1503
|
-
self.spinner.show()
|
|
1504
|
-
except Exception as e:
|
|
1505
|
-
# Log the error but don't crash the entire UI
|
|
1506
|
-
render([f"\r\x1b[31mError handling step execution start: {e!s}\x1b[0m\n"])
|
|
1507
|
-
|
|
1508
|
-
@pause_spinner
|
|
1509
|
-
def _handle_step_execution_result_event(self, event: dict[str, Any]) -> None:
|
|
1510
|
-
"""Handle the result of a step execution.
|
|
1511
|
-
|
|
1512
|
-
Shows the step output/result and marks the step as completed.
|
|
1513
|
-
This is modeled after the file_write_result_event handler.
|
|
1514
|
-
"""
|
|
1515
|
-
# Display results regardless of UUID matching to ensure we always see them
|
|
1516
|
-
seqs = []
|
|
1517
|
-
|
|
1518
|
-
# Check for result content with fallbacks
|
|
1519
|
-
result = event.get("stepOutputRaw") or event.get("stepSummary", "")
|
|
1520
|
-
if result:
|
|
1521
|
-
# Render a result block with proper formatting
|
|
1522
|
-
seqs.append(self._render_block_header("Step result"))
|
|
1523
|
-
|
|
1524
|
-
# Format lines with proper background color
|
|
1525
|
-
lines = result.strip().split("\n")
|
|
1526
|
-
formatted_lines = []
|
|
1527
|
-
|
|
1528
|
-
for line in lines:
|
|
1529
|
-
formatted_lines.append(
|
|
1530
|
-
[
|
|
1531
|
-
erase_line_seq(),
|
|
1532
|
-
" " + line,
|
|
1533
|
-
reset_attrs_seq(),
|
|
1534
|
-
"\n",
|
|
1535
|
-
]
|
|
1536
|
-
)
|
|
1537
|
-
|
|
1538
|
-
seqs.extend(formatted_lines)
|
|
1539
|
-
seqs.append(["\n"])
|
|
1540
|
-
|
|
1541
|
-
# Add the completion footer
|
|
1542
|
-
seqs.append(self._render_block_footer("Step completed", status="completed"))
|
|
1543
|
-
seqs.append(["\n"])
|
|
1544
|
-
|
|
1545
|
-
render(seqs)
|
|
1546
|
-
|
|
1547
|
-
# Reset all state now that execution is complete
|
|
1548
|
-
self.pending_event = None
|
|
1549
|
-
self.confirmation_required = False
|
|
1550
|
-
self.spinner = self.default_spinner
|
|
1551
|
-
|
|
1552
|
-
def handle_checkpoint_interrupt(self, event: dict[str, Any]) -> None:
|
|
1553
|
-
self.buffer = NullBuffer()
|
|
1554
|
-
render(["Now back to it:", "\n", "\n"])
|
|
1555
|
-
self.render_event(event["__typename"], event)
|
|
1556
|
-
|
|
1557
|
-
# rendering the event toggles on the spinner
|
|
1558
|
-
self.end_turn()
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
class StaticView(BaseView):
|
|
1562
|
-
def __init__(self, theme: Theme) -> None:
|
|
1563
|
-
super().__init__(theme)
|
|
1564
|
-
self.command_data: dict[str, Any] | None = None
|
|
1565
|
-
self.thinking_start_time: float | None = None
|
|
1566
|
-
self.pending_event: dict[str, Any] | None = None
|
|
1567
|
-
self.confirmation_required: bool = False
|
|
1568
|
-
|
|
1569
|
-
def render_event(self, kind: str, event: dict[str, Any]) -> None:
|
|
1570
|
-
if kind == "MessageEvent":
|
|
1571
|
-
if event["role"] == "assistant":
|
|
1572
|
-
print(event["content"].strip())
|
|
1573
|
-
print()
|
|
1574
|
-
|
|
1575
|
-
elif (
|
|
1576
|
-
kind == "CommandChunkEvent"
|
|
1577
|
-
and event.get("data", {}).get("__typename") == "ThinkingCommandData"
|
|
1578
|
-
):
|
|
1579
|
-
# Record start time for thinking
|
|
1580
|
-
self.thinking_start_time = time.time()
|
|
1581
|
-
|
|
1582
|
-
elif (
|
|
1583
|
-
kind == "CommandEvent"
|
|
1584
|
-
and event.get("data", {}).get("__typename") == "ThinkingCommandData"
|
|
1585
|
-
):
|
|
1586
|
-
# Calculate thinking duration
|
|
1587
|
-
if self.thinking_start_time is not None:
|
|
1588
|
-
duration = time.time() - self.thinking_start_time
|
|
1589
|
-
# Format duration nicely
|
|
1590
|
-
if duration < 1:
|
|
1591
|
-
duration_str = f"{int(duration * 1000)}ms"
|
|
1592
|
-
elif duration < 60:
|
|
1593
|
-
# Round to nearest second to match spinner display
|
|
1594
|
-
duration_str = f"{int(duration)}s"
|
|
1595
|
-
else:
|
|
1596
|
-
mins = int(duration // 60)
|
|
1597
|
-
secs = int(duration % 60)
|
|
1598
|
-
duration_str = f"{mins}m {secs}s"
|
|
1599
|
-
|
|
1600
|
-
# Improved message with more natural language and decorations
|
|
1601
|
-
# Use render() instead of print() to ensure proper terminal rendering
|
|
1602
|
-
render(
|
|
1603
|
-
[
|
|
1604
|
-
f"\x1b[2m\x1b[38;5;249m◇ Exponent thought for {duration_str} ◇\x1b[0m\n\n"
|
|
1605
|
-
]
|
|
1606
|
-
)
|
|
1607
|
-
|
|
1608
|
-
# Reset the start time
|
|
1609
|
-
self.thinking_start_time = None
|
|
1610
|
-
|
|
1611
|
-
elif kind == "StepChunkEvent" or kind == "StepEvent":
|
|
1612
|
-
title = event.get("stepTitle") or "Step"
|
|
1613
|
-
content = (
|
|
1614
|
-
event.get("stepContent")
|
|
1615
|
-
or event.get("stepDescription", "")
|
|
1616
|
-
or event.get("description", "")
|
|
1617
|
-
)
|
|
1618
|
-
|
|
1619
|
-
if content:
|
|
1620
|
-
# For StaticView, we use simpler formatting without ANSI sequences
|
|
1621
|
-
formatted_content = ""
|
|
1622
|
-
for line in content.strip().split("\n"):
|
|
1623
|
-
formatted_content += f" {line}\n"
|
|
1624
|
-
|
|
1625
|
-
seqs = [
|
|
1626
|
-
self._render_block_header(f"Step: {title}"),
|
|
1627
|
-
formatted_content,
|
|
1628
|
-
"\n",
|
|
1629
|
-
]
|
|
1630
|
-
|
|
1631
|
-
# Store for confirmation if needed
|
|
1632
|
-
if kind == "StepEvent" and event.get("requireConfirmation", False):
|
|
1633
|
-
self.pending_event = event
|
|
1634
|
-
self.confirmation_required = True
|
|
1635
|
-
seqs.append(
|
|
1636
|
-
self._render_block_footer(
|
|
1637
|
-
"Confirm step with <ctrl+y>. Sending a new message will cancel this step."
|
|
1638
|
-
)
|
|
1639
|
-
)
|
|
1640
|
-
|
|
1641
|
-
render(seqs)
|
|
1642
|
-
|
|
1643
|
-
elif kind == "StepExecutionResultEvent":
|
|
1644
|
-
result = event.get("stepOutputRaw") or event.get("stepSummary", "")
|
|
1645
|
-
if result:
|
|
1646
|
-
# For StaticView, we use simpler formatting without ANSI sequences
|
|
1647
|
-
formatted_result = ""
|
|
1648
|
-
for line in result.strip().split("\n"):
|
|
1649
|
-
formatted_result += f" {line}\n"
|
|
1650
|
-
|
|
1651
|
-
seqs = [
|
|
1652
|
-
self._render_block_header("Step result"),
|
|
1653
|
-
formatted_result,
|
|
1654
|
-
"\n",
|
|
1655
|
-
self._render_block_footer("Step completed", status="completed"),
|
|
1656
|
-
"\n",
|
|
1657
|
-
]
|
|
1658
|
-
render(seqs)
|
|
1659
|
-
|
|
1660
|
-
# Clear pending event and reset confirmation flag
|
|
1661
|
-
self.pending_event = None
|
|
1662
|
-
self.confirmation_required = False
|
|
1663
|
-
|
|
1664
|
-
elif kind == "CommandEvent":
|
|
1665
|
-
if event.get("data", {}).get("__typename") == "ThinkingCommandData":
|
|
1666
|
-
# We already handled this case above
|
|
1667
|
-
pass
|
|
1668
|
-
elif event.get("data", {}).get("type") in ["FILE_READ", "FILE_OPEN"]:
|
|
1669
|
-
command_type = (
|
|
1670
|
-
"Read File"
|
|
1671
|
-
if event.get("data", {}).get("type") == "FILE_READ"
|
|
1672
|
-
else "Open File"
|
|
1673
|
-
)
|
|
1674
|
-
|
|
1675
|
-
seqs = [
|
|
1676
|
-
self._render_command_block_start(command_type),
|
|
1677
|
-
self._render_command_block_content(event["data"]),
|
|
1678
|
-
"\n",
|
|
1679
|
-
]
|
|
1680
|
-
|
|
1681
|
-
render(seqs)
|
|
1682
|
-
self.command_data = event["data"]
|
|
1683
|
-
|
|
1684
|
-
elif kind == "FileWriteEvent":
|
|
1685
|
-
write = event["writeContent"]
|
|
1686
|
-
|
|
1687
|
-
content = (
|
|
1688
|
-
write.get("naturalEdit") or write.get("content") or event["content"]
|
|
1689
|
-
)
|
|
1690
|
-
|
|
1691
|
-
seqs = [
|
|
1692
|
-
self._render_file_write_block_start(event["filePath"]),
|
|
1693
|
-
self._render_file_write_block_content(content, event["language"]),
|
|
1694
|
-
"\n",
|
|
1695
|
-
]
|
|
1696
|
-
|
|
1697
|
-
error = write.get("errorContent")
|
|
1698
|
-
|
|
1699
|
-
if error is None:
|
|
1700
|
-
if write.get("newFile") is not None:
|
|
1701
|
-
seqs.append(self._render_natural_edit_diff(write))
|
|
1702
|
-
else:
|
|
1703
|
-
seqs.append(self._render_natural_edit_error(error))
|
|
1704
|
-
|
|
1705
|
-
render(seqs)
|
|
1706
|
-
|
|
1707
|
-
elif kind == "FileWriteResultEvent":
|
|
1708
|
-
seqs = [self._render_file_write_block_result(event["content"]), "\n"]
|
|
1709
|
-
render(seqs)
|
|
1710
|
-
|
|
1711
|
-
elif kind == "CodeBlockEvent":
|
|
1712
|
-
seqs = [
|
|
1713
|
-
self._render_code_block_start(event["language"]),
|
|
1714
|
-
self._render_code_block_content(event["content"], event["language"]),
|
|
1715
|
-
"\n",
|
|
1716
|
-
]
|
|
1717
|
-
|
|
1718
|
-
render(seqs)
|
|
1719
|
-
|
|
1720
|
-
elif kind == "CodeExecutionEvent":
|
|
1721
|
-
render([self._render_code_block_output(event["content"]), "\n"])
|
|
1722
|
-
|
|
1723
|
-
elif kind == "CommandEvent":
|
|
1724
|
-
if event["data"]["type"] not in ["FILE_READ", "FILE_OPEN"]:
|
|
1725
|
-
return
|
|
1726
|
-
|
|
1727
|
-
command_type = (
|
|
1728
|
-
"Read File" if event["data"]["type"] == "FILE_READ" else "Open File"
|
|
1729
|
-
)
|
|
1730
|
-
|
|
1731
|
-
seqs = [
|
|
1732
|
-
self._render_command_block_start(command_type),
|
|
1733
|
-
self._render_command_block_content(event["data"]),
|
|
1734
|
-
"\n",
|
|
1735
|
-
]
|
|
1736
|
-
|
|
1737
|
-
render(seqs)
|
|
1738
|
-
self.command_data = event["data"]
|
|
1739
|
-
|
|
1740
|
-
elif kind == "CommandResultEvent":
|
|
1741
|
-
if self.command_data is not None:
|
|
1742
|
-
# For FILE_READ commands, only show that the command ran successfully
|
|
1743
|
-
if self.command_data["type"] == "FILE_READ":
|
|
1744
|
-
seqs = [
|
|
1745
|
-
self._render_block_footer(
|
|
1746
|
-
"File read successfully", status="completed"
|
|
1747
|
-
),
|
|
1748
|
-
"\n",
|
|
1749
|
-
]
|
|
1750
|
-
else:
|
|
1751
|
-
command_type = (
|
|
1752
|
-
"Read File"
|
|
1753
|
-
if self.command_data["type"] == "FILE_READ"
|
|
1754
|
-
else "Open File"
|
|
1755
|
-
)
|
|
1756
|
-
seqs = [
|
|
1757
|
-
self._render_command_block_output(
|
|
1758
|
-
event["content"],
|
|
1759
|
-
command_type,
|
|
1760
|
-
self.command_data["language"],
|
|
1761
|
-
),
|
|
1762
|
-
"\n",
|
|
1763
|
-
]
|
|
1764
|
-
|
|
1765
|
-
render(seqs)
|
|
1766
|
-
self.command_data = None
|
|
1767
|
-
|
|
1768
|
-
elif kind == "Error":
|
|
1769
|
-
error_message = event["message"]
|
|
1770
|
-
print(event)
|
|
1771
|
-
print(f"Error: {error_message}")
|
|
1772
|
-
print()
|
|
1773
|
-
elif kind == "RateLimitError":
|
|
1774
|
-
error_message = event["message"]
|
|
1775
|
-
print(event)
|
|
1776
|
-
print(f"Error: {error_message}")
|
|
1777
|
-
print(
|
|
1778
|
-
"Visit https://www.exponent.run/settings to update your billing settings."
|
|
1779
|
-
)
|
|
1780
|
-
print()
|
|
1781
|
-
|
|
1782
|
-
elif kind == "CheckpointError":
|
|
1783
|
-
print(f"Checkpoint error: {event['message']}")
|
|
1784
|
-
print()
|
|
1785
|
-
|
|
1786
|
-
elif kind == "CheckpointCreatedEvent":
|
|
1787
|
-
render(self._render_checkpoint_created_block(event))
|
|
1788
|
-
|
|
1789
|
-
def start_turn(self) -> None:
|
|
1790
|
-
pass
|
|
1791
|
-
|
|
1792
|
-
def end_turn(self) -> None:
|
|
1793
|
-
pass
|
|
1794
|
-
|
|
1795
|
-
def handle_checkpoint_interrupt(self, event: dict[str, Any]) -> None:
|
|
1796
|
-
pass
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
class Chat:
|
|
1800
|
-
def __init__(
|
|
1801
|
-
self,
|
|
1802
|
-
chat_uuid: str,
|
|
1803
|
-
parent_event_uuid: str | None,
|
|
1804
|
-
base_url: str,
|
|
1805
|
-
working_directory: str,
|
|
1806
|
-
gql_client: GraphQLClient,
|
|
1807
|
-
model: str,
|
|
1808
|
-
strategy: str,
|
|
1809
|
-
autorun: bool,
|
|
1810
|
-
depth: int,
|
|
1811
|
-
view: StaticView | LiveView,
|
|
1812
|
-
checkpoints: list[dict[str, Any]],
|
|
1813
|
-
thinking: bool = False,
|
|
1814
|
-
) -> None:
|
|
1815
|
-
self.chat_uuid = chat_uuid
|
|
1816
|
-
self.base_url = base_url
|
|
1817
|
-
self.working_directory = working_directory
|
|
1818
|
-
self.gql_client = gql_client
|
|
1819
|
-
self.model = model
|
|
1820
|
-
self.strategy = strategy
|
|
1821
|
-
self.auto_confirm_mode: AutoConfirmMode = (
|
|
1822
|
-
AutoConfirmMode.ALL if autorun else AutoConfirmMode.OFF
|
|
1823
|
-
)
|
|
1824
|
-
self.depth = depth
|
|
1825
|
-
self.thinking = thinking
|
|
1826
|
-
self.view = view
|
|
1827
|
-
self.pending_event = None
|
|
1828
|
-
self.pending_event_list: list[Any] = []
|
|
1829
|
-
self.parent_uuid: str | None = parent_event_uuid
|
|
1830
|
-
self.block_row_offset = 0
|
|
1831
|
-
self.console = Console()
|
|
1832
|
-
self.checkpoints = checkpoints
|
|
1833
|
-
self.inside_step_execution = False
|
|
1834
|
-
|
|
1835
|
-
async def send_prompt(self, prompt: str) -> None:
|
|
1836
|
-
self.view.start_turn()
|
|
1837
|
-
paths = self.extract_attachment_paths(prompt)
|
|
1838
|
-
attachments = [(await self.build_attachment(path)) for path in paths]
|
|
1839
|
-
|
|
1840
|
-
await self.process_chat_subscription(
|
|
1841
|
-
{"prompt": {"message": prompt, "attachments": attachments}}
|
|
1842
|
-
)
|
|
1843
|
-
|
|
1844
|
-
def extract_attachment_paths(self, prompt: str) -> list[str]:
|
|
1845
|
-
paths = [
|
|
1846
|
-
prompt[match.start() : match.end()].strip()[1:]
|
|
1847
|
-
for match in ATTACHMENT_PATH_PATTERN.finditer(prompt)
|
|
1848
|
-
]
|
|
1849
|
-
|
|
1850
|
-
return list(filter(os.path.isfile, paths))
|
|
1851
|
-
|
|
1852
|
-
async def build_attachment(self, path: str) -> dict[str, Any]:
|
|
1853
|
-
content = await safe_read_file(path)
|
|
1854
|
-
|
|
1855
|
-
return {
|
|
1856
|
-
"fileAttachment": {
|
|
1857
|
-
"file": {
|
|
1858
|
-
"filePath": path,
|
|
1859
|
-
"workingDirectory": self.working_directory,
|
|
1860
|
-
},
|
|
1861
|
-
"content": content,
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1865
|
-
async def send_confirmation(self) -> None:
|
|
1866
|
-
self.view.start_turn()
|
|
1867
|
-
if not self.pending_event_list:
|
|
1868
|
-
return
|
|
1869
|
-
|
|
1870
|
-
pending_event = self.pending_event_list.pop()
|
|
1871
|
-
await self.confirm_and_continue_subscription(
|
|
1872
|
-
{
|
|
1873
|
-
"requestUuid": pending_event["uuid"],
|
|
1874
|
-
}
|
|
1875
|
-
)
|
|
1876
|
-
|
|
1877
|
-
async def send_direct_action(self, action_type: str, args: dict[str, Any]) -> None:
|
|
1878
|
-
self.view.start_turn()
|
|
1879
|
-
print()
|
|
1880
|
-
if action_type == "shell":
|
|
1881
|
-
await self.process_chat_subscription(
|
|
1882
|
-
{"directAction": {"shellDirectAction": args}}
|
|
1883
|
-
)
|
|
1884
|
-
elif action_type == "checkpoint":
|
|
1885
|
-
await self.process_chat_subscription(
|
|
1886
|
-
{"directAction": {"createCheckpointDirectAction": args}}
|
|
1887
|
-
)
|
|
1888
|
-
|
|
1889
|
-
if self.pending_event:
|
|
1890
|
-
self.view.handle_checkpoint_interrupt(self.pending_event)
|
|
1891
|
-
elif action_type == "rollback":
|
|
1892
|
-
await self.process_chat_subscription(
|
|
1893
|
-
{"directAction": {"checkpointRollbackDirectAction": args}}
|
|
1894
|
-
)
|
|
1895
|
-
else:
|
|
1896
|
-
raise ValueError(f"Unsupported direct action type: {action_type}")
|
|
1897
|
-
|
|
1898
|
-
def toggle_autorun(self) -> None:
|
|
1899
|
-
if self.auto_confirm_mode == AutoConfirmMode.OFF:
|
|
1900
|
-
self.auto_confirm_mode = AutoConfirmMode.READ_ONLY
|
|
1901
|
-
elif self.auto_confirm_mode == AutoConfirmMode.READ_ONLY:
|
|
1902
|
-
self.auto_confirm_mode = AutoConfirmMode.ALL
|
|
1903
|
-
else:
|
|
1904
|
-
self.auto_confirm_mode = AutoConfirmMode.OFF
|
|
1905
|
-
|
|
1906
|
-
def toggle_thinking(self) -> bool:
|
|
1907
|
-
self.thinking = not self.thinking
|
|
1908
|
-
return self.thinking
|
|
1909
|
-
|
|
1910
|
-
async def halt_stream(self) -> None:
|
|
1911
|
-
await self.gql_client.execute(
|
|
1912
|
-
HALT_CHAT_STREAM_MUTATION, {"chatUuid": self.chat_uuid}, "HaltChatStream"
|
|
1913
|
-
)
|
|
1914
|
-
|
|
1915
|
-
def url(self) -> str:
|
|
1916
|
-
return f"{self.base_url}/chats/{self.chat_uuid}"
|
|
1917
|
-
|
|
1918
|
-
async def process_chat_subscription(self, extra_vars: dict[str, Any]) -> None:
|
|
1919
|
-
vars = {
|
|
1920
|
-
"chatUuid": self.chat_uuid,
|
|
1921
|
-
"parentUuid": self.parent_uuid,
|
|
1922
|
-
"model": self.model,
|
|
1923
|
-
"strategyNameOverride": self.strategy,
|
|
1924
|
-
"requireConfirmation": self.auto_confirm_mode == AutoConfirmMode.OFF,
|
|
1925
|
-
"readOnly": self.auto_confirm_mode == AutoConfirmMode.READ_ONLY,
|
|
1926
|
-
"depthLimit": self.depth,
|
|
1927
|
-
"enableThinking": self.thinking,
|
|
1928
|
-
}
|
|
1929
|
-
|
|
1930
|
-
vars.update(extra_vars)
|
|
1931
|
-
|
|
1932
|
-
try:
|
|
1933
|
-
async for response in self.gql_client.subscribe(
|
|
1934
|
-
INDENT_EVENTS_SUBSCRIPTION, vars
|
|
1935
|
-
):
|
|
1936
|
-
# print(response)
|
|
1937
|
-
event = response["indentChat"]
|
|
1938
|
-
kind = event["__typename"]
|
|
1939
|
-
message_kind = event["messageData"]["__typename"]
|
|
1940
|
-
message_data = event["messageData"]
|
|
1941
|
-
|
|
1942
|
-
if event["isSidechain"]:
|
|
1943
|
-
self.inside_step_execution = True
|
|
1944
|
-
else:
|
|
1945
|
-
self.inside_step_execution = False
|
|
1946
|
-
|
|
1947
|
-
if (
|
|
1948
|
-
kind == "SystemEvent"
|
|
1949
|
-
and message_kind == "ToolPermissionStatusMessage"
|
|
1950
|
-
):
|
|
1951
|
-
if message_data["permissionStatus"] == "REQUESTED":
|
|
1952
|
-
self.pending_event_list.append(event)
|
|
1953
|
-
|
|
1954
|
-
# Render the event
|
|
1955
|
-
if not self.inside_step_execution:
|
|
1956
|
-
self.view.render_event(kind, event)
|
|
1957
|
-
else:
|
|
1958
|
-
self.view.render_event_summary(kind, event)
|
|
1959
|
-
|
|
1960
|
-
# # Track parent UUID for future requests
|
|
1961
|
-
self.parent_uuid = event.get("uuid") or self.parent_uuid
|
|
1962
|
-
|
|
1963
|
-
except Exception as e:
|
|
1964
|
-
# Log subscription processing errors only if needed
|
|
1965
|
-
import traceback
|
|
1966
|
-
|
|
1967
|
-
traceback.print_exc()
|
|
1968
|
-
render([f"\r\x1b[31mError: {e!s}\x1b[0m\n"])
|
|
1969
|
-
finally:
|
|
1970
|
-
self.view.end_turn()
|
|
1971
|
-
|
|
1972
|
-
async def confirm_and_continue_subscription(
|
|
1973
|
-
self, extra_vars: dict[str, Any]
|
|
1974
|
-
) -> None:
|
|
1975
|
-
vars = {
|
|
1976
|
-
"chatUuid": self.chat_uuid,
|
|
1977
|
-
"model": self.model,
|
|
1978
|
-
"strategyNameOverride": self.strategy,
|
|
1979
|
-
"requireConfirmation": self.auto_confirm_mode == AutoConfirmMode.OFF,
|
|
1980
|
-
"readOnly": self.auto_confirm_mode == AutoConfirmMode.READ_ONLY,
|
|
1981
|
-
"depthLimit": self.depth,
|
|
1982
|
-
"enableThinking": self.thinking,
|
|
1983
|
-
}
|
|
1984
|
-
|
|
1985
|
-
vars.update(extra_vars)
|
|
1986
|
-
|
|
1987
|
-
try:
|
|
1988
|
-
async for response in self.gql_client.subscribe(
|
|
1989
|
-
CONFIRM_AND_CONTINUE_SUBSCRIPTION, vars
|
|
1990
|
-
):
|
|
1991
|
-
# print(response)
|
|
1992
|
-
event = response["confirmAndContinue"]
|
|
1993
|
-
kind = event["__typename"]
|
|
1994
|
-
message_kind = event["messageData"]["__typename"]
|
|
1995
|
-
message_data = event["messageData"]
|
|
1996
|
-
|
|
1997
|
-
if event["isSidechain"]:
|
|
1998
|
-
self.inside_step_execution = True
|
|
1999
|
-
else:
|
|
2000
|
-
self.inside_step_execution = False
|
|
2001
|
-
|
|
2002
|
-
if (
|
|
2003
|
-
kind == "SystemEvent"
|
|
2004
|
-
and message_kind == "ToolPermissionStatusMessage"
|
|
2005
|
-
):
|
|
2006
|
-
if message_data["permissionStatus"] == "requested":
|
|
2007
|
-
self.pending_event_list.append(event)
|
|
2008
|
-
|
|
2009
|
-
# Render the event
|
|
2010
|
-
if not self.inside_step_execution:
|
|
2011
|
-
self.view.render_event(kind, event)
|
|
2012
|
-
else:
|
|
2013
|
-
self.view.render_event_summary(kind, event)
|
|
2014
|
-
|
|
2015
|
-
# # Track parent UUID for future requests
|
|
2016
|
-
self.parent_uuid = event.get("uuid") or self.parent_uuid
|
|
2017
|
-
|
|
2018
|
-
except Exception as e:
|
|
2019
|
-
# Log subscription processing errors only if needed
|
|
2020
|
-
import traceback
|
|
2021
|
-
|
|
2022
|
-
traceback.print_exc()
|
|
2023
|
-
render([f"\r\x1b[31mError: {e!s}\x1b[0m\n"])
|
|
2024
|
-
finally:
|
|
2025
|
-
self.view.end_turn()
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
class HighlightLexer(Lexer):
|
|
2029
|
-
def lex_document(self, document: Document) -> Any:
|
|
2030
|
-
def get_line(line_number: int) -> list[tuple[str, str]]:
|
|
2031
|
-
line_text = document.lines[line_number]
|
|
2032
|
-
tokens = []
|
|
2033
|
-
last_index = 0
|
|
2034
|
-
|
|
2035
|
-
for match in ATTACHMENT_PATH_PATTERN.finditer(line_text):
|
|
2036
|
-
start, end = match.span()
|
|
2037
|
-
|
|
2038
|
-
if start > last_index:
|
|
2039
|
-
tokens.append(("", line_text[last_index:start]))
|
|
2040
|
-
|
|
2041
|
-
tokens.append(("class:attachment-path", line_text[start:end]))
|
|
2042
|
-
last_index = end
|
|
2043
|
-
|
|
2044
|
-
if last_index < len(line_text):
|
|
2045
|
-
tokens.append(("", line_text[last_index:]))
|
|
2046
|
-
|
|
2047
|
-
return tokens
|
|
2048
|
-
|
|
2049
|
-
return get_line
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
class InputHandler:
|
|
2053
|
-
def __init__(
|
|
2054
|
-
self, theme: Theme, loop: AbstractEventLoop, file_cache: FileCache
|
|
2055
|
-
) -> None:
|
|
2056
|
-
self.theme = theme
|
|
2057
|
-
self.bindings = KeyBindings()
|
|
2058
|
-
self.shortcut = None
|
|
2059
|
-
self.default = ""
|
|
2060
|
-
|
|
2061
|
-
self.session: PromptSession[Any] = PromptSession(
|
|
2062
|
-
completer=FuzzyCompleter(ExponentCompleter(loop, file_cache)),
|
|
2063
|
-
complete_while_typing=True,
|
|
2064
|
-
complete_in_thread=True,
|
|
2065
|
-
key_bindings=merge_key_bindings([load_key_bindings(), self.bindings]),
|
|
2066
|
-
lexer=HighlightLexer(),
|
|
2067
|
-
)
|
|
2068
|
-
|
|
2069
|
-
self.style = Style.from_dict(
|
|
2070
|
-
{
|
|
2071
|
-
"bottom-toolbar": "noreverse",
|
|
2072
|
-
"attachment-path": "underline",
|
|
2073
|
-
}
|
|
2074
|
-
)
|
|
2075
|
-
|
|
2076
|
-
handler = self
|
|
2077
|
-
|
|
2078
|
-
@self.bindings.add("enter")
|
|
2079
|
-
def _(event: Any) -> None:
|
|
2080
|
-
buf = event.current_buffer
|
|
2081
|
-
state = buf.complete_state
|
|
2082
|
-
|
|
2083
|
-
if state and state.current_completion:
|
|
2084
|
-
buf.apply_completion(state.current_completion)
|
|
2085
|
-
|
|
2086
|
-
if event.app.current_buffer.document.text[0] == "/":
|
|
2087
|
-
event.current_buffer.validate_and_handle()
|
|
2088
|
-
else:
|
|
2089
|
-
event.current_buffer.validate_and_handle()
|
|
2090
|
-
|
|
2091
|
-
# shift-enter
|
|
2092
|
-
@self.bindings.add("escape", "[", "1", "3", ";", "2", "u")
|
|
2093
|
-
def _(event: Any) -> None:
|
|
2094
|
-
event.app.current_buffer.newline()
|
|
2095
|
-
|
|
2096
|
-
@self.bindings.add("c-a")
|
|
2097
|
-
@self.bindings.add("escape", "[", "9", "7", ";", "5", "u")
|
|
2098
|
-
def _(event: Any) -> None:
|
|
2099
|
-
handler.default = event.app.current_buffer.document.text
|
|
2100
|
-
self.shortcut = "<c-a>"
|
|
2101
|
-
event.app.exit()
|
|
2102
|
-
|
|
2103
|
-
@self.bindings.add("escape", "[", "9", "9", ";", "5", "u")
|
|
2104
|
-
def _(event: Any) -> None:
|
|
2105
|
-
self.shortcut = "<c-c>"
|
|
2106
|
-
event.app.exit()
|
|
2107
|
-
|
|
2108
|
-
@self.bindings.add("c-d")
|
|
2109
|
-
@self.bindings.add("escape", "[", "1", "0", "0", ";", "5", "u")
|
|
2110
|
-
def _(event: Any) -> None:
|
|
2111
|
-
self.shortcut = "<c-d>"
|
|
2112
|
-
event.app.exit()
|
|
2113
|
-
|
|
2114
|
-
@self.bindings.add("c-e")
|
|
2115
|
-
@self.bindings.add("escape", "[", "1", "0", "1", ";", "5", "u")
|
|
2116
|
-
def _(event: Any) -> None:
|
|
2117
|
-
event.app.current_buffer.open_in_editor()
|
|
2118
|
-
|
|
2119
|
-
@self.bindings.add("escape", "[", "1", "0", "7", ";", "5", "u")
|
|
2120
|
-
def _(event: Any) -> None:
|
|
2121
|
-
event.app.key_processor.feed(KeyPress(Keys.ControlK))
|
|
2122
|
-
|
|
2123
|
-
@self.bindings.add("escape", "[", "1", "0", "8", ";", "5", "u")
|
|
2124
|
-
def _(event: Any) -> None:
|
|
2125
|
-
event.app.key_processor.feed(KeyPress(Keys.ControlL))
|
|
2126
|
-
|
|
2127
|
-
@self.bindings.add("escape", "[", "1", "1", "0", ";", "5", "u")
|
|
2128
|
-
def _(event: Any) -> None:
|
|
2129
|
-
event.app.key_processor.feed(KeyPress(Keys.ControlN))
|
|
2130
|
-
|
|
2131
|
-
@self.bindings.add("escape", "[", "1", "1", "2", ";", "5", "u")
|
|
2132
|
-
def _(event: Any) -> None:
|
|
2133
|
-
event.app.key_processor.feed(KeyPress(Keys.ControlP))
|
|
2134
|
-
|
|
2135
|
-
@self.bindings.add("escape", "[", "1", "1", "4", ";", "5", "u")
|
|
2136
|
-
def _(event: Any) -> None:
|
|
2137
|
-
pass
|
|
2138
|
-
|
|
2139
|
-
@self.bindings.add("escape", "[", "1", "1", "5", ";", "5", "u")
|
|
2140
|
-
def _(event: Any) -> None:
|
|
2141
|
-
pass
|
|
2142
|
-
|
|
2143
|
-
# Thinking is slow + not making chats better, disabling for now
|
|
2144
|
-
# @self.bindings.add("c-t")
|
|
2145
|
-
# @self.bindings.add("escape", "[", "1", "1", "6", ";", "5", "u")
|
|
2146
|
-
# def _(event: Any) -> None:
|
|
2147
|
-
# handler.default = event.app.current_buffer.document.text
|
|
2148
|
-
# self.shortcut = "<c-t>"
|
|
2149
|
-
# event.app.exit()
|
|
2150
|
-
|
|
2151
|
-
@self.bindings.add("c-y")
|
|
2152
|
-
@self.bindings.add("escape", "[", "1", "2", "1", ";", "5", "u")
|
|
2153
|
-
def _(event: Any) -> None:
|
|
2154
|
-
handler.default = event.app.current_buffer.document.text
|
|
2155
|
-
self.shortcut = "<c-y>"
|
|
2156
|
-
event.app.exit()
|
|
2157
|
-
|
|
2158
|
-
@self.bindings.add("escape", "[", "1", "2", "7", ";", "3", "u")
|
|
2159
|
-
def _(event: Any) -> None:
|
|
2160
|
-
event.app.key_processor.feed(KeyPress(Keys.Escape))
|
|
2161
|
-
event.app.key_processor.feed(KeyPress(Keys.Backspace))
|
|
2162
|
-
|
|
2163
|
-
@self.bindings.add("escape", "[", "1", "2", "7", ";", "5", "u")
|
|
2164
|
-
def _(event: Any) -> None:
|
|
2165
|
-
event.app.key_processor.feed(KeyPress(Keys.ControlH))
|
|
2166
|
-
|
|
2167
|
-
def prompt(
|
|
2168
|
-
self, auto_confirm_mode: AutoConfirmMode, thinking: bool, seed: str
|
|
2169
|
-
) -> str:
|
|
2170
|
-
toolbar = [("", " ")]
|
|
2171
|
-
default_color = self.theme.statusbar_default_fg
|
|
2172
|
-
# thinking_on_color = self.theme.statusbar_thinking_on
|
|
2173
|
-
autorun_ro_color = self.theme.statusbar_autorun_ro
|
|
2174
|
-
autorun_all_color = self.theme.statusbar_autorun_all
|
|
2175
|
-
|
|
2176
|
-
if auto_confirm_mode == AutoConfirmMode.ALL:
|
|
2177
|
-
toolbar.append((f"bold {autorun_all_color}", "➔ autorun "))
|
|
2178
|
-
toolbar.append((f"bold {autorun_all_color} reverse", " all "))
|
|
2179
|
-
elif auto_confirm_mode == AutoConfirmMode.READ_ONLY:
|
|
2180
|
-
toolbar.append((f"bold {autorun_ro_color}", "➔ autorun "))
|
|
2181
|
-
toolbar.append((f"bold {autorun_ro_color} reverse", " read only "))
|
|
2182
|
-
else:
|
|
2183
|
-
toolbar.append((f"bold {default_color}", "➔ autorun "))
|
|
2184
|
-
toolbar.append((f"bold {default_color} reverse", " off "))
|
|
2185
|
-
|
|
2186
|
-
toolbar.append(("", " "))
|
|
2187
|
-
toolbar.append((f"{default_color}", "(toggle with "))
|
|
2188
|
-
toolbar.append((f"{default_color} bold", "<ctrl+a>"))
|
|
2189
|
-
toolbar.append((f"{default_color}", ")"))
|
|
2190
|
-
toolbar.append(("", " "))
|
|
2191
|
-
|
|
2192
|
-
# Thinking is slow + not making chats better, disabling for now
|
|
2193
|
-
# if thinking:
|
|
2194
|
-
# toolbar.append((f"bold {thinking_on_color}", "⋯ thinking "))
|
|
2195
|
-
# toolbar.append((f"bold {thinking_on_color} reverse", " on "))
|
|
2196
|
-
# else:
|
|
2197
|
-
# toolbar.append((f"bold {default_color}", "⋯ thinking "))
|
|
2198
|
-
# toolbar.append((f"bold {default_color} reverse", " off "))
|
|
2199
|
-
|
|
2200
|
-
# toolbar.append(("", " "))
|
|
2201
|
-
# toolbar.append((f"{default_color}", "(toggle with "))
|
|
2202
|
-
# toolbar.append((f"{default_color} bold", "<ctrl+t>"))
|
|
2203
|
-
# toolbar.append((f"{default_color}", ")"))
|
|
2204
|
-
|
|
2205
|
-
self.shortcut = None
|
|
2206
|
-
kitty_kbd_proto_enabled = enable_kitty_kbd_protocol()
|
|
2207
|
-
|
|
2208
|
-
try:
|
|
2209
|
-
default = self.default or seed
|
|
2210
|
-
self.default = ""
|
|
2211
|
-
|
|
2212
|
-
user_input = self.session.prompt(
|
|
2213
|
-
FormattedText([(f"bold {self.theme.exponent_green}", "∷ ")]),
|
|
2214
|
-
default=default,
|
|
2215
|
-
multiline=kitty_kbd_proto_enabled,
|
|
2216
|
-
bottom_toolbar=FormattedText(toolbar),
|
|
2217
|
-
style=self.style,
|
|
2218
|
-
)
|
|
2219
|
-
finally:
|
|
2220
|
-
if kitty_kbd_proto_enabled:
|
|
2221
|
-
disable_kitty_kbd_protocol()
|
|
2222
|
-
|
|
2223
|
-
if self.shortcut is not None:
|
|
2224
|
-
return self.shortcut
|
|
2225
|
-
else:
|
|
2226
|
-
assert isinstance(user_input, str)
|
|
2227
|
-
return user_input
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
class Shell:
|
|
2231
|
-
def __init__(
|
|
2232
|
-
self,
|
|
2233
|
-
prompt: str | None,
|
|
2234
|
-
loop: asyncio.AbstractEventLoop,
|
|
2235
|
-
input_handler: InputHandler,
|
|
2236
|
-
chat: Chat,
|
|
2237
|
-
connection_tracker: ConnectionTracker,
|
|
2238
|
-
environment: Environment,
|
|
2239
|
-
) -> None:
|
|
2240
|
-
self.prompt = prompt
|
|
2241
|
-
self.loop = loop
|
|
2242
|
-
self.input_handler = input_handler
|
|
2243
|
-
self.chat = chat
|
|
2244
|
-
self.environment = environment
|
|
2245
|
-
self.stream_fut: Future[Any] | None = None
|
|
2246
|
-
self.connection_tracker = connection_tracker
|
|
2247
|
-
|
|
2248
|
-
def run(self) -> None:
|
|
2249
|
-
self._send_initial_prompt()
|
|
2250
|
-
seed = ""
|
|
2251
|
-
|
|
2252
|
-
while True:
|
|
2253
|
-
try:
|
|
2254
|
-
self._wait_for_stream_completion()
|
|
2255
|
-
|
|
2256
|
-
text = self.input_handler.prompt(
|
|
2257
|
-
self.chat.auto_confirm_mode, self.chat.thinking, seed
|
|
2258
|
-
)
|
|
2259
|
-
|
|
2260
|
-
seed = ""
|
|
2261
|
-
|
|
2262
|
-
if text.startswith("/"):
|
|
2263
|
-
self._run_command(text[1:].strip())
|
|
2264
|
-
elif text == "<c-y>":
|
|
2265
|
-
if not self._confirm_execution():
|
|
2266
|
-
seed = "yes"
|
|
2267
|
-
elif text == "<c-a>":
|
|
2268
|
-
self._run_command("autorun")
|
|
2269
|
-
elif text == "<c-t>":
|
|
2270
|
-
self._run_command("thinking")
|
|
2271
|
-
elif text in {"q", "exit"}:
|
|
2272
|
-
print()
|
|
2273
|
-
break
|
|
2274
|
-
elif text in ("<c-c>", "<c-d>"):
|
|
2275
|
-
print()
|
|
2276
|
-
do_quit = ask_for_quit_confirmation()
|
|
2277
|
-
print()
|
|
2278
|
-
|
|
2279
|
-
if do_quit:
|
|
2280
|
-
break
|
|
2281
|
-
elif text:
|
|
2282
|
-
print()
|
|
2283
|
-
self._send_prompt(text)
|
|
2284
|
-
|
|
2285
|
-
except KeyboardInterrupt:
|
|
2286
|
-
if self._handle_keyboard_interrupt():
|
|
2287
|
-
break
|
|
2288
|
-
|
|
2289
|
-
except ExponentError as e:
|
|
2290
|
-
self._print_error_message(e)
|
|
2291
|
-
break
|
|
2292
|
-
except RateLimitError as e:
|
|
2293
|
-
self._print_rate_limit_error_message(e)
|
|
2294
|
-
break
|
|
2295
|
-
|
|
2296
|
-
def _handle_keyboard_interrupt(self) -> bool:
|
|
2297
|
-
try:
|
|
2298
|
-
if self.stream_fut is not None:
|
|
2299
|
-
self._run_coroutine(self.chat.halt_stream()).result()
|
|
2300
|
-
return False
|
|
2301
|
-
return ask_for_quit_confirmation()
|
|
2302
|
-
except KeyboardInterrupt:
|
|
2303
|
-
return True
|
|
2304
|
-
|
|
2305
|
-
def _ensure_connected(self) -> None:
|
|
2306
|
-
if not self.connection_tracker.is_connected():
|
|
2307
|
-
self._run_coroutine(self._wait_for_reconnection()).result()
|
|
2308
|
-
|
|
2309
|
-
async def _wait_for_reconnection(self) -> None:
|
|
2310
|
-
render([clear_line_seq(), "Disconnected..."])
|
|
2311
|
-
await asyncio.sleep(1)
|
|
2312
|
-
spinner = Spinner("Reconnecting...")
|
|
2313
|
-
spinner.show()
|
|
2314
|
-
await self.connection_tracker.wait_for_reconnection()
|
|
2315
|
-
spinner.hide()
|
|
2316
|
-
render([fg_color_seq(2), bold_seq(), "✓ Reconnected"])
|
|
2317
|
-
await asyncio.sleep(1)
|
|
2318
|
-
render([clear_line_seq()])
|
|
2319
|
-
|
|
2320
|
-
def _print_error_message(self, e: ExponentError) -> None:
|
|
2321
|
-
print(f"\n\n\x1b[1;31m{e}\x1b[0m")
|
|
2322
|
-
print("\x1b[3;33m")
|
|
2323
|
-
print("Reach out to team@exponent.run if you need support.")
|
|
2324
|
-
print("\x1b[0m")
|
|
2325
|
-
|
|
2326
|
-
def _print_rate_limit_error_message(self, e: RateLimitError) -> None:
|
|
2327
|
-
print(f"\n\n\x1b[1;31m{e}\x1b[0m")
|
|
2328
|
-
print("\x1b[3;33m")
|
|
2329
|
-
print(
|
|
2330
|
-
"Visit https://www.exponent.run/settings to update your billing settings."
|
|
2331
|
-
)
|
|
2332
|
-
print("\x1b[0m")
|
|
2333
|
-
|
|
2334
|
-
def _show_help(self) -> None:
|
|
2335
|
-
print()
|
|
2336
|
-
|
|
2337
|
-
print(f" {bold_seq()}Commands:{not_bold_seq()}")
|
|
2338
|
-
print()
|
|
2339
|
-
|
|
2340
|
-
for command, description in sorted(SLASH_COMMANDS.items()):
|
|
2341
|
-
if command != "/help":
|
|
2342
|
-
print(f" {bold_seq()}{command}{not_bold_seq()} - {description}")
|
|
2343
|
-
|
|
2344
|
-
print()
|
|
2345
|
-
print(f" {bold_seq()}Keyboard shortcuts:{not_bold_seq()}")
|
|
2346
|
-
print()
|
|
2347
|
-
print(
|
|
2348
|
-
f" {bold_seq()}shift{not_bold_seq()}+{bold_seq()}enter{not_bold_seq()} - Add new line to the prompt (on supported terminals)"
|
|
2349
|
-
)
|
|
2350
|
-
print(
|
|
2351
|
-
f" {bold_seq()}ctrl{not_bold_seq()}+{bold_seq()}a{not_bold_seq()} - Toggle between autorun modes"
|
|
2352
|
-
)
|
|
2353
|
-
print(
|
|
2354
|
-
f" {bold_seq()}ctrl{not_bold_seq()}+{bold_seq()}c{not_bold_seq()} / {bold_seq()}ctrl{not_bold_seq()}+{bold_seq()}d{not_bold_seq()} - Quit Exponent"
|
|
2355
|
-
)
|
|
2356
|
-
print(
|
|
2357
|
-
f" {bold_seq()}ctrl{not_bold_seq()}+{bold_seq()}e{not_bold_seq()} - Edit prompt in $EDITOR"
|
|
2358
|
-
)
|
|
2359
|
-
print(
|
|
2360
|
-
f" {bold_seq()}ctrl{not_bold_seq()}+{bold_seq()}t{not_bold_seq()} - Toggle thinking mode"
|
|
2361
|
-
)
|
|
2362
|
-
print(
|
|
2363
|
-
f' {bold_seq()}ctrl{not_bold_seq()}+{bold_seq()}y{not_bold_seq()} - Confirm action or insert "yes" into the prompt'
|
|
2364
|
-
)
|
|
2365
|
-
print()
|
|
2366
|
-
|
|
2367
|
-
print(f" {bold_seq()}Autorun modes:{not_bold_seq()}")
|
|
2368
|
-
print()
|
|
2369
|
-
print(
|
|
2370
|
-
f" - {bold_seq()}off{not_bold_seq()} - You manually confirm each action (default)"
|
|
2371
|
-
)
|
|
2372
|
-
print(
|
|
2373
|
-
f" - {bold_seq()}read only{not_bold_seq()} - Auto-confirm read-only actions like search and file access"
|
|
2374
|
-
)
|
|
2375
|
-
print(
|
|
2376
|
-
f" - {bold_seq()}all{not_bold_seq()} - Auto-confirm all actions, including read, search, and edit"
|
|
2377
|
-
)
|
|
2378
|
-
print()
|
|
2379
|
-
|
|
2380
|
-
print(
|
|
2381
|
-
f" Tip: Type {bold_seq()}@{not_bold_seq()} to auto-complete project files."
|
|
2382
|
-
)
|
|
2383
|
-
print()
|
|
2384
|
-
|
|
2385
|
-
def _run_command(self, command: str) -> None:
|
|
2386
|
-
parts = command.split(maxsplit=1)
|
|
2387
|
-
cmd = parts[0]
|
|
2388
|
-
args = parts[1] if len(parts) > 1 else ""
|
|
2389
|
-
|
|
2390
|
-
if cmd == "cmd":
|
|
2391
|
-
self._execute_shell_command(args)
|
|
2392
|
-
elif cmd == "help":
|
|
2393
|
-
self._show_help()
|
|
2394
|
-
elif cmd == "autorun":
|
|
2395
|
-
self._toggle_autorun()
|
|
2396
|
-
# Thinking is slow + not making chats better, disabling for now
|
|
2397
|
-
# elif cmd == "thinking":
|
|
2398
|
-
# self._toggle_thinking()
|
|
2399
|
-
elif cmd == "web":
|
|
2400
|
-
self._switch_cli_chat_to_web()
|
|
2401
|
-
elif cmd == "c" or cmd == "checkpoint":
|
|
2402
|
-
self._create_checkpoint()
|
|
2403
|
-
elif cmd == "r" or cmd == "rollback":
|
|
2404
|
-
self._checkpoint_rollback(args)
|
|
2405
|
-
else:
|
|
2406
|
-
print(f"\n Unknown command: /{command}\n")
|
|
2407
|
-
|
|
2408
|
-
def _execute_shell_command(self, shell_command: str) -> None:
|
|
2409
|
-
if not shell_command:
|
|
2410
|
-
click.echo("\n Error: Command command is empty. Usage: /cmd <command>\n")
|
|
2411
|
-
return
|
|
2412
|
-
|
|
2413
|
-
self._ensure_connected()
|
|
2414
|
-
self.stream_fut = self._run_coroutine(
|
|
2415
|
-
self.chat.send_direct_action("shell", {"command": shell_command})
|
|
2416
|
-
)
|
|
2417
|
-
|
|
2418
|
-
def _create_checkpoint(self) -> None:
|
|
2419
|
-
self._ensure_connected()
|
|
2420
|
-
self.stream_fut = self._run_coroutine(
|
|
2421
|
-
self.chat.send_direct_action("checkpoint", {"commitMessage": None})
|
|
2422
|
-
)
|
|
2423
|
-
|
|
2424
|
-
def _checkpoint_rollback(self, args: str) -> None:
|
|
2425
|
-
self._ensure_connected()
|
|
2426
|
-
print()
|
|
2427
|
-
|
|
2428
|
-
if not self.chat.checkpoints:
|
|
2429
|
-
print("No checkpoints to rollback to. Use /checkpoint to create one.\n")
|
|
2430
|
-
return
|
|
2431
|
-
|
|
2432
|
-
choices = [
|
|
2433
|
-
questionary.Choice(
|
|
2434
|
-
title=f"{get_short_git_commit_hash(checkpoint['commitHash'])}: {checkpoint['commitMessage']}",
|
|
2435
|
-
value=index,
|
|
2436
|
-
)
|
|
2437
|
-
for index, checkpoint in enumerate(self.chat.checkpoints)
|
|
2438
|
-
]
|
|
2439
|
-
checkpoint_index = questionary.select(
|
|
2440
|
-
"Chose a checkpoint to rollback to:",
|
|
2441
|
-
choices=choices,
|
|
2442
|
-
qmark="",
|
|
2443
|
-
).ask()
|
|
2444
|
-
|
|
2445
|
-
if checkpoint_index is None:
|
|
2446
|
-
print()
|
|
2447
|
-
return
|
|
2448
|
-
|
|
2449
|
-
self.chat.checkpoints = self.chat.checkpoints[: checkpoint_index + 1]
|
|
2450
|
-
checkpoint_uuid = self.chat.checkpoints[-1]["eventUuid"]
|
|
2451
|
-
|
|
2452
|
-
self.chat.parent_uuid = checkpoint_uuid
|
|
2453
|
-
self.stream_fut = self._run_coroutine(
|
|
2454
|
-
self.chat.send_direct_action(
|
|
2455
|
-
"rollback", {"checkpointCreatedEventUuid": checkpoint_uuid}
|
|
2456
|
-
)
|
|
2457
|
-
)
|
|
2458
|
-
|
|
2459
|
-
def _toggle_autorun(self) -> None:
|
|
2460
|
-
self.chat.toggle_autorun()
|
|
2461
|
-
|
|
2462
|
-
render(
|
|
2463
|
-
[
|
|
2464
|
-
"\r",
|
|
2465
|
-
move_cursor_up_seq(1),
|
|
2466
|
-
clear_line_seq(),
|
|
2467
|
-
]
|
|
2468
|
-
)
|
|
2469
|
-
|
|
2470
|
-
def _toggle_thinking(self) -> None:
|
|
2471
|
-
self.chat.toggle_thinking()
|
|
2472
|
-
|
|
2473
|
-
render(
|
|
2474
|
-
[
|
|
2475
|
-
"\r",
|
|
2476
|
-
move_cursor_up_seq(1),
|
|
2477
|
-
clear_line_seq(),
|
|
2478
|
-
]
|
|
2479
|
-
)
|
|
2480
|
-
|
|
2481
|
-
def _switch_cli_chat_to_web(self) -> None:
|
|
2482
|
-
url = self.chat.url()
|
|
2483
|
-
print(f"\nThis chat has been moved to {url}\n")
|
|
2484
|
-
|
|
2485
|
-
if not inside_ssh_session():
|
|
2486
|
-
launch_exponent_browser(
|
|
2487
|
-
self.environment, self.chat.base_url, self.chat.chat_uuid
|
|
2488
|
-
)
|
|
2489
|
-
|
|
2490
|
-
while True:
|
|
2491
|
-
input()
|
|
2492
|
-
|
|
2493
|
-
def _confirm_execution(self) -> bool:
|
|
2494
|
-
render(
|
|
2495
|
-
[
|
|
2496
|
-
"\r",
|
|
2497
|
-
move_cursor_up_seq(1),
|
|
2498
|
-
clear_line_seq(),
|
|
2499
|
-
]
|
|
2500
|
-
)
|
|
2501
|
-
|
|
2502
|
-
success = len(self.chat.pending_event_list) > 0
|
|
2503
|
-
self._ensure_connected()
|
|
2504
|
-
self.stream_fut = self._run_coroutine(self.chat.send_confirmation())
|
|
2505
|
-
|
|
2506
|
-
return success
|
|
2507
|
-
|
|
2508
|
-
def _send_initial_prompt(self) -> None:
|
|
2509
|
-
if self.prompt is not None:
|
|
2510
|
-
self._send_prompt(self.prompt)
|
|
2511
|
-
|
|
2512
|
-
def _send_prompt(self, text: str) -> None:
|
|
2513
|
-
self._ensure_connected()
|
|
2514
|
-
self.stream_fut = self._run_coroutine(self.chat.send_prompt(text))
|
|
2515
|
-
|
|
2516
|
-
def _wait_for_stream_completion(self) -> None:
|
|
2517
|
-
if self.stream_fut is not None:
|
|
2518
|
-
self.stream_fut.result()
|
|
2519
|
-
self.stream_fut = None
|
|
2520
|
-
|
|
2521
|
-
def _run_coroutine(self, coro: Coroutine[Any, Any, Any]) -> Future[Any]:
|
|
2522
|
-
return asyncio.run_coroutine_threadsafe(coro, self.loop)
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
class ExponentCompleter(Completer):
|
|
2526
|
-
def __init__(self, loop: AbstractEventLoop, file_cache: FileCache):
|
|
2527
|
-
self.loop = loop
|
|
2528
|
-
self.file_cache = file_cache
|
|
2529
|
-
|
|
2530
|
-
def get_completions(
|
|
2531
|
-
self, document: Document, complete_event: CompleteEvent
|
|
2532
|
-
) -> Iterable[Completion]:
|
|
2533
|
-
text = document.text
|
|
2534
|
-
|
|
2535
|
-
if text.startswith("/"):
|
|
2536
|
-
for command in SLASH_COMMANDS:
|
|
2537
|
-
if command.startswith(text):
|
|
2538
|
-
yield Completion(command, start_position=-len(text))
|
|
2539
|
-
|
|
2540
|
-
elif text == "@" or text.endswith(" @"):
|
|
2541
|
-
for path in asyncio.run_coroutine_threadsafe(
|
|
2542
|
-
self.file_cache.get_files(), self.loop
|
|
2543
|
-
).result():
|
|
2544
|
-
yield Completion(path + " ", start_position=0)
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
class Buffer:
|
|
2548
|
-
def __init__(self, event_uuid: str) -> None:
|
|
2549
|
-
self.event_uuid = event_uuid
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
class NullBuffer(Buffer):
|
|
2553
|
-
def __init__(self) -> None:
|
|
2554
|
-
super().__init__("")
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
class CharBuffer(Buffer):
|
|
2558
|
-
def __init__(self, event_uuid: str, term_width: int) -> None:
|
|
2559
|
-
super().__init__(event_uuid)
|
|
2560
|
-
self.term_width = term_width
|
|
2561
|
-
self.content_len = 0
|
|
2562
|
-
self.current_term_line_len = 0
|
|
2563
|
-
self.strip_next = False
|
|
2564
|
-
|
|
2565
|
-
def render_new_chars(self, message: str) -> list[Any]:
|
|
2566
|
-
message = message.strip()
|
|
2567
|
-
new_text = message[self.content_len :]
|
|
2568
|
-
self.content_len = len(message)
|
|
2569
|
-
available_width = self.term_width - 3
|
|
2570
|
-
seqs = []
|
|
2571
|
-
|
|
2572
|
-
while new_text:
|
|
2573
|
-
max_chunk_width = available_width - self.current_term_line_len
|
|
2574
|
-
chunk = new_text[0:max_chunk_width]
|
|
2575
|
-
new_text = new_text[max_chunk_width:]
|
|
2576
|
-
lines = chunk.split("\n")
|
|
2577
|
-
|
|
2578
|
-
for idx, line in enumerate(lines):
|
|
2579
|
-
if self.current_term_line_len == 0:
|
|
2580
|
-
seqs.append(" ")
|
|
2581
|
-
|
|
2582
|
-
if self.strip_next:
|
|
2583
|
-
self.strip_next = False
|
|
2584
|
-
line = line.lstrip()
|
|
2585
|
-
|
|
2586
|
-
seqs.append(line)
|
|
2587
|
-
|
|
2588
|
-
if idx < len(lines) - 1:
|
|
2589
|
-
seqs.append("\n")
|
|
2590
|
-
self.current_term_line_len = 0
|
|
2591
|
-
else:
|
|
2592
|
-
self.current_term_line_len += len(line)
|
|
2593
|
-
|
|
2594
|
-
if self.current_term_line_len == available_width:
|
|
2595
|
-
seqs.append("\n")
|
|
2596
|
-
self.current_term_line_len = 0
|
|
2597
|
-
self.strip_next = True
|
|
2598
|
-
|
|
2599
|
-
return seqs
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
class LineBuffer(Buffer):
|
|
2603
|
-
def __init__(self, event_uuid: str) -> None:
|
|
2604
|
-
super().__init__(event_uuid)
|
|
2605
|
-
self.line_count = 0
|
|
2606
|
-
|
|
2607
|
-
def render_new_lines(
|
|
2608
|
-
self,
|
|
2609
|
-
code: str,
|
|
2610
|
-
theme: Theme,
|
|
2611
|
-
) -> list[Any]:
|
|
2612
|
-
seqs: list[Any] = []
|
|
2613
|
-
lines = code.split("\n")
|
|
2614
|
-
lines = lines[0 : len(lines) - 1]
|
|
2615
|
-
new_line_count = len(lines)
|
|
2616
|
-
|
|
2617
|
-
seqs.append(bg_color_seq(theme.block_body_bg))
|
|
2618
|
-
|
|
2619
|
-
if self.line_count > 0:
|
|
2620
|
-
seqs.append([move_cursor_up_seq(1), "\r"])
|
|
2621
|
-
lines = lines[self.line_count - 1 :]
|
|
2622
|
-
|
|
2623
|
-
lines = [line + "\n" for line in lines]
|
|
2624
|
-
seqs.append(lines)
|
|
2625
|
-
self.line_count = new_line_count
|
|
2626
|
-
|
|
2627
|
-
return seqs
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
# TODO maybe unify with LineBuffer?
|
|
2631
|
-
class CommandBuffer(Buffer):
|
|
2632
|
-
def __init__(self, event_uuid: str) -> None:
|
|
2633
|
-
super().__init__(event_uuid)
|
|
2634
|
-
self.line_count = 0
|
|
2635
|
-
|
|
2636
|
-
def render_new_lines(self, paths: list[Any]) -> list[Any]:
|
|
2637
|
-
seqs: list[Any] = []
|
|
2638
|
-
new_line_count = len(paths)
|
|
2639
|
-
|
|
2640
|
-
if self.line_count > 0:
|
|
2641
|
-
seqs.append([move_cursor_up_seq(1), "\r"])
|
|
2642
|
-
paths = paths[self.line_count - 1 :]
|
|
2643
|
-
|
|
2644
|
-
seqs.append(paths)
|
|
2645
|
-
self.line_count = new_line_count
|
|
2646
|
-
|
|
2647
|
-
return seqs
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
def highlight_code(
|
|
2651
|
-
code: str,
|
|
2652
|
-
lang: str,
|
|
2653
|
-
theme: Theme,
|
|
2654
|
-
line_numbers: bool = False,
|
|
2655
|
-
padding: tuple[int, int] = (0, 2),
|
|
2656
|
-
) -> str:
|
|
2657
|
-
syntax = Syntax(
|
|
2658
|
-
code,
|
|
2659
|
-
lang,
|
|
2660
|
-
theme=theme.hl_theme_name,
|
|
2661
|
-
line_numbers=line_numbers,
|
|
2662
|
-
word_wrap=True,
|
|
2663
|
-
padding=padding,
|
|
2664
|
-
)
|
|
2665
|
-
|
|
2666
|
-
console = Console()
|
|
2667
|
-
|
|
2668
|
-
with console.capture() as capture:
|
|
2669
|
-
console.print(syntax)
|
|
2670
|
-
|
|
2671
|
-
result = capture.get()
|
|
2672
|
-
|
|
2673
|
-
return (
|
|
2674
|
-
result.replace("\x1b[0m", f"\x1b[0m{bg_color_seq(theme.block_body_bg)}")
|
|
2675
|
-
+ "\x1b[49m"
|
|
2676
|
-
)
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
def generate_diff(before_lines: str, after_lines: str) -> str:
|
|
2680
|
-
diff = difflib.unified_diff(
|
|
2681
|
-
before_lines.split("\n"),
|
|
2682
|
-
after_lines.split("\n"),
|
|
2683
|
-
fromfile="before",
|
|
2684
|
-
tofile="after",
|
|
2685
|
-
lineterm="",
|
|
2686
|
-
)
|
|
2687
|
-
|
|
2688
|
-
return "\n".join(list(diff)[2:])
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
def pad_left(text: str, padding: str) -> str:
|
|
2692
|
-
return "\n".join([padding + line for line in text.strip().split("\n")])
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
def bold_seq() -> str:
|
|
2696
|
-
return "\x1b[1m"
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
def not_bold_seq() -> str:
|
|
2700
|
-
return "\x1b[22m"
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
def italic_seq() -> str:
|
|
2704
|
-
return "\x1b[3m"
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
def reverse_seq() -> str:
|
|
2708
|
-
return "\x1b[7m"
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
def not_reverse_seq() -> str:
|
|
2712
|
-
return "\x1b[27m"
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
def erase_line_seq() -> str:
|
|
2716
|
-
return "\x1b[2K"
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
def erase_display_seq() -> str:
|
|
2720
|
-
return "\x1b[0J"
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
def reset_attrs_seq() -> str:
|
|
2724
|
-
return "\x1b[0m"
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
def clear_line_seq() -> str:
|
|
2728
|
-
return f"\r{reset_attrs_seq()}{erase_line_seq()}"
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
def move_cursor_up_seq(n: int) -> str:
|
|
2732
|
-
if n > 0:
|
|
2733
|
-
return f"\x1b[{n}A"
|
|
2734
|
-
else:
|
|
2735
|
-
return ""
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
def move_cursor_down_seq(n: int) -> str:
|
|
2739
|
-
if n > 0:
|
|
2740
|
-
return f"\x1b[{n}B"
|
|
2741
|
-
else:
|
|
2742
|
-
return ""
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
def render(seqs: str | list[Any] | None) -> None:
|
|
2746
|
-
print(collect(seqs), end="")
|
|
2747
|
-
sys.stdout.flush()
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
def collect(seqs: str | list[Any] | None) -> str:
|
|
2751
|
-
if seqs is None:
|
|
2752
|
-
return ""
|
|
2753
|
-
|
|
2754
|
-
if isinstance(seqs, str):
|
|
2755
|
-
return seqs
|
|
2756
|
-
|
|
2757
|
-
assert isinstance(seqs, list)
|
|
2758
|
-
|
|
2759
|
-
text = ""
|
|
2760
|
-
|
|
2761
|
-
for seq in seqs:
|
|
2762
|
-
text += collect(seq)
|
|
2763
|
-
|
|
2764
|
-
return text
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
def enable_kitty_kbd_protocol() -> bool:
|
|
2768
|
-
if not POSIX_TERMINAL or not sys.stdin.isatty():
|
|
2769
|
-
return False # Not supported on Windows or when stdin is not a TTY
|
|
2770
|
-
|
|
2771
|
-
with RawMode(sys.stdin.fileno()):
|
|
2772
|
-
stdin_fd = sys.stdin.fileno()
|
|
2773
|
-
stdout_fd = sys.stdout.fileno()
|
|
2774
|
-
|
|
2775
|
-
if stdout_fd in select.select([], [stdout_fd], [])[1]:
|
|
2776
|
-
# Send the following sequences to the terminal:
|
|
2777
|
-
# CSI >1u - enable kitty keyboard protocol
|
|
2778
|
-
# CSI ?u - query status of kitty keyboard protocol
|
|
2779
|
-
# CSI c - query device attributes (DA)
|
|
2780
|
-
os.write(stdout_fd, b"\x1b[>1u\x1b[?u\x1b[c")
|
|
2781
|
-
# If the terminal doesn't support the protocol then adding the
|
|
2782
|
-
# device attributes (DA) query forces it to respond anyway (it's
|
|
2783
|
-
# widely supported), which let's us check if the response includes
|
|
2784
|
-
# the kitty protocol status (CSI ?1u) without timing out.
|
|
2785
|
-
|
|
2786
|
-
got_da_response = False
|
|
2787
|
-
kitty_proto_supported = False
|
|
2788
|
-
|
|
2789
|
-
try:
|
|
2790
|
-
# Read terminal responses until we get DA response
|
|
2791
|
-
while not got_da_response:
|
|
2792
|
-
if stdin_fd in select.select([stdin_fd], [], [], 100)[0]:
|
|
2793
|
-
reply = os.read(stdin_fd, 1024)
|
|
2794
|
-
seqs = reply.split(b"\x1b[")
|
|
2795
|
-
|
|
2796
|
-
for seq in seqs:
|
|
2797
|
-
if seq == b"?1u":
|
|
2798
|
-
kitty_proto_supported = True
|
|
2799
|
-
elif seq.endswith(b"c"):
|
|
2800
|
-
got_da_response = True
|
|
2801
|
-
|
|
2802
|
-
except Exception as e:
|
|
2803
|
-
logger.debug("Error enabling kitty kbd protocol", exc_info=e)
|
|
2804
|
-
|
|
2805
|
-
return kitty_proto_supported
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
def disable_kitty_kbd_protocol() -> None:
|
|
2809
|
-
if POSIX_TERMINAL:
|
|
2810
|
-
sys.stdout.write("\x1b[<u")
|
|
2811
|
-
sys.stdout.flush()
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
class RawMode:
|
|
2815
|
-
def __init__(self, fd: IO[str] | int) -> None:
|
|
2816
|
-
self.fd = fd
|
|
2817
|
-
self.restore: bool = False
|
|
2818
|
-
self.mode: list[Any] | None = None
|
|
2819
|
-
|
|
2820
|
-
def __enter__(self) -> None:
|
|
2821
|
-
if POSIX_TERMINAL:
|
|
2822
|
-
try:
|
|
2823
|
-
self.mode = termios.tcgetattr(self.fd)
|
|
2824
|
-
tty.setraw(self.fd)
|
|
2825
|
-
self.restore = True
|
|
2826
|
-
except (termios.error, AttributeError):
|
|
2827
|
-
pass
|
|
2828
|
-
# On Windows, we don't modify terminal settings
|
|
2829
|
-
# A more sophisticated implementation could use the Windows Console API
|
|
2830
|
-
|
|
2831
|
-
def __exit__(self, _type: str, _value: str, _traceback: str) -> None:
|
|
2832
|
-
if self.restore and POSIX_TERMINAL:
|
|
2833
|
-
time.sleep(0.01) # give the terminal time to send answerbacks
|
|
2834
|
-
assert isinstance(self.mode, list)
|
|
2835
|
-
termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.mode)
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
def get_term_width() -> int:
|
|
2839
|
-
(cols, _) = shutil.get_terminal_size((80, 24))
|
|
2840
|
-
return cols
|