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,287 @@
1
+ """Literal, width-aware edit blocks, independent of assistant Markdown."""
2
+
3
+ import asyncio
4
+ from collections.abc import Callable
5
+ from dataclasses import dataclass
6
+ from functools import cache
7
+
8
+ from prompt_toolkit.formatted_text import ANSI, to_formatted_text
9
+ from prompt_toolkit.lexers import Lexer
10
+ from pygments.token import Generic, Token
11
+ from rich.console import Console, ConsoleOptions, RenderResult
12
+ from rich.segment import Segment
13
+ from rich.style import Style
14
+ from rich.syntax import Syntax
15
+ from rich.text import Text
16
+
17
+ from pcode.block import DONE, block_heading, block_rule
18
+ from pcode.delta import Delta, preview_patch
19
+ from pcode.edits import edit_text, patch_text
20
+ from pcode.runtime import EditCompleted
21
+ from pcode.syntax import transparent_theme
22
+ from pcode.tool_display import command_text
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class EditTranscript:
27
+ change: EditCompleted
28
+ code_theme: str = "monokai"
29
+ max_rows: int = 60
30
+ delta: Delta | None = None
31
+ dedent: bool = True
32
+
33
+ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
34
+ change = self.change
35
+ yield block_rule(
36
+ block_heading(
37
+ DONE,
38
+ f"{change.operation.capitalize()} {edit_text(change.path)}"
39
+ f" · +{change.added} −{change.removed}",
40
+ )
41
+ )
42
+ if change.patch:
43
+ text = patch_text(change.patch, dedent=self.dedent)
44
+ lines = self.delta.render(text, options.max_width) if self.delta else None
45
+ patch = (
46
+ Text("\n").join(lines)
47
+ if lines is not None
48
+ else Syntax(text, "diff", theme=transparent_theme(self.code_theme), word_wrap=True)
49
+ )
50
+ rows = console.render_lines(patch, options, pad=False)
51
+ for row in rows[: self.max_rows]:
52
+ yield from row
53
+ yield Segment.line()
54
+ if len(rows) > self.max_rows or change.truncated:
55
+ yield Text("… additional diff rows omitted", style="pcode.muted")
56
+ if change.omitted:
57
+ yield Text(f"Diff unavailable: {edit_text(change.omitted)}", style="pcode.muted")
58
+ yield block_rule()
59
+
60
+
61
+ def prefetch_edits(objects, width: int) -> None:
62
+ """Render every delta edit block in `objects` through one delta process each."""
63
+ patches: dict[Delta, list[str]] = {}
64
+ for obj in objects:
65
+ if isinstance(obj, EditTranscript) and obj.delta is not None and obj.change.patch:
66
+ text = patch_text(obj.change.patch, dedent=obj.dedent)
67
+ patches.setdefault(obj.delta, []).append(text)
68
+ for delta, texts in patches.items():
69
+ delta.prefetch(texts, width)
70
+
71
+
72
+ @cache
73
+ def _token_style(code_theme: str, token) -> str:
74
+ """Render one syntax token as a prompt_toolkit style string."""
75
+ style = transparent_theme(code_theme).get_style_for_token(token)
76
+ # Convert only a library-generated marker, never file content. This keeps
77
+ # ANSI palette colors native while matching Rich's RGB syntax colors too.
78
+ marker = Style(color=style.color, bold=style.bold).render("x", color_system="truecolor")
79
+ return to_formatted_text(ANSI(marker))[0][0]
80
+
81
+
82
+ @cache
83
+ def _preview_style(code_theme: str, added: bool) -> str:
84
+ token = Generic.Inserted if added else Generic.Deleted
85
+ return "class:bottom-toolbar.text " + _token_style(code_theme, token)
86
+
87
+
88
+ def diff_token(line: str):
89
+ """Classify a logical diff line the way Pygments' diff lexer does."""
90
+ if line.startswith(("+", "> ")):
91
+ return Generic.Inserted
92
+ if line.startswith(("-", "< ")):
93
+ return Generic.Deleted
94
+ if line.startswith("@"):
95
+ return Generic.Subheading
96
+ if line.startswith(("diff", "index", "Index:", "=")):
97
+ return Generic.Heading
98
+ if line.startswith("!"):
99
+ return Generic.Strong
100
+ return Token.Text
101
+
102
+
103
+ @cache
104
+ def _rich_style(style: Style) -> str:
105
+ """A Rich style as a prompt_toolkit style string, by way of its ANSI codes."""
106
+ if not style:
107
+ return ""
108
+ # A link would open with an OSC 8 sequence ahead of the marker's colors.
109
+ marker = style.clear_meta_and_links().render("x")
110
+ return to_formatted_text(ANSI(marker))[0][0]
111
+
112
+
113
+ _FRAGMENT_CONSOLE = Console(width=10_000, color_system="truecolor", force_terminal=True)
114
+
115
+
116
+ def text_fragments(text: Text, console: Console = _FRAGMENT_CONSOLE) -> list[tuple[str, str]]:
117
+ """One unwrapped Rich line as prompt_toolkit fragments."""
118
+ # Console.render, unlike Text.render, applies the text's own base style.
119
+ options = console.options.update(no_wrap=True, overflow="ignore")
120
+ return [
121
+ (_rich_style(segment.style), segment.text)
122
+ for segment in console.render(text, options)
123
+ if segment.text and segment.text != "\n"
124
+ ]
125
+
126
+
127
+ class DiffLexer(Lexer):
128
+ """Color diffs in prompt_toolkit with the same theme scrollback diffs use.
129
+
130
+ `rows` holds fragments already styled elsewhere (delta's output), by row;
131
+ every other row is classified as a plain diff line.
132
+ """
133
+
134
+ def __init__(self, code_theme: str = "monokai") -> None:
135
+ self.code_theme = code_theme
136
+ self._rows: dict[int, list[tuple[str, str]]] = {}
137
+ self._version = 0
138
+
139
+ @property
140
+ def rows(self) -> dict[int, list[tuple[str, str]]]:
141
+ return self._rows
142
+
143
+ @rows.setter
144
+ def rows(self, rows: dict[int, list[tuple[str, str]]]) -> None:
145
+ self._rows = rows
146
+ self._version += 1
147
+
148
+ def lex_document(self, document):
149
+ rows = self.rows
150
+
151
+ def line(number: int):
152
+ if number in rows:
153
+ return rows[number]
154
+ text = document.lines[number]
155
+ return [(_token_style(self.code_theme, diff_token(text)), text)]
156
+
157
+ return line
158
+
159
+ def invalidation_hash(self):
160
+ return self._version
161
+
162
+
163
+ class LiveDeltaPreview:
164
+ """delta rows for the streaming edit preview, rendered off the event loop.
165
+
166
+ The preview grows every few dozen milliseconds while a call streams, and
167
+ one delta run takes longer than that, so `rows` never waits. It shows the
168
+ newest rendering delta has finished for this call, and any lines that
169
+ arrived since straight away, the way delta lays them out (no +/- gutter)
170
+ but colored only as added or removed, until the next run highlights them.
171
+ Lines therefore appear as fast as without delta; only their syntax colors
172
+ trail. Side by side cannot be approximated a line at a time, so that
173
+ layout waits for delta. One run is in flight at a time and the newest
174
+ body runs next. None means draw with Rich: delta failed for this call, or
175
+ there is nothing to show yet. `on_ready` runs on the event loop when a
176
+ rendering lands.
177
+ """
178
+
179
+ def __init__(self, on_ready: Callable[[], None]) -> None:
180
+ self.on_ready = on_ready
181
+ # Bumped by forget(), so a run still going for a finished preview is
182
+ # never taken for the next one's, even with the same call id and path.
183
+ self.generation = 0
184
+ self.forget()
185
+ self.running = False
186
+
187
+ def forget(self) -> None:
188
+ """Drop the current call's renderings; its preview has gone."""
189
+ self.generation += 1
190
+ self.call: tuple | None = None
191
+ self.done: tuple[str, list] | None = None
192
+ self.wanted: tuple | None = None
193
+ self.failed = False
194
+
195
+ def rows(self, delta: Delta, call_id: str, path: str, body: str, width: int, code_theme: str):
196
+ if not any(line[:1] in ("-", "+") for line in body.split("\n")):
197
+ return None
198
+ call = (delta, call_id, path, width)
199
+ if self.call is None or self.call[:4] != call:
200
+ # Call ids repeat across responses, so a new path is a new call too.
201
+ self.forget()
202
+ self.call = (*call, self.generation)
203
+ call = self.call
204
+ if self.failed:
205
+ return None
206
+ done = self.done
207
+ if done is None or done[0] != body:
208
+ self.wanted = (call, body)
209
+ if not self.running:
210
+ self._start()
211
+ unified = not delta.side_by_side(width)
212
+ if done is None:
213
+ return _pending_rows(body, width, code_theme) if unified else None
214
+ rendered, rows = done
215
+ if rendered != body and unified and body.startswith(rendered + "\n"):
216
+ return rows + _pending_rows(body[len(rendered) + 1 :], width, code_theme)
217
+ return rows
218
+
219
+ def _start(self) -> None:
220
+ try:
221
+ loop = asyncio.get_running_loop()
222
+ key = self.wanted
223
+ (delta, _, path, width, _), body = key
224
+ future = loop.run_in_executor(None, _live_rows, delta, path, body, width)
225
+ except RuntimeError: # outside an application, or the loop is closing
226
+ return
227
+ self.running = True
228
+ future.add_done_callback(lambda future: self._finished(key, future))
229
+
230
+ def _finished(self, key: tuple, future) -> None:
231
+ self.running = False
232
+ call, body = key
233
+ if call == self.call:
234
+ rows = None if future.cancelled() or future.exception() else future.result()
235
+ if rows is None:
236
+ self.failed = True
237
+ self.on_ready()
238
+ return
239
+ self.done = (body, rows)
240
+ self.on_ready()
241
+ if self.wanted is not None and self.wanted != key and not self.failed:
242
+ self._start()
243
+
244
+
245
+ def _live_rows(delta: Delta, path: str, body: str, width: int):
246
+ """The preview's delta rendering, wrapped to `width` rows of fragments."""
247
+ lines = delta.render(preview_patch(path, body), width, cache=False)
248
+ if lines is None:
249
+ return None
250
+ # A console of its own: this runs on a worker thread.
251
+ console = Console(width=width, color_system="truecolor", force_terminal=True)
252
+ return [
253
+ text_fragments(row, console)
254
+ for line in lines
255
+ for row in line.wrap(console, width, overflow="fold", no_wrap=False)
256
+ ]
257
+
258
+
259
+ def _pending_rows(body: str, width: int, code_theme: str) -> list[tuple[str, str]]:
260
+ """Lines delta has not rendered yet, laid out as delta will lay them out."""
261
+ console = Console(width=width)
262
+ rows = []
263
+ for line in body.split("\n"):
264
+ if line[:1] not in ("-", "+"):
265
+ continue
266
+ style = _preview_style(code_theme, line.startswith("+"))
267
+ wrapped = Text(line[1:]).wrap(console, width, overflow="fold", no_wrap=False)
268
+ rows.extend((style, row.plain) for row in wrapped)
269
+ return rows
270
+
271
+
272
+ def edit_preview_rows(text: str, width: int, code_theme: str) -> list[tuple[str, str]]:
273
+ """Color logical +/- lines before wrapping so continuation rows keep their color."""
274
+ width = max(1, width)
275
+ console = Console(width=width)
276
+ rows = []
277
+ for line in command_text(text).split("\n"):
278
+ style = (
279
+ _preview_style(code_theme, line.startswith("+"))
280
+ if line.startswith(("+", "-"))
281
+ else "class:bottom-toolbar.text"
282
+ )
283
+ rows.extend(
284
+ (style, row.plain)
285
+ for row in Text(line).wrap(console, width, overflow="fold", no_wrap=False)
286
+ )
287
+ return rows