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/links_ui.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Temporary link chooser for URLs seen in the conversation."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
|
|
5
|
+
from prompt_toolkit.application import Application
|
|
6
|
+
from prompt_toolkit.document import Document
|
|
7
|
+
from prompt_toolkit.filters import Always, has_focus
|
|
8
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
9
|
+
from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous
|
|
10
|
+
from prompt_toolkit.layout import Layout
|
|
11
|
+
from prompt_toolkit.layout.containers import HSplit
|
|
12
|
+
from prompt_toolkit.widgets import Label, TextArea
|
|
13
|
+
|
|
14
|
+
from pcode.frame import Dialog
|
|
15
|
+
from pcode.links import Link
|
|
16
|
+
from pcode.popup_ui import (
|
|
17
|
+
bind_list_paging,
|
|
18
|
+
fit_width,
|
|
19
|
+
popup_container,
|
|
20
|
+
popup_mouse,
|
|
21
|
+
popup_style,
|
|
22
|
+
steer_list_from_query,
|
|
23
|
+
)
|
|
24
|
+
from pcode.prefix_keys import PrefixKeys
|
|
25
|
+
|
|
26
|
+
_ROW_WIDTH = 100
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def link_rows(links: list[Link]) -> list[tuple[str, str]]:
|
|
30
|
+
rows = []
|
|
31
|
+
for link in links:
|
|
32
|
+
prefix = f"{link.source}: " if link.source else ""
|
|
33
|
+
text = f"{link.label} — {link.url}" if link.label else link.url
|
|
34
|
+
text = text.replace("\n", " ↵ ").replace("\r", " ")
|
|
35
|
+
if len(text) > _ROW_WIDTH:
|
|
36
|
+
text = text[: _ROW_WIDTH - 1] + "…"
|
|
37
|
+
rows.append((link.url, prefix + text))
|
|
38
|
+
return rows
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class LinkPicker:
|
|
42
|
+
"""A search line over the links, newest first; Enter picks, Esc clears then backs out.
|
|
43
|
+
|
|
44
|
+
``container`` and ``key_bindings`` let a popup host it as an overlay, which
|
|
45
|
+
the side-answer viewer does; ``links_dialog`` wraps it in an application of
|
|
46
|
+
its own and adds the tool-link toggle.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
links: list[Link],
|
|
52
|
+
on_pick: Callable[[str], None],
|
|
53
|
+
on_cancel: Callable[[], None],
|
|
54
|
+
*,
|
|
55
|
+
message_links: list[Link] | None = None,
|
|
56
|
+
toggles_tools: bool = False,
|
|
57
|
+
shortcuts: PrefixKeys | None = None,
|
|
58
|
+
footer: tuple = (),
|
|
59
|
+
) -> None:
|
|
60
|
+
self.links = links
|
|
61
|
+
# Collect message-only links separately: a URL first seen in a tool can also
|
|
62
|
+
# appear in a reply, and must survive hiding tools despite URL deduplication.
|
|
63
|
+
if message_links is None:
|
|
64
|
+
message_links = [link for link in links if link.source in ("", "user", "assistant")]
|
|
65
|
+
self.message_links = message_links
|
|
66
|
+
self.toggles_tools = toggles_tools
|
|
67
|
+
self.show_tools = True
|
|
68
|
+
self.visible: list[Link] = []
|
|
69
|
+
self.query = TextArea(height=1, prompt="Search: ", multiline=False)
|
|
70
|
+
self.list = TextArea(
|
|
71
|
+
read_only=True,
|
|
72
|
+
wrap_lines=False,
|
|
73
|
+
scrollbar=True,
|
|
74
|
+
# Fit every row, not just the filtered ones, so searching does not resize the box.
|
|
75
|
+
width=fit_width([text for _, text in link_rows(links)]),
|
|
76
|
+
)
|
|
77
|
+
self.list.window.cursorline = Always()
|
|
78
|
+
keys = self.key_bindings = KeyBindings()
|
|
79
|
+
self.query.buffer.on_text_changed += lambda _: self.refresh()
|
|
80
|
+
steer_list_from_query(keys, self.query, self.list)
|
|
81
|
+
bind_list_paging(keys, self.list, has_focus(self.list) | has_focus(self.query))
|
|
82
|
+
# Modal, so the bindings of whatever frames it cannot reach here.
|
|
83
|
+
keys.add("tab")(focus_next)
|
|
84
|
+
keys.add("s-tab")(focus_previous)
|
|
85
|
+
|
|
86
|
+
# The picker opens in the search line, so Enter picks from there too.
|
|
87
|
+
@keys.add("enter", eager=True)
|
|
88
|
+
def pick(event):
|
|
89
|
+
url = self.selected()
|
|
90
|
+
if url is not None:
|
|
91
|
+
on_pick(url)
|
|
92
|
+
|
|
93
|
+
@keys.add("escape", eager=True)
|
|
94
|
+
def escape(event):
|
|
95
|
+
if self.query.text and event.app.layout.has_focus(self.query):
|
|
96
|
+
self.query.text = ""
|
|
97
|
+
else:
|
|
98
|
+
on_cancel()
|
|
99
|
+
|
|
100
|
+
@keys.add("c-c")
|
|
101
|
+
def cancel(event):
|
|
102
|
+
on_cancel()
|
|
103
|
+
|
|
104
|
+
self.container = HSplit(
|
|
105
|
+
[
|
|
106
|
+
Label(self.summary, dont_extend_height=True),
|
|
107
|
+
self.query,
|
|
108
|
+
self.list,
|
|
109
|
+
*footer,
|
|
110
|
+
],
|
|
111
|
+
padding=1,
|
|
112
|
+
# Gated, so a waiting leader still owns Enter and Esc.
|
|
113
|
+
key_bindings=shortcuts.gate(keys) if shortcuts else keys,
|
|
114
|
+
modal=True,
|
|
115
|
+
)
|
|
116
|
+
self.refresh()
|
|
117
|
+
|
|
118
|
+
def help(self) -> list[tuple[str, str]]:
|
|
119
|
+
return [
|
|
120
|
+
("Type", "Search in the search field"),
|
|
121
|
+
("↑/↓", "Select link"),
|
|
122
|
+
("PgUp/PgDn", "Page"),
|
|
123
|
+
("Ctrl+U/D", "Half page"),
|
|
124
|
+
("Enter", "Open selected link"),
|
|
125
|
+
("Tab/Shift+Tab", "Switch search / list"),
|
|
126
|
+
("Esc", "Clear search when focused, otherwise cancel"),
|
|
127
|
+
("Ctrl+C", "Cancel"),
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
def summary(self) -> str:
|
|
131
|
+
tools = f" · Tools: {'shown' if self.show_tools else 'hidden'}"
|
|
132
|
+
return f"{len(self.visible)} links" + (tools if self.toggles_tools else "")
|
|
133
|
+
|
|
134
|
+
def selected(self) -> str | None:
|
|
135
|
+
row = self.list.document.cursor_position_row
|
|
136
|
+
return self.visible[row].url if row < len(self.visible) else None
|
|
137
|
+
|
|
138
|
+
def toggle_tools(self) -> None:
|
|
139
|
+
self.show_tools = not self.show_tools
|
|
140
|
+
self.refresh()
|
|
141
|
+
|
|
142
|
+
def refresh(self) -> None:
|
|
143
|
+
previous = self.selected()
|
|
144
|
+
term = self.query.text.casefold()
|
|
145
|
+
self.visible[:] = [
|
|
146
|
+
link
|
|
147
|
+
for link in reversed(self.links if self.show_tools else self.message_links)
|
|
148
|
+
if term in f"{link.url} {link.label} {link.source}".casefold()
|
|
149
|
+
]
|
|
150
|
+
selected = next((i for i, link in enumerate(self.visible) if link.url == previous), 0)
|
|
151
|
+
lines = [text for _, text in link_rows(self.visible)]
|
|
152
|
+
position = sum(len(line) + 1 for line in lines[:selected])
|
|
153
|
+
self.list.buffer.set_document(
|
|
154
|
+
Document("\n".join(lines) or "No matching links.", position), bypass_readonly=True
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def links_dialog(
|
|
159
|
+
links: list[Link],
|
|
160
|
+
*,
|
|
161
|
+
message_links: list[Link] | None = None,
|
|
162
|
+
input=None,
|
|
163
|
+
output=None,
|
|
164
|
+
style=None,
|
|
165
|
+
key_prefix: str | None = None,
|
|
166
|
+
):
|
|
167
|
+
"""A popup application whose result is the picked URL, or None."""
|
|
168
|
+
app: Application
|
|
169
|
+
shortcuts = PrefixKeys(key_prefix)
|
|
170
|
+
picker = LinkPicker(
|
|
171
|
+
links,
|
|
172
|
+
on_pick=lambda url: app.exit(result=url),
|
|
173
|
+
on_cancel=lambda: app.exit(result=None),
|
|
174
|
+
message_links=message_links,
|
|
175
|
+
toggles_tools=True,
|
|
176
|
+
shortcuts=shortcuts,
|
|
177
|
+
footer=(Label(shortcuts.summary),),
|
|
178
|
+
)
|
|
179
|
+
shortcuts.set_help(picker.help)
|
|
180
|
+
|
|
181
|
+
@shortcuts.add("f", "Search")
|
|
182
|
+
def search(event):
|
|
183
|
+
event.app.layout.focus(picker.query)
|
|
184
|
+
|
|
185
|
+
@shortcuts.add("t", "Show/hide tool links")
|
|
186
|
+
def toggle_tools(event):
|
|
187
|
+
picker.toggle_tools()
|
|
188
|
+
|
|
189
|
+
dialog = Dialog(title="Links", body=picker.container)
|
|
190
|
+
app = Application(
|
|
191
|
+
# Open in the search line, so typing filters straight away.
|
|
192
|
+
layout=Layout(popup_container(dialog, shortcuts), focused_element=picker.query),
|
|
193
|
+
key_bindings=shortcuts.key_bindings(KeyBindings()),
|
|
194
|
+
full_screen=True,
|
|
195
|
+
mouse_support=popup_mouse(shortcuts),
|
|
196
|
+
input=input,
|
|
197
|
+
output=output,
|
|
198
|
+
style=popup_style(style),
|
|
199
|
+
)
|
|
200
|
+
return app
|