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/syntax_colors.py ADDED
@@ -0,0 +1,206 @@
1
+ """Derive prompt-chrome colors from the selected Pygments style.
2
+
3
+ A Pygments style is written for code sitting on its own background, not for
4
+ UI chrome, so nothing in it can be trusted as-is:
5
+
6
+ - Token colors are optional. `gruvbox-light` leaves `Token.Text` uncolored.
7
+ - `highlight_color` is only loosely related to the background. `gruvbox-dark`
8
+ pairs a near-white highlight (#ebdbb2) with near-white text (#dddddd), so
9
+ using it as the selected-row background would hide the row.
10
+ - Nothing forces the style saved for the dark palette to actually be dark:
11
+ `/syntax solarized-light` is accepted while `/theme` is dark.
12
+
13
+ Every derived color is therefore checked against the surface it will be
14
+ painted on, and falls back to the palette's own value (then to a blend) when
15
+ the contrast is too low. Brightness uses the same weighting as
16
+ `theme.background_theme`, so "is this light or dark" means one thing here.
17
+ """
18
+
19
+ import re
20
+ from collections.abc import Iterable, Mapping
21
+
22
+ from pygments.styles import get_style_by_name
23
+ from pygments.token import Token
24
+ from pygments.util import ClassNotFound
25
+
26
+ # The fields a Palette is built from; every one of them is produced below.
27
+ FIELDS = (
28
+ "accent",
29
+ "muted",
30
+ "surface",
31
+ "foreground",
32
+ "selected",
33
+ "task_heading",
34
+ "success",
35
+ "agents",
36
+ )
37
+ # Where each sub-agent hue comes from, in ring order. Kinds of token a style
38
+ # tends to colour apart from functions (the accent) and strings (success).
39
+ _AGENT_TOKENS = (
40
+ (Token.Keyword, Token.Name.Class),
41
+ (Token.Literal.Number, Token.Name.Builtin),
42
+ (Token.Name.Tag, Token.Name.Decorator),
43
+ )
44
+
45
+ _HEX = re.compile(r"^#?(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$")
46
+ _WEIGHTS = (0.299, 0.587, 0.114)
47
+
48
+ # Minimum brightness gap between a color and the surface behind it. Text has
49
+ # to be read, so it demands more than a background band that only has to be
50
+ # seen. Both are deliberately loose: the aim is to catch invisible pairings,
51
+ # not to impose a contrast standard on a style the user chose on purpose.
52
+ _TEXT_GAP = 0.3
53
+ _ACCENT_GAP = 0.18
54
+ _BAND_GAP = 0.04
55
+
56
+
57
+ def _normalize(color: str | None) -> str | None:
58
+ """Return `#rrggbb`, or None when the style left the value unusable."""
59
+ if not color or not _HEX.match(color):
60
+ return None
61
+ digits = color.lstrip("#")
62
+ if len(digits) == 3:
63
+ digits = "".join(digit * 2 for digit in digits)
64
+ return f"#{digits.lower()}"
65
+
66
+
67
+ def _rgb(color: str) -> tuple[float, ...]:
68
+ digits = color.lstrip("#")
69
+ return tuple(int(digits[index : index + 2], 16) / 255 for index in (0, 2, 4))
70
+
71
+
72
+ def _brightness(color: str) -> float:
73
+ return sum(value * weight for value, weight in zip(_rgb(color), _WEIGHTS))
74
+
75
+
76
+ def _mix(color: str, other: str, weight: float) -> str:
77
+ blended = (
78
+ round((left * (1 - weight) + right * weight) * 255)
79
+ for left, right in zip(_rgb(color), _rgb(other))
80
+ )
81
+ return "#" + "".join(f"{value:02x}" for value in blended)
82
+
83
+
84
+ def _pick(
85
+ candidates: Iterable[str | None],
86
+ requirements: Iterable[tuple[str, float]],
87
+ default: str,
88
+ ) -> str:
89
+ """First candidate keeping the required brightness gap from every backdrop."""
90
+ requirements = tuple(requirements)
91
+ for candidate in candidates:
92
+ color = _normalize(candidate)
93
+ if color and all(
94
+ abs(_brightness(color) - _brightness(backdrop)) >= gap for backdrop, gap in requirements
95
+ ):
96
+ return color
97
+ return default
98
+
99
+
100
+ def _token(style, token) -> str | None:
101
+ return dict(style).get(token, {}).get("color")
102
+
103
+
104
+ def derive_colors(style_name: str, fallback: Mapping, backdrop: str | None = None) -> dict:
105
+ """Palette field values for `style_name`, backed by `fallback`'s values.
106
+
107
+ Without a `backdrop` the colors are meant for the popup, which paints the
108
+ style's own background behind them. Prompt chrome has no such background:
109
+ it sits on whatever the terminal paints, and a style chosen for the other
110
+ appearance would vanish there (`bw` hands out near-black on a dark
111
+ terminal). Pass the background it will actually land on, and the accents
112
+ are checked against that instead, falling back to the palette's own.
113
+
114
+ An unknown style name yields the fallback unchanged: a saved preference
115
+ can outlive the Pygments plugin that provided the style.
116
+ """
117
+ try:
118
+ style = get_style_by_name(style_name)
119
+ except ClassNotFound:
120
+ return {field: fallback[field] for field in FIELDS}
121
+
122
+ surface = _normalize(style.background_color) or fallback["surface"]
123
+ foreground = _pick(
124
+ (_token(style, Token.Text), fallback["foreground"]),
125
+ ((surface, _TEXT_GAP),),
126
+ "#ffffff" if _brightness(surface) < 0.5 else "#1c1c1c",
127
+ )
128
+ # The selected row is a background: it only has to separate from the
129
+ # surface, but whatever is written on it still has to stay readable. The
130
+ # palette's own `selected` is not a candidate -- it belongs to a different
131
+ # background, so a blend off this style's surface stays more coherent.
132
+ selected = _pick(
133
+ (style.highlight_color, getattr(style, "line_number_background_color", None)),
134
+ ((surface, _BAND_GAP), (foreground, _TEXT_GAP)),
135
+ _mix(surface, foreground, 0.25),
136
+ )
137
+ # An accent is written on the surface and on the selected row both, unless
138
+ # the caller named the one background that really is behind it.
139
+ if backdrop is None:
140
+ on_surface = ((surface, _ACCENT_GAP),)
141
+ on_selection = ((surface, _ACCENT_GAP), (selected, _ACCENT_GAP))
142
+ last_resort = {field: foreground for field in ("accent", "task_heading", "success")}
143
+ last_resort["muted"] = _mix(foreground, surface, 0.45)
144
+ last_resort["agents"] = (foreground,) * len(_AGENT_TOKENS)
145
+ else:
146
+ on_surface = on_selection = ((backdrop, _ACCENT_GAP),)
147
+ last_resort = {
148
+ field: fallback[field] for field in ("accent", "muted", "task_heading", "success")
149
+ }
150
+ last_resort["agents"] = fallback["agents"]
151
+ accent = _pick(
152
+ (
153
+ _token(style, Token.Name.Function),
154
+ _token(style, Token.Keyword),
155
+ fallback["accent"],
156
+ ),
157
+ on_selection,
158
+ last_resort["accent"],
159
+ )
160
+ # The tick and each hue must differ from the accent and from each other,
161
+ # or a sub-agent would merge with the active task (or another sub-agent).
162
+ # Monochrome styles (`algol`) colour everything alike, so the palette's
163
+ # own value is the usual answer there.
164
+ taken = {accent}
165
+
166
+ def distinct(candidates, resort):
167
+ color = _pick((c for c in candidates if _normalize(c) not in taken), on_surface, resort)
168
+ taken.add(color)
169
+ return color
170
+
171
+ success = distinct(
172
+ (_token(style, Token.Literal.String), fallback["success"]), last_resort["success"]
173
+ )
174
+ agents = [
175
+ distinct([*(_token(style, token) for token in tokens), default], resort)
176
+ for tokens, default, resort in zip(_AGENT_TOKENS, fallback["agents"], last_resort["agents"])
177
+ ]
178
+ return {
179
+ "surface": surface,
180
+ "foreground": foreground,
181
+ "selected": selected,
182
+ "success": success,
183
+ "agents": tuple(agents),
184
+ "accent": accent,
185
+ "muted": _pick(
186
+ (_token(style, Token.Comment), fallback["muted"]),
187
+ on_surface,
188
+ last_resort["muted"],
189
+ ),
190
+ # Apart from the accent, since the status row's detail and the session
191
+ # name wear it beside the accented phase. Not added to `taken`: the
192
+ # first sub-agent hue shares the Keyword colour on purpose.
193
+ "task_heading": _pick(
194
+ (
195
+ color
196
+ for color in (
197
+ _token(style, Token.Keyword),
198
+ _token(style, Token.Name.Class),
199
+ fallback["task_heading"],
200
+ )
201
+ if _normalize(color) != accent
202
+ ),
203
+ on_surface,
204
+ last_resort["task_heading"],
205
+ ),
206
+ }
pcode/task_prompt.py ADDED
@@ -0,0 +1,28 @@
1
+ """Literal, quote-styled submitted prompts."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from rich.console import Console, ConsoleOptions, RenderResult
6
+ from rich.segment import Segment
7
+ from rich.text import Text
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class TaskPrompt:
12
+ text: str
13
+
14
+ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
15
+ # Popups render prompts on a console that may carry no pcode theme.
16
+ style = console.get_style("pcode.accent", default="none")
17
+ # Leave room for a quote rail on every physical line, including wraps.
18
+ # On one-column terminals prioritize the text rather than the decoration.
19
+ prefix = "▌ " if options.max_width > 2 else ""
20
+ lines = console.render_lines(
21
+ Text(self.text, style=style),
22
+ options.update(width=max(1, options.max_width - len(prefix))),
23
+ pad=False,
24
+ )
25
+ for line in lines:
26
+ yield Segment(prefix, style)
27
+ yield from line
28
+ yield Segment.line()