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/appearance.py ADDED
@@ -0,0 +1,181 @@
1
+ """Notice the terminal switching between light and dark while pcode runs.
2
+
3
+ Two triggers, both answered through `TerminalReplyParser` in the input stream:
4
+
5
+ - Mode 2031: terminals that support it (Ghostty, kitty, iTerm2, VTE, foot, tmux)
6
+ report `CSI ? 997 ; 1|2 n` whenever their color scheme changes. Works over
7
+ SSH and on any OS; terminals without it ignore the request.
8
+ - The desktop's own setting (macOS, or GNOME through `gsettings`), polled.
9
+ A change only prompts a fresh OSC 11 background query: what matters is the
10
+ terminal's background, and a terminal pinned to dark stays dark when the
11
+ desktop goes light. Skipped over SSH, where the desktop is someone else's.
12
+
13
+ Polling is a cheap in-process read on macOS, so the slower GNOME path is the
14
+ only one that spawns anything.
15
+ """
16
+
17
+ import asyncio
18
+ import os
19
+ import shutil
20
+ import subprocess
21
+ import sys
22
+ from collections.abc import Callable
23
+ from contextlib import contextmanager
24
+
25
+ from pcode.terminal_notify import _isatty, _write_all
26
+
27
+ POLL_SECONDS = 2.0
28
+ # The desktop setting can flip before the terminal repaints; ask again later
29
+ # too. Repeated identical answers change nothing.
30
+ QUERY_DELAYS = (0.3, 1.5)
31
+ ENABLE_REPORTS = b"\x1b[?2031h"
32
+ DISABLE_REPORTS = b"\x1b[?2031l"
33
+ QUERY_BACKGROUND = b"\x1b]11;?\x1b\\"
34
+
35
+
36
+ def _macos_reader():
37
+ """A function reading the global AppleInterfaceStyle, or None without CoreFoundation."""
38
+ import ctypes
39
+ import ctypes.util
40
+
41
+ path = ctypes.util.find_library("CoreFoundation")
42
+ if path is None:
43
+ return None
44
+ cf = ctypes.CDLL(path)
45
+ utf8 = 0x08000100
46
+ cf.CFStringCreateWithCString.restype = ctypes.c_void_p
47
+ cf.CFStringCreateWithCString.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_uint32]
48
+ cf.CFPreferencesCopyAppValue.restype = ctypes.c_void_p
49
+ cf.CFPreferencesCopyAppValue.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
50
+ cf.CFPreferencesAppSynchronize.argtypes = [ctypes.c_void_p]
51
+ cf.CFStringGetCString.restype = ctypes.c_bool
52
+ cf.CFGetTypeID.restype = ctypes.c_ulong
53
+ cf.CFGetTypeID.argtypes = [ctypes.c_void_p]
54
+ cf.CFStringGetTypeID.restype = ctypes.c_ulong
55
+ string_type = cf.CFStringGetTypeID()
56
+ cf.CFStringGetCString.argtypes = [
57
+ ctypes.c_void_p,
58
+ ctypes.c_char_p,
59
+ ctypes.c_long,
60
+ ctypes.c_uint32,
61
+ ]
62
+ cf.CFRelease.argtypes = [ctypes.c_void_p]
63
+ key = cf.CFStringCreateWithCString(None, b"AppleInterfaceStyle", utf8)
64
+ domain = ctypes.c_void_p.in_dll(cf, "kCFPreferencesAnyApplication")
65
+
66
+ def read() -> str:
67
+ # Without the sync the process keeps its first cached answer.
68
+ cf.CFPreferencesAppSynchronize(domain)
69
+ value = cf.CFPreferencesCopyAppValue(key, domain)
70
+ if not value: # The key only exists while dark.
71
+ return "light"
72
+ buffer = ctypes.create_string_buffer(16)
73
+ # Any property list type can sit under the key; only a string is ours.
74
+ ok = cf.CFGetTypeID(value) == string_type and cf.CFStringGetCString(
75
+ value, buffer, len(buffer), utf8
76
+ )
77
+ cf.CFRelease(value)
78
+ return "dark" if ok and buffer.value == b"Dark" else "light"
79
+
80
+ return read
81
+
82
+
83
+ def _gnome_read() -> str | None:
84
+ try:
85
+ result = subprocess.run(
86
+ ["gsettings", "get", "org.gnome.desktop.interface", "color-scheme"],
87
+ capture_output=True,
88
+ text=True,
89
+ timeout=2,
90
+ )
91
+ except (OSError, subprocess.SubprocessError):
92
+ return None
93
+ if result.returncode != 0:
94
+ return None
95
+ return "dark" if "dark" in result.stdout else "light"
96
+
97
+
98
+ def desktop_reader():
99
+ """How to read the desktop's light/dark setting here, or None to not poll."""
100
+ if os.environ.get("SSH_CONNECTION") or os.environ.get("SSH_TTY"):
101
+ return None
102
+ try:
103
+ if sys.platform == "darwin":
104
+ return _macos_reader()
105
+ except (OSError, AttributeError, ValueError):
106
+ return None
107
+ if sys.platform.startswith("linux") and shutil.which("gsettings"):
108
+ if os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY"):
109
+ return _gnome_read
110
+ return None
111
+
112
+
113
+ class AppearanceWatch:
114
+ """Ask the terminal for its appearance whenever it may have changed.
115
+
116
+ Answers arrive as input and are handled by the editor's input parser, so
117
+ this only writes requests and never reads.
118
+ """
119
+
120
+ def __init__(
121
+ self, fd: int | None, reader=None, active: Callable[[], bool] = lambda: True
122
+ ) -> None:
123
+ self.fd = fd
124
+ self.reader = reader
125
+ # Desktop polling only matters while the theme is `auto`; mode 2031
126
+ # reports stay on regardless, so switching back to auto starts current.
127
+ self.active = active
128
+ self.enabled = False
129
+ self.suspended = False
130
+
131
+ def close(self) -> None:
132
+ if self.enabled and _write_all(self.fd, DISABLE_REPORTS):
133
+ self.enabled = False
134
+
135
+ @contextmanager
136
+ def paused(self):
137
+ """Hand the terminal to another program: no reports or replies land in its input."""
138
+ enabled = self.enabled
139
+ self.close()
140
+ self.suspended = True
141
+ try:
142
+ yield
143
+ finally:
144
+ self.suspended = False
145
+ if enabled:
146
+ self.enabled = _write_all(self.fd, ENABLE_REPORTS)
147
+
148
+ async def query(self) -> None:
149
+ for delay in QUERY_DELAYS:
150
+ await asyncio.sleep(delay)
151
+ if not self.suspended:
152
+ _write_all(self.fd, QUERY_BACKGROUND)
153
+
154
+ async def run(self) -> None:
155
+ if self.fd is None or not _isatty(self.fd) or os.environ.get("TERM") == "dumb":
156
+ return
157
+ self.enabled = _write_all(self.fd, ENABLE_REPORTS)
158
+ reader = self.reader if self.reader is not None else desktop_reader()
159
+ pending = None
160
+ try:
161
+ if reader is None:
162
+ await asyncio.Future() # Mode 2031 only; disable it on the way out.
163
+ last = await asyncio.to_thread(reader)
164
+ while True:
165
+ await asyncio.sleep(POLL_SECONDS)
166
+ if not self.active():
167
+ # Keep `last`: a flip made meanwhile is still asked about
168
+ # on the first poll after switching back to auto.
169
+ continue
170
+ current = await asyncio.to_thread(reader)
171
+ if current is None:
172
+ continue
173
+ if last is not None and current != last:
174
+ if pending is not None:
175
+ pending.cancel()
176
+ pending = asyncio.create_task(self.query())
177
+ last = current
178
+ finally:
179
+ if pending is not None:
180
+ pending.cancel()
181
+ self.close()