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/inspection.py
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""Read-only inspection projection; never used to replay or execute tools.
|
|
2
|
+
|
|
3
|
+
Saved payloads stay in the existing private UI journal and are loaded on selection.
|
|
4
|
+
Unsaved payloads have a global memory budget; call metadata is retained on eviction.
|
|
5
|
+
File changes are held in memory either way: each patch is already capped when made.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import math
|
|
10
|
+
import re
|
|
11
|
+
from collections import deque
|
|
12
|
+
from dataclasses import asdict, dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from pcode.edits import change_from_record
|
|
16
|
+
from pcode.runtime import EditCompleted, ToolStarted, ToolSummary
|
|
17
|
+
from pcode.tool_display import command_text
|
|
18
|
+
|
|
19
|
+
PAYLOAD_LIMIT = 128 * 1024 # characters per arguments/result field
|
|
20
|
+
MEMORY_LIMIT = 8 * 1024 * 1024 # UTF-8 bytes of unsaved payload text
|
|
21
|
+
MISSING = "Details were not captured for this call (older session or preview)."
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _redact_fields(value: object) -> object:
|
|
25
|
+
if isinstance(value, dict):
|
|
26
|
+
return {
|
|
27
|
+
key: "[redacted]"
|
|
28
|
+
if re.search(
|
|
29
|
+
r"(?i)password|passwd|secret|token|authorization|api.?key|private.?key", str(key)
|
|
30
|
+
)
|
|
31
|
+
else _redact_fields(item)
|
|
32
|
+
for key, item in value.items()
|
|
33
|
+
}
|
|
34
|
+
if isinstance(value, (list, tuple)):
|
|
35
|
+
return [_redact_fields(item) for item in value]
|
|
36
|
+
return value
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def capture(value: object) -> str:
|
|
40
|
+
"""Render a bounded, redacted text projection, not arbitrary object reprs."""
|
|
41
|
+
text = (
|
|
42
|
+
value
|
|
43
|
+
if isinstance(value, str)
|
|
44
|
+
else json.dumps(
|
|
45
|
+
_redact_fields(value),
|
|
46
|
+
ensure_ascii=False,
|
|
47
|
+
indent=2,
|
|
48
|
+
default=lambda obj: f"<{type(obj).__name__} omitted>",
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
text = command_text(text)
|
|
52
|
+
if len(text) > PAYLOAD_LIMIT:
|
|
53
|
+
text = text[:PAYLOAD_LIMIT] + "\n[Inspector truncated this payload at 128 Ki characters.]"
|
|
54
|
+
return text
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class Payload:
|
|
59
|
+
text: str | None = None
|
|
60
|
+
path: Path | None = None
|
|
61
|
+
offset: int = 0
|
|
62
|
+
key: str = ""
|
|
63
|
+
|
|
64
|
+
def read(self) -> str:
|
|
65
|
+
if self.path is None:
|
|
66
|
+
return self.text if self.text is not None else MISSING
|
|
67
|
+
try:
|
|
68
|
+
with self.path.open("rb") as file:
|
|
69
|
+
file.seek(self.offset)
|
|
70
|
+
record = json.loads(file.readline())
|
|
71
|
+
if not isinstance(record, dict):
|
|
72
|
+
return "Saved details are unavailable: damaged journal record."
|
|
73
|
+
value = record.get(self.key)
|
|
74
|
+
if value is None and self.key == "result" and record.get("error"):
|
|
75
|
+
return capture(record["error"]) + "\n\n" + MISSING
|
|
76
|
+
return capture(value) if value is not None else MISSING
|
|
77
|
+
except (OSError, ValueError):
|
|
78
|
+
return "Saved details are unavailable or the journal record is damaged."
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class InspectedCall:
|
|
83
|
+
call_id: str
|
|
84
|
+
name: str
|
|
85
|
+
run_id: str
|
|
86
|
+
started_at: str = "unavailable"
|
|
87
|
+
state: str = "running"
|
|
88
|
+
detail: str = ""
|
|
89
|
+
summary: str = ""
|
|
90
|
+
elapsed: float | None = None
|
|
91
|
+
outcome: str = ""
|
|
92
|
+
process_id: str = ""
|
|
93
|
+
execution: str = ""
|
|
94
|
+
arguments: Payload = field(default_factory=Payload)
|
|
95
|
+
result: Payload = field(default_factory=Payload)
|
|
96
|
+
# The file changes this call made: already sanitized and size-capped.
|
|
97
|
+
changes: list[EditCompleted] = field(default_factory=list)
|
|
98
|
+
|
|
99
|
+
def title(self) -> str:
|
|
100
|
+
# Only "background" is worth a row tag: waiting is the default, and the
|
|
101
|
+
# detail pane names the mode either way.
|
|
102
|
+
tag = " (background)" if self.execution == "background" else ""
|
|
103
|
+
return f"{self.state:11} {self.name}{tag} · {self.detail}"
|
|
104
|
+
|
|
105
|
+
def metadata(self, calls: list["InspectedCall"]) -> list[tuple[str, str]]:
|
|
106
|
+
"""Label/value rows describing the call, shared by text and Rich renderings."""
|
|
107
|
+
timing = f"{self.elapsed:.2f}s" if self.elapsed is not None else "unavailable"
|
|
108
|
+
rows = [
|
|
109
|
+
("Call", self.call_id),
|
|
110
|
+
("Run", self.run_id),
|
|
111
|
+
("Started", self.started_at),
|
|
112
|
+
("Duration", timing),
|
|
113
|
+
*([("Execution", self.execution)] if self.execution else []),
|
|
114
|
+
("Framework outcome", self.outcome or "unavailable"),
|
|
115
|
+
("Summary", self.summary),
|
|
116
|
+
]
|
|
117
|
+
if self.process_id:
|
|
118
|
+
related = [
|
|
119
|
+
c.call_id for c in calls if c is not self and c.process_id == self.process_id
|
|
120
|
+
]
|
|
121
|
+
rows.append(("Background process", self.process_id))
|
|
122
|
+
rows.append(("Related calls", ", ".join(related) or "none"))
|
|
123
|
+
return rows
|
|
124
|
+
|
|
125
|
+
def details(self, calls: list["InspectedCall"]) -> str:
|
|
126
|
+
metadata = f"{self.name} · {self.state}\n" + "".join(
|
|
127
|
+
f"{label}: {value}\n" for label, value in self.metadata(calls)
|
|
128
|
+
)
|
|
129
|
+
return command_text(
|
|
130
|
+
metadata
|
|
131
|
+
+ "\nArguments\n─────────\n"
|
|
132
|
+
+ self.arguments.read()
|
|
133
|
+
+ "\n\nReturned result / error\n───────────────────────\n"
|
|
134
|
+
+ self.result.read()
|
|
135
|
+
+ "\n\nOnly captured tool output is shown; tool-side truncation cannot be recovered."
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class ToolArchive:
|
|
140
|
+
def __init__(self) -> None:
|
|
141
|
+
self.calls: list[InspectedCall] = []
|
|
142
|
+
self._running: dict[tuple[str, str], InspectedCall] = {}
|
|
143
|
+
self._payloads: deque[tuple[Payload, int]] = deque()
|
|
144
|
+
self._bytes = 0
|
|
145
|
+
self.run_id = "unavailable"
|
|
146
|
+
self._path: Path | None = None
|
|
147
|
+
self._file_id: tuple[int, int] | None = None
|
|
148
|
+
self._offset = 0
|
|
149
|
+
|
|
150
|
+
def settle(self, state: str) -> None:
|
|
151
|
+
for call in self._running.values():
|
|
152
|
+
call.state = state
|
|
153
|
+
self._running.clear()
|
|
154
|
+
|
|
155
|
+
def _payload(self, record: dict, key: str, path: Path | None, offset: int) -> Payload:
|
|
156
|
+
if path is not None:
|
|
157
|
+
return Payload(path=path, offset=offset, key=key)
|
|
158
|
+
value = record.get(key)
|
|
159
|
+
if value is None and key == "result" and record.get("error"):
|
|
160
|
+
value = capture(record["error"]) + "\n\n" + MISSING
|
|
161
|
+
payload = Payload(text=capture(value) if value is not None else None)
|
|
162
|
+
size = len((payload.text or "").encode("utf-8"))
|
|
163
|
+
self._payloads.append((payload, size))
|
|
164
|
+
self._bytes += size
|
|
165
|
+
while self._bytes > MEMORY_LIMIT:
|
|
166
|
+
old, count = self._payloads.popleft()
|
|
167
|
+
old.text = "[Payload evicted from the unsaved session's 8 MiB inspection budget.]"
|
|
168
|
+
self._bytes -= count
|
|
169
|
+
return payload
|
|
170
|
+
|
|
171
|
+
def record(self, record: dict, *, path: Path | None = None, offset: int = 0) -> None:
|
|
172
|
+
kind = record.get("kind")
|
|
173
|
+
if not isinstance(kind, str):
|
|
174
|
+
return
|
|
175
|
+
# Ignore malformed metadata without losing healthy surrounding records.
|
|
176
|
+
for key in (
|
|
177
|
+
"name",
|
|
178
|
+
"call_id",
|
|
179
|
+
"run_id",
|
|
180
|
+
"detail",
|
|
181
|
+
"command",
|
|
182
|
+
"purpose",
|
|
183
|
+
"process_id",
|
|
184
|
+
"execution",
|
|
185
|
+
"started_at",
|
|
186
|
+
"time",
|
|
187
|
+
"outcome",
|
|
188
|
+
):
|
|
189
|
+
if key in record and not isinstance(record[key], str):
|
|
190
|
+
return
|
|
191
|
+
if kind == "turn_started":
|
|
192
|
+
self.settle("unknown")
|
|
193
|
+
self.run_id = record.get("run_id", str(offset))
|
|
194
|
+
return
|
|
195
|
+
if kind in {"turn_completed", "turn_failed", "turn_cancelled"}:
|
|
196
|
+
self.settle("interrupted" if kind == "turn_cancelled" else "unknown")
|
|
197
|
+
return
|
|
198
|
+
if kind == "EditCompleted":
|
|
199
|
+
self._change(record)
|
|
200
|
+
return
|
|
201
|
+
if kind not in {"ToolStarted", "ToolSummary"}:
|
|
202
|
+
return
|
|
203
|
+
if not record.get("name"):
|
|
204
|
+
return
|
|
205
|
+
call_id = record.get("call_id") or f"unavailable-{len(self.calls) + 1}"
|
|
206
|
+
run_id = record.get("run_id") or self.run_id
|
|
207
|
+
key = (run_id, call_id)
|
|
208
|
+
call = self._running.get(key)
|
|
209
|
+
if call is None:
|
|
210
|
+
call = InspectedCall(call_id, capture(record["name"]), run_id)
|
|
211
|
+
self.calls.append(call)
|
|
212
|
+
# Purpose leads the row, command follows it: this list is the inventory
|
|
213
|
+
# of what ran, so it must keep the command a stated intention can't prove.
|
|
214
|
+
invocation = record.get("command") or record.get("detail", "")
|
|
215
|
+
purpose = record.get("purpose") or ""
|
|
216
|
+
if purpose and record.get("command"):
|
|
217
|
+
invocation = f"{purpose} · {invocation}"
|
|
218
|
+
call.detail = command_text(invocation)[:300]
|
|
219
|
+
call.summary = command_text(record.get("detail", ""))[:1000]
|
|
220
|
+
call.process_id = command_text(record.get("process_id") or call.process_id)
|
|
221
|
+
call.execution = record.get("execution") or call.execution
|
|
222
|
+
if kind == "ToolStarted":
|
|
223
|
+
call.started_at = record.get("started_at") or record.get("time", "unavailable")
|
|
224
|
+
call.arguments = self._payload(record, "arguments", path, offset)
|
|
225
|
+
self._running[key] = call
|
|
226
|
+
else:
|
|
227
|
+
call.result = self._payload(record, "result", path, offset)
|
|
228
|
+
elapsed = record.get("elapsed_seconds")
|
|
229
|
+
call.elapsed = (
|
|
230
|
+
elapsed
|
|
231
|
+
if (type(elapsed) in (int, float) and math.isfinite(elapsed) and elapsed >= 0)
|
|
232
|
+
else None
|
|
233
|
+
)
|
|
234
|
+
call.state = "failed" if record.get("failed") else "succeeded"
|
|
235
|
+
call.outcome = record.get("outcome", "")
|
|
236
|
+
self._running.pop(key, None)
|
|
237
|
+
|
|
238
|
+
def _change(self, record: dict) -> None:
|
|
239
|
+
"""Attach a change to the call in this turn that made it."""
|
|
240
|
+
call_id = record.get("call_id")
|
|
241
|
+
if not (
|
|
242
|
+
call_id
|
|
243
|
+
and isinstance(record.get("path"), str)
|
|
244
|
+
and isinstance(record.get("operation"), str)
|
|
245
|
+
and all(isinstance(record.get(key, ""), str) for key in ("patch", "omitted"))
|
|
246
|
+
and all(type(record.get(key, 0)) is int for key in ("added", "removed"))
|
|
247
|
+
and type(record.get("truncated", False)) is bool
|
|
248
|
+
):
|
|
249
|
+
return
|
|
250
|
+
# Retries and resumed sessions can reuse a call id, so the turn must match too.
|
|
251
|
+
run_id = record.get("run_id") or self.run_id
|
|
252
|
+
call = next(
|
|
253
|
+
(c for c in reversed(self.calls) if (c.run_id, c.call_id) == (run_id, call_id)), None
|
|
254
|
+
)
|
|
255
|
+
if call is not None:
|
|
256
|
+
call.changes.append(change_from_record(record))
|
|
257
|
+
|
|
258
|
+
def event(self, event: ToolStarted | ToolSummary | EditCompleted) -> None:
|
|
259
|
+
self.record({"kind": type(event).__name__, **asdict(event)})
|
|
260
|
+
|
|
261
|
+
def update(self, path: Path) -> None:
|
|
262
|
+
"""Incrementally index complete appends; payloads remain file-offset references."""
|
|
263
|
+
stat = path.stat()
|
|
264
|
+
identity = (stat.st_dev, stat.st_ino)
|
|
265
|
+
if path != self._path or identity != self._file_id or stat.st_size < self._offset:
|
|
266
|
+
self.__init__()
|
|
267
|
+
self._path, self._file_id = path, identity
|
|
268
|
+
with path.open("rb") as file:
|
|
269
|
+
file.seek(self._offset)
|
|
270
|
+
while True:
|
|
271
|
+
offset = file.tell()
|
|
272
|
+
line = file.readline()
|
|
273
|
+
if not line or not line.endswith(b"\n"):
|
|
274
|
+
break # Retry an incomplete append on the next open.
|
|
275
|
+
self._offset = file.tell()
|
|
276
|
+
try:
|
|
277
|
+
record = json.loads(line)
|
|
278
|
+
except ValueError:
|
|
279
|
+
continue
|
|
280
|
+
if isinstance(record, dict):
|
|
281
|
+
self.record(record, path=path, offset=offset)
|
|
282
|
+
for call in self._running.values():
|
|
283
|
+
call.state = "unknown"
|
|
284
|
+
|
|
285
|
+
@classmethod
|
|
286
|
+
def load(cls, path: Path) -> "ToolArchive":
|
|
287
|
+
archive = cls()
|
|
288
|
+
archive.update(path)
|
|
289
|
+
return archive
|