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.
- pcode/__init__.py +7 -0
- pcode/_job_supervisor.py +65 -0
- pcode/agent.py +592 -0
- pcode/anthropic_oauth.py +469 -0
- pcode/app.py +3165 -0
- pcode/appearance.py +181 -0
- pcode/aside.py +541 -0
- pcode/aside_guard.py +39 -0
- pcode/aside_ui.py +683 -0
- pcode/auth.py +113 -0
- pcode/block.py +39 -0
- pcode/browser.py +393 -0
- pcode/cache_diagnostics.py +336 -0
- pcode/cache_settings.py +58 -0
- pcode/cache_warnings.py +139 -0
- pcode/claude_sdk/__init__.py +96 -0
- pcode/claude_sdk/cli.py +52 -0
- pcode/claude_sdk/errors.py +61 -0
- pcode/claude_sdk/messages.py +154 -0
- pcode/claude_sdk/model.py +179 -0
- pcode/claude_sdk/resume.py +114 -0
- pcode/claude_sdk/session.py +547 -0
- pcode/claude_sdk/session_pool.py +251 -0
- pcode/claude_sdk/workspace.py +34 -0
- pcode/cli.py +73 -0
- pcode/clipboard.py +76 -0
- pcode/code_mode.py +43 -0
- pcode/codex_login.py +189 -0
- pcode/command_transcript.py +71 -0
- pcode/commands.py +145 -0
- pcode/compaction.py +457 -0
- pcode/completion.py +263 -0
- pcode/config.py +223 -0
- pcode/context_breakdown.py +178 -0
- pcode/context_usage.py +52 -0
- pcode/controller.py +3496 -0
- pcode/conversation_ids.py +27 -0
- pcode/conversation_tree.py +160 -0
- pcode/copy_ui.py +172 -0
- pcode/delegation.py +236 -0
- pcode/delta.py +295 -0
- pcode/diagnostics.py +226 -0
- pcode/edit_preview.py +158 -0
- pcode/edit_transcript.py +287 -0
- pcode/edit_ui.py +850 -0
- pcode/edits.py +162 -0
- pcode/email_remote/__init__.py +14 -0
- pcode/email_remote/cli.py +133 -0
- pcode/email_remote/listener.py +695 -0
- pcode/email_remote/mailbox.py +336 -0
- pcode/email_remote/outbound.py +75 -0
- pcode/email_remote/parsing.py +226 -0
- pcode/email_remote/render.py +175 -0
- pcode/email_remote/state.py +79 -0
- pcode/error_report.py +38 -0
- pcode/ext.py +602 -0
- pcode/extension_guide.md +311 -0
- pcode/extensions/browser.py +284 -0
- pcode/extensions/ponytail.py +217 -0
- pcode/extensions/sandbox.py +138 -0
- pcode/extensions/session_history.py +170 -0
- pcode/extensions/web_research.py +89 -0
- pcode/file_refs.py +265 -0
- pcode/filesystem.py +138 -0
- pcode/frame.py +149 -0
- pcode/gateway.py +465 -0
- pcode/git_diff.py +497 -0
- pcode/history.py +589 -0
- pcode/history_cursor.py +147 -0
- pcode/history_embeddings.py +102 -0
- pcode/host.py +941 -0
- pcode/host_protocol.py +291 -0
- pcode/host_ui.py +225 -0
- pcode/input_keys.py +35 -0
- pcode/inspection.py +289 -0
- pcode/inspector_ui.py +448 -0
- pcode/isolated_delegation.py +425 -0
- pcode/job_notices.py +54 -0
- pcode/jobs.py +584 -0
- pcode/jobs_ui.py +265 -0
- pcode/keymap.py +215 -0
- pcode/layout_speed.py +103 -0
- pcode/links.py +90 -0
- pcode/links_ui.py +200 -0
- pcode/live.py +1301 -0
- pcode/llm_proxy.py +44 -0
- pcode/logins.py +233 -0
- pcode/mcp.py +683 -0
- pcode/mcp_notice.py +91 -0
- pcode/mcp_oauth.py +381 -0
- pcode/meridian.py +233 -0
- pcode/meridian_process.py +337 -0
- pcode/meridian_reminders.py +107 -0
- pcode/meridian_setup.py +356 -0
- pcode/model_metadata.py +451 -0
- pcode/model_ui.py +168 -0
- pcode/models.py +209 -0
- pcode/native_results.py +119 -0
- pcode/oauth_pages.py +66 -0
- pcode/output_limits.py +41 -0
- pcode/paste.py +45 -0
- pcode/path_completion.py +66 -0
- pcode/plan_preview.py +131 -0
- pcode/plan_sizes.py +35 -0
- pcode/planning.py +161 -0
- pcode/popup_ui.py +873 -0
- pcode/preferences.py +1176 -0
- pcode/prefix_keys.py +490 -0
- pcode/profile_benchmark.py +332 -0
- pcode/profiling.py +566 -0
- pcode/project_trust.py +94 -0
- pcode/prompt_keys.py +286 -0
- pcode/remote.py +717 -0
- pcode/remote_print.py +125 -0
- pcode/remote_profile.py +237 -0
- pcode/replay_benchmark.py +319 -0
- pcode/repo_context.py +158 -0
- pcode/retries.py +28 -0
- pcode/rpc.py +280 -0
- pcode/runtime.py +244 -0
- pcode/sandbox.py +454 -0
- pcode/session_naming.py +82 -0
- pcode/session_ui.py +550 -0
- pcode/sessions.py +892 -0
- pcode/shell.py +113 -0
- pcode/shell_mode.py +172 -0
- pcode/shell_tools.py +459 -0
- pcode/skills.py +161 -0
- pcode/stall_log.py +187 -0
- pcode/startup_input.py +96 -0
- pcode/steering.py +27 -0
- pcode/stream_display.py +188 -0
- pcode/stream_events.py +437 -0
- pcode/strict_tools.py +130 -0
- pcode/syntax.py +43 -0
- pcode/syntax_colors.py +206 -0
- pcode/task_prompt.py +28 -0
- pcode/task_worktrees.py +482 -0
- pcode/terminal_notify.py +383 -0
- pcode/theme.py +135 -0
- pcode/theme_gallery.py +88 -0
- pcode/thinking_markdown.py +41 -0
- pcode/token_accounting.py +67 -0
- pcode/tool_display.py +784 -0
- pcode/tool_output_limits.py +108 -0
- pcode/tool_panel.py +377 -0
- pcode/transcript_log.py +127 -0
- pcode/transcript_notice.py +103 -0
- pcode/tree_ui.py +288 -0
- pcode/turn.py +44 -0
- pcode/ui.py +3745 -0
- pcode/usage.py +293 -0
- pcode/word_wrap.py +107 -0
- pcode/worker_ui.py +235 -0
- pcode/workers.py +136 -0
- pcode/workspace.py +57 -0
- pcode/workspace_filesystem.py +72 -0
- pcode/worktree.py +650 -0
- pcode-0.1.0.dist-info/METADATA +145 -0
- pcode-0.1.0.dist-info/RECORD +163 -0
- pcode-0.1.0.dist-info/WHEEL +4 -0
- pcode-0.1.0.dist-info/entry_points.txt +3 -0
- 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
|