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/prompt_keys.py ADDED
@@ -0,0 +1,286 @@
1
+ """The main prompt's key bindings: its shortcuts and its editing keys.
2
+
3
+ Registration order is behavior here. prompt_toolkit runs the last of several
4
+ bindings that match the same keys, so a later ``c-d`` outranks an earlier one
5
+ under the same filter, and the shortcuts list in the order they are added.
6
+ """
7
+
8
+ from collections.abc import Callable
9
+ from dataclasses import dataclass
10
+
11
+ from prompt_toolkit.application import get_app
12
+ from prompt_toolkit.buffer import Buffer
13
+ from prompt_toolkit.document import Document
14
+ from prompt_toolkit.filters import Condition, vi_insert_mode, vi_mode
15
+ from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
16
+ from prompt_toolkit.key_binding.vi_state import InputMode
17
+ from prompt_toolkit.keys import Keys
18
+
19
+ from pcode.clipboard import copy as copy_to_clipboard
20
+ from pcode.paste import PastedText
21
+ from pcode.preferences import SETTINGS, load_preferences
22
+ from pcode.prefix_keys import Choice, PrefixKeys
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class PromptCallbacks:
27
+ """What the prompt asks of the app. Any may be absent; its key then does less."""
28
+
29
+ on_cancel: Callable | None = None
30
+ on_effort: Callable | None = None
31
+ on_model: Callable | None = None
32
+ on_tasks: Callable | None = None
33
+ on_thinking: Callable | None = None
34
+ on_commands: Callable | None = None
35
+ on_send_mode: Callable | None = None
36
+ on_previous_session: Callable | None = None
37
+ on_copy_response: Callable | None = None
38
+
39
+
40
+ def prompt_key_bindings(
41
+ activity,
42
+ transcript,
43
+ callbacks: PromptCallbacks,
44
+ key_prefix: str | None,
45
+ default_buffer: Callable[[], Buffer],
46
+ ) -> tuple[KeyBindings, PrefixKeys]:
47
+ """The editor's own keys and its shortcuts, sharing one paste collapser.
48
+
49
+ ``default_buffer`` is asked for at keypress time, since the session that
50
+ owns the buffer is built from these bindings.
51
+ """
52
+ pasted = PastedText()
53
+ # The prompt and popups share one contextual keybinding overlay.
54
+ shortcuts = PrefixKeys(
55
+ key_prefix,
56
+ vi_prefix=load_preferences().get("vi_key_prefix", SETTINGS["vi_key_prefix"].default),
57
+ )
58
+ _add_shortcuts(shortcuts, activity, transcript, callbacks, pasted)
59
+ shortcuts.set_help(
60
+ lambda: [
61
+ ("Enter", "Send"),
62
+ ("Ctrl+J", "Newline"),
63
+ ("Tab", "Complete"),
64
+ ("↑ / ↓", "Move / history"),
65
+ ("Ctrl+C", "Interrupt" if activity.busy else "Clear input"),
66
+ *([] if activity.busy else [("Ctrl+D", "Exit (empty prompt)")]),
67
+ ],
68
+ title="Keybindings · Prompt",
69
+ )
70
+ keys = _editing_keys(activity, transcript, callbacks, pasted, default_buffer)
71
+ return keys, shortcuts
72
+
73
+
74
+ def _add_shortcuts(
75
+ shortcuts: PrefixKeys, activity, transcript, callbacks: PromptCallbacks, pasted: PastedText
76
+ ) -> None:
77
+ on_send_mode = callbacks.on_send_mode
78
+ on_model = callbacks.on_model
79
+ on_effort = callbacks.on_effort
80
+ on_tasks = callbacks.on_tasks
81
+ on_thinking = callbacks.on_thinking
82
+ on_commands = callbacks.on_commands
83
+ on_previous_session = callbacks.on_previous_session
84
+ on_copy_response = callbacks.on_copy_response
85
+
86
+ @shortcuts.add("s", "Cycle send mode", filter=on_send_mode is not None)
87
+ def cycle_send_mode(event: KeyPressEvent) -> None:
88
+ on_send_mode()
89
+ event.app.invalidate()
90
+
91
+ @shortcuts.add("l", "Select model", filter=on_model is not None)
92
+ def choose_model(event: KeyPressEvent) -> None:
93
+ on_model()
94
+
95
+ effort = "Thinking effort up / down"
96
+
97
+ @shortcuts.add("n", "Increase thinking effort", filter=on_effort is not None, group=effort)
98
+ def increase_effort(event: KeyPressEvent) -> None:
99
+ on_effort(1)
100
+ event.app.invalidate()
101
+
102
+ @shortcuts.add("p", "Decrease thinking effort", filter=on_effort is not None, group=effort)
103
+ def decrease_effort(event: KeyPressEvent) -> None:
104
+ on_effort(-1)
105
+ event.app.invalidate()
106
+
107
+ @shortcuts.add("o", lambda: "Hide task panel" if activity.tasks_shown else "Show task panel")
108
+ def toggle_tasks(event: KeyPressEvent) -> None:
109
+ shown = activity.toggle_tasks()
110
+ if on_tasks is not None:
111
+ on_tasks(shown)
112
+ event.app.invalidate()
113
+
114
+ @shortcuts.add("t", "Select thinking visibility")
115
+ def choose_thinking(event: KeyPressEvent) -> None:
116
+ def select(mode: str):
117
+ def apply(event: KeyPressEvent) -> None:
118
+ if on_thinking is not None:
119
+ on_thinking(mode)
120
+ else:
121
+ activity.thinking_mode = mode
122
+
123
+ return apply
124
+
125
+ shortcuts.choose(
126
+ "Thinking visibility",
127
+ [
128
+ Choice(
129
+ key,
130
+ label + (" (current)" if activity.thinking_mode == mode else ""),
131
+ select(mode),
132
+ )
133
+ for key, mode, label in (
134
+ ("o", "off", "Off"),
135
+ ("s", "status-line", "Status line"),
136
+ ("b", "scrollback", "Scrollback"),
137
+ )
138
+ ],
139
+ )
140
+
141
+ def command_output_label() -> str:
142
+ shown = getattr(transcript, "command_scrollback", None)
143
+ if shown is None:
144
+ return "Toggle command output"
145
+ return "Hide command output" if shown else "Show command output"
146
+
147
+ @shortcuts.add("g", command_output_label, filter=on_commands is not None)
148
+ def toggle_command_scrollback(event: KeyPressEvent) -> None:
149
+ on_commands()
150
+ event.app.invalidate()
151
+
152
+ # Vim's alternate-buffer key; terminals send Ctrl+^ for Ctrl+6 as well.
153
+ @shortcuts.add("^", "Previous session", filter=on_previous_session is not None)
154
+ def previous_session(event: KeyPressEvent) -> None:
155
+ on_previous_session()
156
+
157
+ @shortcuts.add("y", "Copy draft / last response")
158
+ def copy_draft(event: KeyPressEvent) -> None:
159
+ # Collapsed pastes are a display device, so copy what sending would:
160
+ # the expanded text, not the `[pasted …]` marker standing in for it.
161
+ text = pasted.expand(event.current_buffer.text)
162
+ # With nothing typed, there is no draft to copy: copy the last response.
163
+ if not text and on_copy_response is not None:
164
+ on_copy_response()
165
+ return
166
+ if transcript is None:
167
+ return
168
+ if not text:
169
+ transcript.flash("Nothing to copy")
170
+ return
171
+ copied, truncated = copy_to_clipboard(text, event.app.output)
172
+ limit = " (truncated)" if truncated else ""
173
+ transcript.flash(f"Copied prompt{limit}" if copied else "Could not copy prompt")
174
+
175
+
176
+ def _down_would_idle() -> bool:
177
+ # prompt_toolkit's Down moves within the text, walks the completion
178
+ # menu, or steps forward through history; only when none of those
179
+ # apply does it do nothing. Claim just that case for a newline, so ↓
180
+ # never loses its existing meanings. Vi normal mode keeps `j`.
181
+ app = get_app()
182
+ buffer = app.current_buffer
183
+ document = buffer.document
184
+ if buffer.complete_state or document.cursor_position_row < document.line_count - 1:
185
+ return False
186
+ if buffer.working_index < len(buffer._working_lines) - 1:
187
+ return False
188
+ return not vi_mode() or app.vi_state.input_mode == InputMode.INSERT
189
+
190
+
191
+ def _editing_keys(
192
+ activity,
193
+ transcript,
194
+ callbacks: PromptCallbacks,
195
+ pasted: PastedText,
196
+ default_buffer: Callable[[], Buffer],
197
+ ) -> KeyBindings:
198
+ keys = KeyBindings()
199
+
200
+ @keys.add(Keys.BracketedPaste)
201
+ def paste(event: KeyPressEvent) -> None:
202
+ # Same line-ending cleanup as prompt_toolkit's default paste binding,
203
+ # then large pastes collapse to a preview until the prompt is sent.
204
+ data = event.data.replace("\r\n", "\n").replace("\r", "\n")
205
+ event.current_buffer.insert_text(pasted.collapse(data))
206
+
207
+ @keys.add("enter")
208
+ def submit(event: KeyPressEvent) -> None:
209
+ buffer = event.current_buffer
210
+ if buffer.complete_state and buffer.complete_state.current_completion:
211
+ # First Enter accepts the selected completion; next Enter sends it.
212
+ buffer.complete_state = None
213
+ else:
214
+ expanded = pasted.expand(buffer.text)
215
+ if expanded != buffer.text:
216
+ buffer.document = Document(expanded, len(expanded))
217
+ pasted.clear()
218
+ buffer.validate_and_handle()
219
+
220
+ @keys.add("escape", filter=vi_mode, eager=True)
221
+ def normal_mode(event: KeyPressEvent) -> None:
222
+ # Match native vi Escape semantics, without waiting for Alt bindings.
223
+ buffer = event.current_buffer
224
+ state = event.app.vi_state
225
+ if state.input_mode in (InputMode.INSERT, InputMode.REPLACE):
226
+ buffer.cursor_position += buffer.document.get_cursor_left_position()
227
+ state.input_mode = InputMode.NAVIGATION
228
+ if buffer.selection_state:
229
+ buffer.exit_selection()
230
+
231
+ sequence = load_preferences().get("vi_escape_sequence", SETTINGS["vi_escape_sequence"].default)
232
+ if sequence != "escape":
233
+ # Native sequence buffering preserves unmatched/expired characters. Its
234
+ # timeout is separate from terminal Escape decoding; paste bypasses it.
235
+ keys.add(
236
+ *sequence,
237
+ filter=vi_insert_mode & Condition(lambda: not get_app().quoted_insert),
238
+ eager=True,
239
+ )(normal_mode)
240
+
241
+ @keys.add("c-j")
242
+ @keys.add("escape", "enter", filter=~vi_mode)
243
+ def newline(event: KeyPressEvent) -> None:
244
+ event.current_buffer.insert_text("\n")
245
+
246
+ @keys.add("down", filter=Condition(_down_would_idle))
247
+ def newline_on_down(event: KeyPressEvent) -> None:
248
+ event.current_buffer.insert_text("\n")
249
+
250
+ @keys.add("c-d", filter=Condition(lambda: activity.busy))
251
+ def cancel(event: KeyPressEvent) -> None:
252
+ event.app.exit(exception=KeyboardInterrupt)
253
+
254
+ if transcript is None:
255
+ return keys
256
+
257
+ on_cancel = callbacks.on_cancel
258
+
259
+ @keys.add("c-d", filter=Condition(lambda: activity.busy))
260
+ def interrupt_turn(event):
261
+ on_cancel()
262
+
263
+ @keys.add("c-c")
264
+ def interrupt(event):
265
+ # Never discard a draft and interrupt the turn in one keypress: clear
266
+ # the editor first, so interrupting a busy turn needs an empty prompt.
267
+ buffer = default_buffer()
268
+ if activity.busy and not buffer.text:
269
+ on_cancel()
270
+ return
271
+ buffer.reset()
272
+ pasted.clear()
273
+ transcript.note(
274
+ "Input discarded. Ctrl+C again interrupts."
275
+ if activity.busy
276
+ else "Input discarded. Ctrl+D on an empty prompt exits."
277
+ )
278
+
279
+ @keys.add("c-d", filter=Condition(lambda: not activity.busy))
280
+ def exit_or_delete(event):
281
+ if not event.current_buffer.text:
282
+ event.app.exit()
283
+ else:
284
+ event.current_buffer.delete()
285
+
286
+ return keys