pcode 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pcode/__init__.py +7 -0
- pcode/_job_supervisor.py +65 -0
- pcode/agent.py +592 -0
- pcode/anthropic_oauth.py +469 -0
- pcode/app.py +3165 -0
- pcode/appearance.py +181 -0
- pcode/aside.py +541 -0
- pcode/aside_guard.py +39 -0
- pcode/aside_ui.py +683 -0
- pcode/auth.py +113 -0
- pcode/block.py +39 -0
- pcode/browser.py +393 -0
- pcode/cache_diagnostics.py +336 -0
- pcode/cache_settings.py +58 -0
- pcode/cache_warnings.py +139 -0
- pcode/claude_sdk/__init__.py +96 -0
- pcode/claude_sdk/cli.py +52 -0
- pcode/claude_sdk/errors.py +61 -0
- pcode/claude_sdk/messages.py +154 -0
- pcode/claude_sdk/model.py +179 -0
- pcode/claude_sdk/resume.py +114 -0
- pcode/claude_sdk/session.py +547 -0
- pcode/claude_sdk/session_pool.py +251 -0
- pcode/claude_sdk/workspace.py +34 -0
- pcode/cli.py +73 -0
- pcode/clipboard.py +76 -0
- pcode/code_mode.py +43 -0
- pcode/codex_login.py +189 -0
- pcode/command_transcript.py +71 -0
- pcode/commands.py +145 -0
- pcode/compaction.py +457 -0
- pcode/completion.py +263 -0
- pcode/config.py +223 -0
- pcode/context_breakdown.py +178 -0
- pcode/context_usage.py +52 -0
- pcode/controller.py +3496 -0
- pcode/conversation_ids.py +27 -0
- pcode/conversation_tree.py +160 -0
- pcode/copy_ui.py +172 -0
- pcode/delegation.py +236 -0
- pcode/delta.py +295 -0
- pcode/diagnostics.py +226 -0
- pcode/edit_preview.py +158 -0
- pcode/edit_transcript.py +287 -0
- pcode/edit_ui.py +850 -0
- pcode/edits.py +162 -0
- pcode/email_remote/__init__.py +14 -0
- pcode/email_remote/cli.py +133 -0
- pcode/email_remote/listener.py +695 -0
- pcode/email_remote/mailbox.py +336 -0
- pcode/email_remote/outbound.py +75 -0
- pcode/email_remote/parsing.py +226 -0
- pcode/email_remote/render.py +175 -0
- pcode/email_remote/state.py +79 -0
- pcode/error_report.py +38 -0
- pcode/ext.py +602 -0
- pcode/extension_guide.md +311 -0
- pcode/extensions/browser.py +284 -0
- pcode/extensions/ponytail.py +217 -0
- pcode/extensions/sandbox.py +138 -0
- pcode/extensions/session_history.py +170 -0
- pcode/extensions/web_research.py +89 -0
- pcode/file_refs.py +265 -0
- pcode/filesystem.py +138 -0
- pcode/frame.py +149 -0
- pcode/gateway.py +465 -0
- pcode/git_diff.py +497 -0
- pcode/history.py +589 -0
- pcode/history_cursor.py +147 -0
- pcode/history_embeddings.py +102 -0
- pcode/host.py +941 -0
- pcode/host_protocol.py +291 -0
- pcode/host_ui.py +225 -0
- pcode/input_keys.py +35 -0
- pcode/inspection.py +289 -0
- pcode/inspector_ui.py +448 -0
- pcode/isolated_delegation.py +425 -0
- pcode/job_notices.py +54 -0
- pcode/jobs.py +584 -0
- pcode/jobs_ui.py +265 -0
- pcode/keymap.py +215 -0
- pcode/layout_speed.py +103 -0
- pcode/links.py +90 -0
- pcode/links_ui.py +200 -0
- pcode/live.py +1301 -0
- pcode/llm_proxy.py +44 -0
- pcode/logins.py +233 -0
- pcode/mcp.py +683 -0
- pcode/mcp_notice.py +91 -0
- pcode/mcp_oauth.py +381 -0
- pcode/meridian.py +233 -0
- pcode/meridian_process.py +337 -0
- pcode/meridian_reminders.py +107 -0
- pcode/meridian_setup.py +356 -0
- pcode/model_metadata.py +451 -0
- pcode/model_ui.py +168 -0
- pcode/models.py +209 -0
- pcode/native_results.py +119 -0
- pcode/oauth_pages.py +66 -0
- pcode/output_limits.py +41 -0
- pcode/paste.py +45 -0
- pcode/path_completion.py +66 -0
- pcode/plan_preview.py +131 -0
- pcode/plan_sizes.py +35 -0
- pcode/planning.py +161 -0
- pcode/popup_ui.py +873 -0
- pcode/preferences.py +1176 -0
- pcode/prefix_keys.py +490 -0
- pcode/profile_benchmark.py +332 -0
- pcode/profiling.py +566 -0
- pcode/project_trust.py +94 -0
- pcode/prompt_keys.py +286 -0
- pcode/remote.py +717 -0
- pcode/remote_print.py +125 -0
- pcode/remote_profile.py +237 -0
- pcode/replay_benchmark.py +319 -0
- pcode/repo_context.py +158 -0
- pcode/retries.py +28 -0
- pcode/rpc.py +280 -0
- pcode/runtime.py +244 -0
- pcode/sandbox.py +454 -0
- pcode/session_naming.py +82 -0
- pcode/session_ui.py +550 -0
- pcode/sessions.py +892 -0
- pcode/shell.py +113 -0
- pcode/shell_mode.py +172 -0
- pcode/shell_tools.py +459 -0
- pcode/skills.py +161 -0
- pcode/stall_log.py +187 -0
- pcode/startup_input.py +96 -0
- pcode/steering.py +27 -0
- pcode/stream_display.py +188 -0
- pcode/stream_events.py +437 -0
- pcode/strict_tools.py +130 -0
- pcode/syntax.py +43 -0
- pcode/syntax_colors.py +206 -0
- pcode/task_prompt.py +28 -0
- pcode/task_worktrees.py +482 -0
- pcode/terminal_notify.py +383 -0
- pcode/theme.py +135 -0
- pcode/theme_gallery.py +88 -0
- pcode/thinking_markdown.py +41 -0
- pcode/token_accounting.py +67 -0
- pcode/tool_display.py +784 -0
- pcode/tool_output_limits.py +108 -0
- pcode/tool_panel.py +377 -0
- pcode/transcript_log.py +127 -0
- pcode/transcript_notice.py +103 -0
- pcode/tree_ui.py +288 -0
- pcode/turn.py +44 -0
- pcode/ui.py +3745 -0
- pcode/usage.py +293 -0
- pcode/word_wrap.py +107 -0
- pcode/worker_ui.py +235 -0
- pcode/workers.py +136 -0
- pcode/workspace.py +57 -0
- pcode/workspace_filesystem.py +72 -0
- pcode/worktree.py +650 -0
- pcode-0.1.0.dist-info/METADATA +145 -0
- pcode-0.1.0.dist-info/RECORD +163 -0
- pcode-0.1.0.dist-info/WHEEL +4 -0
- pcode-0.1.0.dist-info/entry_points.txt +3 -0
- pcode-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""Offline streaming benchmark: python -m pcode.profile_benchmark --help."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import gc
|
|
6
|
+
import json
|
|
7
|
+
import platform
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
from contextlib import closing, nullcontext
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from types import SimpleNamespace
|
|
13
|
+
|
|
14
|
+
from prompt_toolkit.data_structures import Size
|
|
15
|
+
from prompt_toolkit.output import DummyOutput
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
|
|
18
|
+
from pcode.profiling import profile_session
|
|
19
|
+
from pcode.replay_benchmark import (
|
|
20
|
+
EndOnlyOutput,
|
|
21
|
+
JournalRuntime,
|
|
22
|
+
JournalSnapshot,
|
|
23
|
+
ReplaySettings,
|
|
24
|
+
journal_turns,
|
|
25
|
+
replay_journal,
|
|
26
|
+
)
|
|
27
|
+
from pcode.ui import CursorSafeOutput, TerminalOutput
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CountingOutput:
|
|
31
|
+
"""Discard rendered content so the benchmark sink cannot look like a leak."""
|
|
32
|
+
|
|
33
|
+
def __init__(self):
|
|
34
|
+
self.characters = 0
|
|
35
|
+
|
|
36
|
+
def write(self, text: str) -> int:
|
|
37
|
+
self.characters += len(text)
|
|
38
|
+
return len(text)
|
|
39
|
+
|
|
40
|
+
def flush(self) -> None:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def benchmark_output(width: int, *, mode: str = "streamed"):
|
|
45
|
+
sink = CountingOutput()
|
|
46
|
+
terminal = DummyOutput()
|
|
47
|
+
terminal.get_size = lambda: Size(rows=24, columns=width)
|
|
48
|
+
app = SimpleNamespace(output=CursorSafeOutput(terminal), invalidate=lambda: None)
|
|
49
|
+
output_type = EndOnlyOutput if mode == "end-only" else TerminalOutput
|
|
50
|
+
output = output_type(Console(file=sink, color_system=None, width=width), app)
|
|
51
|
+
return output, sink
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def stream_workload(kind: str, lines: int, width: int) -> int:
|
|
55
|
+
output, sink = benchmark_output(width)
|
|
56
|
+
if kind == "fence":
|
|
57
|
+
output.delta("```python\n")
|
|
58
|
+
for index in range(lines):
|
|
59
|
+
if kind == "prose":
|
|
60
|
+
text = f"Plain paragraph number {index}.\n\n"
|
|
61
|
+
elif kind == "list":
|
|
62
|
+
text = f"- List item number {index}.\n"
|
|
63
|
+
else:
|
|
64
|
+
text = f"value_{index} = {index}\n"
|
|
65
|
+
output.delta(text)
|
|
66
|
+
await output.flush()
|
|
67
|
+
if kind == "fence":
|
|
68
|
+
output.delta("```\n")
|
|
69
|
+
output.finish()
|
|
70
|
+
await output.flush()
|
|
71
|
+
return sink.characters
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def positive_int(value: str) -> int:
|
|
75
|
+
number = int(value)
|
|
76
|
+
if number <= 0:
|
|
77
|
+
raise argparse.ArgumentTypeError("must be positive")
|
|
78
|
+
return number
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def main() -> None:
|
|
82
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
83
|
+
source = parser.add_mutually_exclusive_group()
|
|
84
|
+
source.add_argument("--kind", choices=("prose", "list", "fence"))
|
|
85
|
+
source.add_argument("--replay", action="append", metavar="SESSION", help="ID/prefix or latest")
|
|
86
|
+
source.add_argument("--recent", type=positive_int, metavar="N", help="Replay N newest sessions")
|
|
87
|
+
source.add_argument(
|
|
88
|
+
"--largest", type=positive_int, metavar="N", help="Replay the N biggest session journals"
|
|
89
|
+
)
|
|
90
|
+
source.add_argument("--journal", type=Path, action="append", help="Replay explicit JSONL files")
|
|
91
|
+
parser.add_argument("--session-dir", type=Path)
|
|
92
|
+
parser.add_argument("--repeat", type=positive_int, default=1)
|
|
93
|
+
parser.add_argument(
|
|
94
|
+
"--render-mode", choices=("streamed", "end-only", "both"), default="streamed"
|
|
95
|
+
)
|
|
96
|
+
parser.add_argument("--show-thinking", action=argparse.BooleanOptionalAction, default=True)
|
|
97
|
+
parser.add_argument("--command-scrollback", action="store_true")
|
|
98
|
+
parser.add_argument("--start-turn", type=positive_int, default=1)
|
|
99
|
+
parser.add_argument("--max-turns", type=positive_int)
|
|
100
|
+
parser.add_argument("--lines", type=positive_int, default=500)
|
|
101
|
+
parser.add_argument("--width", type=positive_int, default=80)
|
|
102
|
+
parser.add_argument("--profile", type=Path, metavar="DIR")
|
|
103
|
+
parser.add_argument("--profile-cpu", action="store_true")
|
|
104
|
+
parser.add_argument("--profile-memory", action="store_true")
|
|
105
|
+
parser.add_argument(
|
|
106
|
+
"--live",
|
|
107
|
+
action="store_true",
|
|
108
|
+
help="Replay one session through the real editor in this terminal, paced by its timestamps",
|
|
109
|
+
)
|
|
110
|
+
parser.add_argument(
|
|
111
|
+
"--speed",
|
|
112
|
+
type=float,
|
|
113
|
+
default=1.0,
|
|
114
|
+
help="Live pacing multiplier: 1 is real time, 0 replays as fast as possible",
|
|
115
|
+
)
|
|
116
|
+
parser.add_argument(
|
|
117
|
+
"--result",
|
|
118
|
+
type=Path,
|
|
119
|
+
metavar="FILE",
|
|
120
|
+
help="Also write the --live result JSON here (the terminal is busy being the UI)",
|
|
121
|
+
)
|
|
122
|
+
args = parser.parse_args()
|
|
123
|
+
if (args.profile_cpu or args.profile_memory) and args.profile is None:
|
|
124
|
+
parser.error("--profile-cpu and --profile-memory require --profile DIR")
|
|
125
|
+
if args.live:
|
|
126
|
+
_live(args, parser)
|
|
127
|
+
return
|
|
128
|
+
if args.replay or args.recent or args.largest or args.journal:
|
|
129
|
+
_replay(args, parser)
|
|
130
|
+
return
|
|
131
|
+
if (
|
|
132
|
+
args.repeat != 1
|
|
133
|
+
or args.render_mode != "streamed"
|
|
134
|
+
or args.session_dir is not None
|
|
135
|
+
or args.start_turn != 1
|
|
136
|
+
or args.max_turns is not None
|
|
137
|
+
or not args.show_thinking
|
|
138
|
+
or args.command_scrollback
|
|
139
|
+
or args.speed != 1.0
|
|
140
|
+
or args.result is not None
|
|
141
|
+
):
|
|
142
|
+
parser.error("replay options require --replay, --recent, --largest, or --journal")
|
|
143
|
+
args.kind = args.kind or "list"
|
|
144
|
+
with capture(args, args.profile):
|
|
145
|
+
started = time.perf_counter()
|
|
146
|
+
cpu_started = time.process_time()
|
|
147
|
+
characters = asyncio.run(stream_workload(args.kind, args.lines, args.width))
|
|
148
|
+
result = dict(
|
|
149
|
+
kind=args.kind,
|
|
150
|
+
lines=args.lines,
|
|
151
|
+
width=args.width,
|
|
152
|
+
rendered_characters=characters,
|
|
153
|
+
wall_seconds=time.perf_counter() - started,
|
|
154
|
+
cpu_seconds=time.process_time() - cpu_started,
|
|
155
|
+
profiled=args.profile is not None,
|
|
156
|
+
cpu_tracing=args.profile_cpu,
|
|
157
|
+
memory_tracing=args.profile_memory,
|
|
158
|
+
)
|
|
159
|
+
print(json.dumps(result))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def capture(args, directory):
|
|
163
|
+
return (
|
|
164
|
+
profile_session(directory, cpu=args.profile_cpu, memory=args.profile_memory)
|
|
165
|
+
if directory
|
|
166
|
+
else nullcontext()
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def select_journals(args) -> list[Path]:
|
|
171
|
+
"""The transcript files the replay options name; may raise OSError/ValueError."""
|
|
172
|
+
from pcode.sessions import list_sessions, resolve_session, session_root
|
|
173
|
+
|
|
174
|
+
root = args.session_dir or session_root()
|
|
175
|
+
if args.journal:
|
|
176
|
+
return args.journal
|
|
177
|
+
if args.recent:
|
|
178
|
+
return [root / info.id / "transcript.jsonl" for info in list_sessions(root)[: args.recent]]
|
|
179
|
+
if args.largest:
|
|
180
|
+
journals = [root / info.id / "transcript.jsonl" for info in list_sessions(root)]
|
|
181
|
+
journals = [path for path in journals if path.is_file()]
|
|
182
|
+
return sorted(journals, key=lambda path: path.stat().st_size, reverse=True)[: args.largest]
|
|
183
|
+
return [resolve_session(selector, root) / "transcript.jsonl" for selector in args.replay or ()]
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _replay(args, parser):
|
|
187
|
+
try:
|
|
188
|
+
journals = select_journals(args)
|
|
189
|
+
if not journals:
|
|
190
|
+
parser.error("no saved sessions found")
|
|
191
|
+
if args.profile:
|
|
192
|
+
args.profile.mkdir(mode=0o700, parents=True, exist_ok=False)
|
|
193
|
+
except (OSError, ValueError) as error:
|
|
194
|
+
parser.error(
|
|
195
|
+
f"Cannot prepare replay ({type(error).__name__}); check source and output paths"
|
|
196
|
+
)
|
|
197
|
+
modes = ("streamed", "end-only") if args.render_mode == "both" else (args.render_mode,)
|
|
198
|
+
settings = ReplaySettings(
|
|
199
|
+
show_thinking=args.show_thinking,
|
|
200
|
+
command_scrollback=args.command_scrollback,
|
|
201
|
+
start_turn=args.start_turn,
|
|
202
|
+
max_turns=args.max_turns,
|
|
203
|
+
)
|
|
204
|
+
failed = False
|
|
205
|
+
for index, path in enumerate(journals, 1):
|
|
206
|
+
try:
|
|
207
|
+
with closing(JournalSnapshot(path)) as snapshot:
|
|
208
|
+
for repeat in range(1, args.repeat + 1):
|
|
209
|
+
# Reduce first-mode cache/order bias in comparison runs.
|
|
210
|
+
ordered_modes = modes if repeat % 2 else modes[::-1]
|
|
211
|
+
for mode in ordered_modes:
|
|
212
|
+
# Transcript/output callbacks form cycles. Collect the previous
|
|
213
|
+
# pass outside the next capture, after dropping its local refs.
|
|
214
|
+
gc.collect()
|
|
215
|
+
result = _replay_pass(args, snapshot, settings, index, repeat, mode)
|
|
216
|
+
print(json.dumps(result), flush=True)
|
|
217
|
+
except Exception as error:
|
|
218
|
+
# Journal contents and even exception messages may contain private data.
|
|
219
|
+
print(json.dumps(dict(session=index, error=type(error).__name__)), flush=True)
|
|
220
|
+
failed = True
|
|
221
|
+
finally:
|
|
222
|
+
gc.collect()
|
|
223
|
+
if failed:
|
|
224
|
+
parser.exit(1, "One or more replays failed; no session data was modified.\n")
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _replay_pass(args, snapshot, settings, index, repeat, mode):
|
|
228
|
+
# Keep renderer ownership inside one pass so no strong reference survives
|
|
229
|
+
# into the next pass, including when decoding/rendering raises an exception.
|
|
230
|
+
output, sink = benchmark_output(args.width, mode=mode)
|
|
231
|
+
directory = args.profile / f"session-{index}-{mode}-{repeat}" if args.profile else None
|
|
232
|
+
with capture(args, directory):
|
|
233
|
+
result = asyncio.run(replay_journal(snapshot, output, settings))
|
|
234
|
+
return dict(
|
|
235
|
+
schema_version=1,
|
|
236
|
+
scope="saved_display_events",
|
|
237
|
+
python=platform.python_version(),
|
|
238
|
+
platform=sys.platform,
|
|
239
|
+
session=index,
|
|
240
|
+
repeat=repeat,
|
|
241
|
+
render_mode=mode,
|
|
242
|
+
width=args.width,
|
|
243
|
+
show_thinking=args.show_thinking,
|
|
244
|
+
command_scrollback=args.command_scrollback,
|
|
245
|
+
start_turn=args.start_turn,
|
|
246
|
+
max_turns=args.max_turns,
|
|
247
|
+
rendered_characters=sink.characters,
|
|
248
|
+
profiled=args.profile is not None,
|
|
249
|
+
cpu_tracing=args.profile_cpu,
|
|
250
|
+
memory_tracing=args.profile_memory,
|
|
251
|
+
**result,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _live(args, parser):
|
|
256
|
+
import resource
|
|
257
|
+
|
|
258
|
+
from pcode.app import PreviewApp
|
|
259
|
+
|
|
260
|
+
if args.render_mode != "streamed" or args.repeat != 1 or args.profile_memory:
|
|
261
|
+
parser.error("--live supports one streamed pass; --profile-cpu is allowed")
|
|
262
|
+
if not (sys.stdin.isatty() and sys.stdout.isatty()):
|
|
263
|
+
parser.error("--live needs a terminal; script it in a tmux pane with --result FILE")
|
|
264
|
+
try:
|
|
265
|
+
journals = select_journals(args)
|
|
266
|
+
if len(journals) != 1:
|
|
267
|
+
parser.error("--live replays exactly one session")
|
|
268
|
+
with closing(JournalSnapshot(journals[0])) as snapshot:
|
|
269
|
+
turns = journal_turns(snapshot)
|
|
270
|
+
if args.profile:
|
|
271
|
+
args.profile.mkdir(mode=0o700, parents=True, exist_ok=False)
|
|
272
|
+
except (OSError, ValueError) as error:
|
|
273
|
+
parser.error(
|
|
274
|
+
f"Cannot prepare replay ({type(error).__name__}); check source and output paths"
|
|
275
|
+
)
|
|
276
|
+
stop = args.start_turn - 1 + (args.max_turns or len(turns))
|
|
277
|
+
turns = turns[args.start_turn - 1 : stop]
|
|
278
|
+
if not turns:
|
|
279
|
+
parser.error("no turns in the selected range")
|
|
280
|
+
runtime = JournalRuntime(turns, speed=args.speed)
|
|
281
|
+
stats = dict(turns=len(turns), speed=args.speed, renders=0, flushes=0)
|
|
282
|
+
|
|
283
|
+
app = PreviewApp(
|
|
284
|
+
model="journal-replay",
|
|
285
|
+
runtime=runtime,
|
|
286
|
+
initial_prompt=turns[0].prompt or "replay",
|
|
287
|
+
console=Console(),
|
|
288
|
+
)
|
|
289
|
+
run_turn = app.controller.run_turn
|
|
290
|
+
|
|
291
|
+
async def replay_turns(text, **options):
|
|
292
|
+
"""Play every journaled turn through the real turn path, then quit."""
|
|
293
|
+
output = app.output
|
|
294
|
+
if stats["renders"] == 0:
|
|
295
|
+
render, flush = output.app.renderer.render, output.flush
|
|
296
|
+
|
|
297
|
+
def counted_render(*a, **kw):
|
|
298
|
+
stats["renders"] += 1
|
|
299
|
+
return render(*a, **kw)
|
|
300
|
+
|
|
301
|
+
async def counted_flush(**options):
|
|
302
|
+
if output.pending or output.rows or output._regenerate is not None:
|
|
303
|
+
stats["flushes"] += 1
|
|
304
|
+
await flush(**options)
|
|
305
|
+
|
|
306
|
+
output.app.renderer.render, output.flush = counted_render, counted_flush
|
|
307
|
+
stats["started"] = time.perf_counter()
|
|
308
|
+
stats["rusage"] = resource.getrusage(resource.RUSAGE_SELF)
|
|
309
|
+
for turn in runtime.turns[:]:
|
|
310
|
+
await run_turn(turn.prompt)
|
|
311
|
+
app.running = False
|
|
312
|
+
return True
|
|
313
|
+
|
|
314
|
+
app.controller.run_turn = replay_turns
|
|
315
|
+
app.activity.thinking_mode = "scrollback" if args.show_thinking else "off"
|
|
316
|
+
app.transcript.command_scrollback = args.command_scrollback
|
|
317
|
+
with capture(args, args.profile):
|
|
318
|
+
app.run()
|
|
319
|
+
before = stats.pop("rusage")
|
|
320
|
+
after = resource.getrusage(resource.RUSAGE_SELF)
|
|
321
|
+
stats["events"] = runtime.events_played
|
|
322
|
+
stats["wall_seconds"] = time.perf_counter() - stats.pop("started")
|
|
323
|
+
stats["cpu_seconds"] = (after.ru_utime - before.ru_utime) + (after.ru_stime - before.ru_stime)
|
|
324
|
+
stats["cpu_fraction"] = stats["cpu_seconds"] / max(stats["wall_seconds"], 1e-9)
|
|
325
|
+
result = json.dumps(dict(scope="live_terminal_replay", **stats))
|
|
326
|
+
print(result)
|
|
327
|
+
if args.result:
|
|
328
|
+
args.result.write_text(result + "\n")
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
if __name__ == "__main__":
|
|
332
|
+
main()
|