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/plan_sizes.py ADDED
@@ -0,0 +1,35 @@
1
+ """Plan step sizes and the progress they weight.
2
+
3
+ Kept free of Pydantic AI so the terminal's tab bar can import it at startup.
4
+ """
5
+
6
+ from collections.abc import Mapping, Sequence
7
+ from typing import Literal
8
+
9
+ Size = Literal["S", "M", "L"]
10
+
11
+ # Relative effort, not time: models compare sizes far better than they guess
12
+ # durations, and three buckets leave nothing to agonise over. Unsized steps
13
+ # count as M. Roughly geometric, so one L outweighs a few quick steps.
14
+ SIZE_WEIGHTS: dict[str, int] = {"S": 1, "M": 3, "L": 8}
15
+
16
+
17
+ def plan_progress(items: Sequence[Mapping]) -> float | None:
18
+ """How far through the plan the work is, 0..1, weighted by step size.
19
+
20
+ Cancelled steps leave the total, so a dropped step never strands the bar
21
+ short of full, and the running step counts half: the bar moves when work
22
+ starts rather than only when it lands. None when nothing counts.
23
+ """
24
+ total = done = 0.0
25
+ for item in items:
26
+ status = item.get("status")
27
+ if status == "cancelled":
28
+ continue
29
+ weight = SIZE_WEIGHTS.get(item.get("size") or "M", SIZE_WEIGHTS["M"])
30
+ total += weight
31
+ if status == "completed":
32
+ done += weight
33
+ elif status == "in_progress":
34
+ done += weight / 2
35
+ return done / total if total else None
pcode/planning.py ADDED
@@ -0,0 +1,161 @@
1
+ """Harness planning, tuned for a plan the user watches as a live checklist."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from pydantic import Field
6
+ from pydantic_ai import CapabilityEvent, RunContext
7
+ from pydantic_ai_harness.planning import PlanItem, Planning, PlanningToolset
8
+
9
+ from pcode.plan_sizes import Size
10
+ from pcode.tool_display import PLAN_TOOLS
11
+
12
+ # Replace Harness's "multi-step work" threshold with default use for visible
13
+ # progress, including small tasks and investigations. Say to create the plan
14
+ # early: maintenance rules alone leave creating one optional.
15
+ GUIDANCE = (
16
+ "Use `write_plan` by default when working on a request. The plan is the user's "
17
+ "live checklist of what you are doing, what is done, and what is left—not just "
18
+ "a tool for organizing complex work. Small tasks and investigations count, even "
19
+ "if the plan has only one step. Create it early and revise it as you learn; "
20
+ "you do not need to know the whole solution first. "
21
+ "Keep it truthful: one step `in_progress` while work is underway, "
22
+ "and when a step finishes, one `update_task_statuses` call that completes it "
23
+ "and starts the next. Use `add_task` for a step you discover midway; "
24
+ "use `write_plan` only to create or restructure the plan, and pass the full "
25
+ "plan when you do. Before your final reply, every step should be `completed` or "
26
+ "`cancelled`, unless you are stopping to ask the user; then leave the rest as it is. "
27
+ "Give a step `size` L when it holds most of the work and S when it is trivial; the "
28
+ "user's progress bar is weighted by it."
29
+ )
30
+
31
+ ID_GUIDANCE = (
32
+ " Plan row numbers are display positions, not task IDs. For granular updates, use "
33
+ "the exact stable IDs returned by write_plan or read_plan; never guess IDs from row "
34
+ "numbers. Preserve those IDs when rewriting a plan."
35
+ )
36
+
37
+ # Harness's description tells the model to call write_plan "first for multi-step
38
+ # work, then again as you start and finish steps", which repeats the narrow
39
+ # trigger GUIDANCE drops and steers routine progress away from the cheaper
40
+ # status tools.
41
+ WRITE_PLAN_DESCRIPTION = (
42
+ "Create or replace the entire plan. Pass the whole ordered list every time -- "
43
+ "including steps that are unchanged, completed, or cancelled -- so there are no "
44
+ "indices to track. Use it to create the plan or restructure it; report routine "
45
+ "progress with `update_task_statuses` instead. Keep one step `in_progress` while "
46
+ "work is underway."
47
+ )
48
+
49
+
50
+ SIZE_DESCRIPTION = (
51
+ "Relative effort: S (a quick check or one-line change), M (typical; the default), "
52
+ "L (the bulk of the work, e.g. the main implementation or a long test run). "
53
+ "Omit for M."
54
+ )
55
+
56
+
57
+ class SizedPlanItem(PlanItem):
58
+ """One step in the plan.
59
+
60
+ `id` is auto-generated when omitted, so the model can create a plan without
61
+ ids and reference existing steps by id when restructuring. `size` weights
62
+ the user's progress bar.
63
+ """
64
+
65
+ size: Size | None = Field(default=None, description=SIZE_DESCRIPTION)
66
+
67
+
68
+ class SizedPlanningToolset(PlanningToolset):
69
+ """Harness's plan tools, with `size` accepted wherever a step is created."""
70
+
71
+ async def write_plan(self, ctx: RunContext, items: list[SizedPlanItem]) -> str: # type: ignore[override]
72
+ """Create or replace the whole plan.
73
+
74
+ Args:
75
+ ctx: Framework-provided run context.
76
+ items: The complete ordered list of plan steps.
77
+ """
78
+ return await super().write_plan(ctx, list(items))
79
+
80
+ async def add_task( # type: ignore[override]
81
+ self,
82
+ ctx: RunContext,
83
+ content: str,
84
+ active_form: str = "",
85
+ size: Size | None = None,
86
+ ) -> str:
87
+ """Add one new pending step.
88
+
89
+ Args:
90
+ ctx: Framework-provided run context.
91
+ content: The step description in imperative form.
92
+ active_form: Optional present-continuous label, e.g. "Fix bug" -> "Fixing bug".
93
+ size: Relative effort: S (a quick check or one-line change), M (typical; the
94
+ default), L (the bulk of the work, e.g. the main implementation or a long
95
+ test run). Omit for M.
96
+ """
97
+ store = self._resolve(ctx)
98
+ before = await store.get_items()
99
+ item = await store.add_item(
100
+ SizedPlanItem(content=content, active_form=active_form, size=size)
101
+ )
102
+ await self._emit_changes(ctx, before, await store.get_items())
103
+ return f"Added step '{content}' with id: {item.id}"
104
+
105
+
106
+ @dataclass(kw_only=True)
107
+ class PlanSnapshot(CapabilityEvent, namespace="pcode_planning", name="snapshot"):
108
+ """The whole plan after a planning call, announced on the run's own event stream.
109
+
110
+ A sub-agent's plan lives in a store private to its run. Announcing it lets
111
+ the parent's display show it without reaching into, or replacing, that store.
112
+ """
113
+
114
+ items: list[dict]
115
+
116
+
117
+ @dataclass
118
+ class IdentifiedPlanning(Planning):
119
+ """Keep upstream validation/storage; change what the model is told about the plan.
120
+
121
+ GUIDANCE names the full core toolset. A caller narrowing `tools` or enabling
122
+ subtasks should pass its own `guidance`, as upstream's `tools` docs already advise.
123
+ """
124
+
125
+ # Tool results already report changes; read_plan retrieves state when needed.
126
+ inject: bool = False
127
+
128
+ def __post_init__(self):
129
+ # An explicit `descriptions` still wins per tool.
130
+ self.descriptions = {"write_plan": WRITE_PLAN_DESCRIPTION, **(self.descriptions or {})}
131
+
132
+ @classmethod
133
+ def from_spec(cls, *, inject: bool = False, **kwargs):
134
+ return super().from_spec(inject=inject, **kwargs)
135
+
136
+ def get_toolset(self):
137
+ return SizedPlanningToolset(self)
138
+
139
+ def get_instructions(self):
140
+ # As upstream: None means the default, "" drops it. The ID note always stays.
141
+ guidance = GUIDANCE if self.guidance is None else self.guidance
142
+ return (guidance + ID_GUIDANCE).strip()
143
+
144
+ async def after_tool_execute(self, ctx, *, call, tool_def, args, result):
145
+ result = await super().after_tool_execute(
146
+ ctx, call=call, tool_def=tool_def, args=args, result=result
147
+ )
148
+ if call.tool_name in PLAN_TOOLS:
149
+ items = await self.resolve_store(ctx).get_items()
150
+ await ctx.emit(PlanSnapshot(items=[item.model_dump(mode="json") for item in items]))
151
+ if (
152
+ call.tool_name == "write_plan"
153
+ and isinstance(result, str)
154
+ and result.startswith("Plan updated:")
155
+ ):
156
+ items = await self.resolve_store(ctx).get_items()
157
+ if items:
158
+ result += "\n\nStable task IDs (not row numbers):\n" + "\n".join(
159
+ f"{index}. task_id={item.id}" for index, item in enumerate(items, 1)
160
+ )
161
+ return result