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
pcode/workers.py ADDED
@@ -0,0 +1,136 @@
1
+ """Each delegated worker's own stream, kept for the read-only viewer; no terminal imports.
2
+
3
+ The conversation's transcript shows a worker only as its delegate row and the
4
+ calls beneath it. This keeps what the worker itself said and did, in order, so
5
+ `/agents` can show it while it runs and after it settles. Memory only: nothing
6
+ here is journaled, so a resumed session starts with no workers.
7
+ """
8
+
9
+ from dataclasses import dataclass, field
10
+ from time import monotonic
11
+
12
+ from pcode.runtime import ChildPlan, ChildText, ToolStarted, ToolSummary
13
+ from pcode.tool_display import PLAN_TOOLS
14
+
15
+ DELEGATE = "delegate_task"
16
+ # Workers accumulate over a long session; keep the recent ones.
17
+ WORKER_HISTORY = 20
18
+
19
+
20
+ @dataclass
21
+ class WorkerEntry:
22
+ """One step of a worker's stream: a run of prose or reasoning, or one tool call."""
23
+
24
+ kind: str # "text", "thinking" or "tool"
25
+ text: str = ""
26
+ tool: ToolStarted | ToolSummary | None = None
27
+
28
+
29
+ @dataclass
30
+ class Worker:
31
+ call_id: str
32
+ agent: str
33
+ task: str
34
+ started: float = field(default_factory=monotonic)
35
+ settled: float | None = None
36
+ failed: bool = False
37
+ # Why it stopped when that was not a normal finish, e.g. "Interrupted".
38
+ ending: str = ""
39
+ activity: str = ""
40
+ plan: list[dict] = field(default_factory=list)
41
+ entries: list[WorkerEntry] = field(default_factory=list)
42
+ # Bumped on every change, so a viewer can skip unchanged repaints.
43
+ version: int = 0
44
+
45
+ @property
46
+ def running(self) -> bool:
47
+ return self.settled is None
48
+
49
+ def elapsed(self) -> float:
50
+ return (self.settled if self.settled is not None else monotonic()) - self.started
51
+
52
+ def state(self) -> str:
53
+ if self.running:
54
+ return self.activity or "Starting"
55
+ return self.ending or ("Failed" if self.failed else "Done")
56
+
57
+ def add_text(self, event: ChildText) -> None:
58
+ kind = "thinking" if event.thinking else "text"
59
+ last = self.entries[-1] if self.entries else None
60
+ if event.start or last is None or last.kind != kind:
61
+ self.entries.append(WorkerEntry(kind, event.text))
62
+ else:
63
+ last.text += event.text
64
+
65
+ def add_tool(self, event: ToolStarted | ToolSummary) -> None:
66
+ """A start adds a row; its restatement or result replaces that row."""
67
+ for entry in reversed(self.entries):
68
+ if entry.tool is not None and entry.tool.call_id == event.call_id:
69
+ entry.tool = event
70
+ return
71
+ self.entries.append(WorkerEntry("tool", tool=event))
72
+
73
+
74
+ @dataclass
75
+ class Workers:
76
+ items: list[Worker] = field(default_factory=list)
77
+
78
+ def get(self, call_id: str) -> Worker | None:
79
+ return next((w for w in self.items if w.call_id == call_id), None)
80
+
81
+ def record(self, event) -> None:
82
+ """Fold one runtime event into the worker it belongs to, if any."""
83
+ if isinstance(event, ToolStarted | ToolSummary) and event.name == DELEGATE:
84
+ self._delegate(event)
85
+ return
86
+ call_id = (
87
+ event.call_id
88
+ if isinstance(event, ChildText | ChildPlan)
89
+ else event.parent_call_id
90
+ if isinstance(event, ToolStarted | ToolSummary)
91
+ else ""
92
+ )
93
+ worker = self.get(call_id) if call_id else None
94
+ if worker is None:
95
+ return
96
+ if isinstance(event, ChildText):
97
+ worker.add_text(event)
98
+ elif isinstance(event, ChildPlan):
99
+ worker.plan = event.items
100
+ elif event.name not in PLAN_TOOLS:
101
+ # The plan has its own section; its tool calls would only repeat it.
102
+ worker.add_tool(event)
103
+ worker.version += 1
104
+
105
+ def _delegate(self, event: ToolStarted | ToolSummary) -> None:
106
+ worker = self.get(event.call_id)
107
+ if isinstance(event, ToolStarted):
108
+ if worker is None:
109
+ worker = Worker(event.call_id, event.agent or "worker", event.task)
110
+ self.items.append(worker)
111
+ del self.items[:-WORKER_HISTORY]
112
+ # A restated start carries the worker's current phase.
113
+ worker.activity = event.activity
114
+ elif worker is not None and worker.running:
115
+ worker.settled = monotonic()
116
+ worker.failed = event.failed
117
+ else:
118
+ return
119
+ worker.version += 1
120
+
121
+ def end_turn(self) -> None:
122
+ """A turn that ends under a running worker stopped it, e.g. by cancellation."""
123
+ for worker in self.items:
124
+ if worker.running:
125
+ worker.settled = monotonic()
126
+ worker.failed = True
127
+ worker.ending = "Interrupted"
128
+ worker.version += 1
129
+
130
+ def running(self) -> int:
131
+ return sum(worker.running for worker in self.items)
132
+
133
+ def latest(self) -> Worker | None:
134
+ """The newest running worker, else the newest one."""
135
+ running = [worker for worker in self.items if worker.running]
136
+ return (running or self.items or [None])[-1]
pcode/workspace.py ADDED
@@ -0,0 +1,57 @@
1
+ """A workspace can be deleted while a session is still working in it.
2
+
3
+ Session worktrees are removed from outside the session that owns them: another
4
+ session merges and removes it, `/worktree clean` sweeps it, someone runs `git
5
+ worktree prune`. Every tool in this process then resolves paths against a
6
+ directory that is gone.
7
+
8
+ Harness reports that as a retryable failure, on the assumption that the model
9
+ destroyed its own working directory and can `cd` out of it. Here the working
10
+ directory is the workspace, fixed for the whole run, so no command the model
11
+ can write will succeed -- a `cd /tmp` prefix does not help, because the tool
12
+ chdirs into the workspace before running it. Retrying only spends the tool
13
+ budget and ends the turn with a message blaming the model. Stop on the first
14
+ call instead, and name the directory that disappeared.
15
+ """
16
+
17
+ from pathlib import Path
18
+
19
+ from pydantic_ai.capabilities import AbstractCapability
20
+
21
+
22
+ class WorkspaceGoneError(Exception):
23
+ """The workspace directory disappeared; nothing in the run can recover.
24
+
25
+ Deliberately not `ModelRetry`: the model has no move that fixes it.
26
+ """
27
+
28
+
29
+ def require_workspace(workspace: Path) -> None:
30
+ """Raise `WorkspaceGoneError` unless tools can still run in `workspace`."""
31
+ if workspace.is_dir():
32
+ return
33
+ raise WorkspaceGoneError(
34
+ f"The workspace {workspace} no longer exists, so no tool can run in it. "
35
+ "It was deleted while this session was open, usually by a worktree removed "
36
+ "from elsewhere. Resume this conversation in another directory with "
37
+ "`pcode -C DIR --continue <session>`."
38
+ )
39
+
40
+
41
+ class WorkspaceGuard(AbstractCapability):
42
+ """Check the workspace still exists before a run and before each tool call.
43
+
44
+ The run-start check comes first in the capability list, so it names the
45
+ missing directory before Harness's own capabilities fail on it with a
46
+ generic `WorkspaceUnavailableError`.
47
+ """
48
+
49
+ def __init__(self, workspace: Path) -> None:
50
+ self.workspace = Path(workspace)
51
+
52
+ async def before_run(self, ctx) -> None:
53
+ require_workspace(self.workspace)
54
+
55
+ async def wrap_tool_execute(self, ctx, *, call, tool_def, args, handler):
56
+ require_workspace(self.workspace)
57
+ return await handler(args)
@@ -0,0 +1,72 @@
1
+ """Unconfined file paths with a stable workspace-relative base.
2
+
3
+ Harness resolves relative paths from the workspace's working directory and
4
+ reports walked paths relative to it; a `root_dir` of `/` lifts the boundary,
5
+ so the workspace stays the base without being a sandbox. See
6
+ dev/dependencies.md before upgrading the filesystem integration.
7
+ """
8
+
9
+ from dataclasses import fields
10
+ from pathlib import Path
11
+
12
+ from pydantic_ai_harness.filesystem import FileSystem, FileSystemToolset
13
+
14
+
15
+ def _any_depth(pattern: str) -> str:
16
+ """Match a protected-file pattern at any depth, since the root is `/`."""
17
+ return pattern if pattern.startswith("**/") else f"**/{pattern}"
18
+
19
+
20
+ class WorkspaceFileSystem(FileSystem):
21
+ """Keep workspace-relative paths without using the workspace as a boundary."""
22
+
23
+ def __post_init__(self) -> None:
24
+ super().__post_init__()
25
+ # Workspace paths are POSIX, so `/` is the whole filesystem on any host.
26
+ self.root_dir = "/"
27
+ # Protected-file rules still apply, including to paths outside the workspace.
28
+ self.read_only_patterns = [_any_depth(pattern) for pattern in self.read_only_patterns]
29
+
30
+ @classmethod
31
+ def from_filesystem(cls, filesystem: FileSystem) -> "WorkspaceFileSystem":
32
+ values = {f.name: getattr(filesystem, f.name) for f in fields(FileSystem) if f.init}
33
+ # Deprecated upstream aliases; passing them only re-triggers the warning.
34
+ values.pop("cwd", None)
35
+ values.pop("protected_patterns", None)
36
+ return cls(**values)
37
+
38
+ def get_instructions(self):
39
+ async def instructions(ctx) -> str:
40
+ base = await ctx.workspace.working_dir()
41
+ return (
42
+ f"File tools accept absolute paths anywhere on the host and relative paths "
43
+ f"based on the workspace {base!r}, including '..'. "
44
+ "Shell cd does not change that file-tool base. File tools are not a sandbox. "
45
+ "Search and listing tools default to the workspace. "
46
+ "Returned relative paths use that same workspace base. "
47
+ "Existing protected-file write rules still apply to file tools, not shell commands."
48
+ )
49
+
50
+ return instructions
51
+
52
+ def _toolset_type(self) -> type[FileSystemToolset]:
53
+ return FileSystemToolset
54
+
55
+ def _file_system_toolset(self) -> FileSystemToolset:
56
+ if self._toolset is None:
57
+ self._toolset = self._toolset_type()(
58
+ root_dir=Path(self.root_dir),
59
+ allowed_patterns=self.allowed_patterns,
60
+ denied_patterns=self.denied_patterns,
61
+ read_only_patterns=self.read_only_patterns,
62
+ max_read_lines=self.max_read_lines,
63
+ max_read_chars=self.max_read_chars,
64
+ max_list_results=self.max_list_results,
65
+ max_search_results=self.max_search_results,
66
+ max_find_results=self.max_find_results,
67
+ id=self.id or "file_system",
68
+ content_hashes=self.content_hashes,
69
+ tools=self.tools,
70
+ max_retries=self.max_retries,
71
+ )
72
+ return self._toolset