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.
Files changed (163) hide show
  1. pcode/__init__.py +7 -0
  2. pcode/_job_supervisor.py +65 -0
  3. pcode/agent.py +592 -0
  4. pcode/anthropic_oauth.py +469 -0
  5. pcode/app.py +3165 -0
  6. pcode/appearance.py +181 -0
  7. pcode/aside.py +541 -0
  8. pcode/aside_guard.py +39 -0
  9. pcode/aside_ui.py +683 -0
  10. pcode/auth.py +113 -0
  11. pcode/block.py +39 -0
  12. pcode/browser.py +393 -0
  13. pcode/cache_diagnostics.py +336 -0
  14. pcode/cache_settings.py +58 -0
  15. pcode/cache_warnings.py +139 -0
  16. pcode/claude_sdk/__init__.py +96 -0
  17. pcode/claude_sdk/cli.py +52 -0
  18. pcode/claude_sdk/errors.py +61 -0
  19. pcode/claude_sdk/messages.py +154 -0
  20. pcode/claude_sdk/model.py +179 -0
  21. pcode/claude_sdk/resume.py +114 -0
  22. pcode/claude_sdk/session.py +547 -0
  23. pcode/claude_sdk/session_pool.py +251 -0
  24. pcode/claude_sdk/workspace.py +34 -0
  25. pcode/cli.py +73 -0
  26. pcode/clipboard.py +76 -0
  27. pcode/code_mode.py +43 -0
  28. pcode/codex_login.py +189 -0
  29. pcode/command_transcript.py +71 -0
  30. pcode/commands.py +145 -0
  31. pcode/compaction.py +457 -0
  32. pcode/completion.py +263 -0
  33. pcode/config.py +223 -0
  34. pcode/context_breakdown.py +178 -0
  35. pcode/context_usage.py +52 -0
  36. pcode/controller.py +3496 -0
  37. pcode/conversation_ids.py +27 -0
  38. pcode/conversation_tree.py +160 -0
  39. pcode/copy_ui.py +172 -0
  40. pcode/delegation.py +236 -0
  41. pcode/delta.py +295 -0
  42. pcode/diagnostics.py +226 -0
  43. pcode/edit_preview.py +158 -0
  44. pcode/edit_transcript.py +287 -0
  45. pcode/edit_ui.py +850 -0
  46. pcode/edits.py +162 -0
  47. pcode/email_remote/__init__.py +14 -0
  48. pcode/email_remote/cli.py +133 -0
  49. pcode/email_remote/listener.py +695 -0
  50. pcode/email_remote/mailbox.py +336 -0
  51. pcode/email_remote/outbound.py +75 -0
  52. pcode/email_remote/parsing.py +226 -0
  53. pcode/email_remote/render.py +175 -0
  54. pcode/email_remote/state.py +79 -0
  55. pcode/error_report.py +38 -0
  56. pcode/ext.py +602 -0
  57. pcode/extension_guide.md +311 -0
  58. pcode/extensions/browser.py +284 -0
  59. pcode/extensions/ponytail.py +217 -0
  60. pcode/extensions/sandbox.py +138 -0
  61. pcode/extensions/session_history.py +170 -0
  62. pcode/extensions/web_research.py +89 -0
  63. pcode/file_refs.py +265 -0
  64. pcode/filesystem.py +138 -0
  65. pcode/frame.py +149 -0
  66. pcode/gateway.py +465 -0
  67. pcode/git_diff.py +497 -0
  68. pcode/history.py +589 -0
  69. pcode/history_cursor.py +147 -0
  70. pcode/history_embeddings.py +102 -0
  71. pcode/host.py +941 -0
  72. pcode/host_protocol.py +291 -0
  73. pcode/host_ui.py +225 -0
  74. pcode/input_keys.py +35 -0
  75. pcode/inspection.py +289 -0
  76. pcode/inspector_ui.py +448 -0
  77. pcode/isolated_delegation.py +425 -0
  78. pcode/job_notices.py +54 -0
  79. pcode/jobs.py +584 -0
  80. pcode/jobs_ui.py +265 -0
  81. pcode/keymap.py +215 -0
  82. pcode/layout_speed.py +103 -0
  83. pcode/links.py +90 -0
  84. pcode/links_ui.py +200 -0
  85. pcode/live.py +1301 -0
  86. pcode/llm_proxy.py +44 -0
  87. pcode/logins.py +233 -0
  88. pcode/mcp.py +683 -0
  89. pcode/mcp_notice.py +91 -0
  90. pcode/mcp_oauth.py +381 -0
  91. pcode/meridian.py +233 -0
  92. pcode/meridian_process.py +337 -0
  93. pcode/meridian_reminders.py +107 -0
  94. pcode/meridian_setup.py +356 -0
  95. pcode/model_metadata.py +451 -0
  96. pcode/model_ui.py +168 -0
  97. pcode/models.py +209 -0
  98. pcode/native_results.py +119 -0
  99. pcode/oauth_pages.py +66 -0
  100. pcode/output_limits.py +41 -0
  101. pcode/paste.py +45 -0
  102. pcode/path_completion.py +66 -0
  103. pcode/plan_preview.py +131 -0
  104. pcode/plan_sizes.py +35 -0
  105. pcode/planning.py +161 -0
  106. pcode/popup_ui.py +873 -0
  107. pcode/preferences.py +1176 -0
  108. pcode/prefix_keys.py +490 -0
  109. pcode/profile_benchmark.py +332 -0
  110. pcode/profiling.py +566 -0
  111. pcode/project_trust.py +94 -0
  112. pcode/prompt_keys.py +286 -0
  113. pcode/remote.py +717 -0
  114. pcode/remote_print.py +125 -0
  115. pcode/remote_profile.py +237 -0
  116. pcode/replay_benchmark.py +319 -0
  117. pcode/repo_context.py +158 -0
  118. pcode/retries.py +28 -0
  119. pcode/rpc.py +280 -0
  120. pcode/runtime.py +244 -0
  121. pcode/sandbox.py +454 -0
  122. pcode/session_naming.py +82 -0
  123. pcode/session_ui.py +550 -0
  124. pcode/sessions.py +892 -0
  125. pcode/shell.py +113 -0
  126. pcode/shell_mode.py +172 -0
  127. pcode/shell_tools.py +459 -0
  128. pcode/skills.py +161 -0
  129. pcode/stall_log.py +187 -0
  130. pcode/startup_input.py +96 -0
  131. pcode/steering.py +27 -0
  132. pcode/stream_display.py +188 -0
  133. pcode/stream_events.py +437 -0
  134. pcode/strict_tools.py +130 -0
  135. pcode/syntax.py +43 -0
  136. pcode/syntax_colors.py +206 -0
  137. pcode/task_prompt.py +28 -0
  138. pcode/task_worktrees.py +482 -0
  139. pcode/terminal_notify.py +383 -0
  140. pcode/theme.py +135 -0
  141. pcode/theme_gallery.py +88 -0
  142. pcode/thinking_markdown.py +41 -0
  143. pcode/token_accounting.py +67 -0
  144. pcode/tool_display.py +784 -0
  145. pcode/tool_output_limits.py +108 -0
  146. pcode/tool_panel.py +377 -0
  147. pcode/transcript_log.py +127 -0
  148. pcode/transcript_notice.py +103 -0
  149. pcode/tree_ui.py +288 -0
  150. pcode/turn.py +44 -0
  151. pcode/ui.py +3745 -0
  152. pcode/usage.py +293 -0
  153. pcode/word_wrap.py +107 -0
  154. pcode/worker_ui.py +235 -0
  155. pcode/workers.py +136 -0
  156. pcode/workspace.py +57 -0
  157. pcode/workspace_filesystem.py +72 -0
  158. pcode/worktree.py +650 -0
  159. pcode-0.1.0.dist-info/METADATA +145 -0
  160. pcode-0.1.0.dist-info/RECORD +163 -0
  161. pcode-0.1.0.dist-info/WHEEL +4 -0
  162. pcode-0.1.0.dist-info/entry_points.txt +3 -0
  163. 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()