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/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