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
pcode/edit_transcript.py
ADDED
|
@@ -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
|