pcode 0.1.2__tar.gz → 0.1.4__tar.gz

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 (173) hide show
  1. {pcode-0.1.2 → pcode-0.1.4}/PKG-INFO +9 -10
  2. {pcode-0.1.2 → pcode-0.1.4}/README.md +3 -3
  3. {pcode-0.1.2 → pcode-0.1.4}/pyproject.toml +5 -6
  4. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/agent.py +7 -2
  5. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/anthropic_oauth.py +1 -1
  6. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/app.py +55 -38
  7. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/appearance.py +2 -2
  8. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/aside_ui.py +18 -27
  9. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/auth.py +1 -1
  10. pcode-0.1.4/src/pcode/background_delegation.py +318 -0
  11. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/browser.py +1 -1
  12. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/cache_diagnostics.py +3 -3
  13. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/cache_warnings.py +18 -25
  14. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/claude_sdk/errors.py +2 -1
  15. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/claude_sdk/messages.py +22 -0
  16. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/claude_sdk/model.py +10 -6
  17. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/claude_sdk/resume.py +1 -1
  18. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/claude_sdk/session.py +135 -9
  19. pcode-0.1.4/src/pcode/claude_sdk/web.py +85 -0
  20. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/cli.py +2 -2
  21. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/clipboard.py +1 -1
  22. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/config.py +27 -3
  23. pcode-0.1.4/src/pcode/config_ui.py +452 -0
  24. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/controller.py +10 -3
  25. pcode-0.1.4/src/pcode/copy_ui.py +394 -0
  26. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/delegation.py +1 -1
  27. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/delta.py +1 -1
  28. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/diagnostics.py +15 -4
  29. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/edit_preview.py +1 -1
  30. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/email_remote/mailbox.py +2 -2
  31. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/email_remote/parsing.py +1 -1
  32. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/ext.py +6 -4
  33. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/extension_guide.md +3 -0
  34. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/extensions/browser.py +1 -1
  35. pcode-0.1.4/src/pcode/extensions/plan_mode.py +152 -0
  36. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/extensions/ponytail.py +2 -2
  37. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/extensions/web_research.py +11 -7
  38. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/file_refs.py +2 -2
  39. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/host.py +2 -1
  40. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/host_protocol.py +2 -2
  41. pcode-0.1.4/src/pcode/input_keys.py +272 -0
  42. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/inspection.py +1 -1
  43. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/inspector_ui.py +1 -1
  44. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/jobs.py +5 -5
  45. pcode-0.1.4/src/pcode/keyboard_protocol.py +104 -0
  46. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/keymap.py +46 -15
  47. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/live.py +53 -2
  48. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/llm_proxy.py +1 -1
  49. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/mcp.py +3 -3
  50. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/mcp_oauth.py +1 -1
  51. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/meridian.py +2 -2
  52. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/meridian_process.py +5 -5
  53. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/meridian_setup.py +6 -6
  54. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/model_metadata.py +2 -2
  55. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/plan_preview.py +1 -1
  56. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/preferences.py +17 -5
  57. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/prefix_keys.py +37 -6
  58. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/proctitle.py +1 -1
  59. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/profiling.py +1 -1
  60. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/project_trust.py +1 -1
  61. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/prompt_keys.py +21 -4
  62. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/remote.py +4 -4
  63. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/replay_benchmark.py +1 -1
  64. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/repo_context.py +1 -1
  65. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/sessions.py +4 -4
  66. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/stall_log.py +52 -4
  67. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/steering.py +11 -6
  68. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/stream_events.py +54 -2
  69. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/task_worktrees.py +1 -1
  70. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/terminal_notify.py +1 -1
  71. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/theme.py +1 -1
  72. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/tool_display.py +7 -6
  73. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/turn.py +2 -0
  74. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/ui.py +50 -43
  75. pcode-0.1.4/src/pcode/update_check.py +156 -0
  76. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/usage.py +2 -2
  77. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/worktree.py +1 -1
  78. pcode-0.1.2/src/pcode/copy_ui.py +0 -172
  79. pcode-0.1.2/src/pcode/input_keys.py +0 -35
  80. {pcode-0.1.2 → pcode-0.1.4}/.gitignore +0 -0
  81. {pcode-0.1.2 → pcode-0.1.4}/LICENSE +0 -0
  82. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/__init__.py +0 -0
  83. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/_job_supervisor.py +0 -0
  84. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/aside.py +0 -0
  85. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/aside_guard.py +0 -0
  86. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/block.py +0 -0
  87. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/cache_settings.py +0 -0
  88. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/claude_sdk/__init__.py +0 -0
  89. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/claude_sdk/cli.py +0 -0
  90. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/claude_sdk/session_pool.py +0 -0
  91. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/claude_sdk/workspace.py +0 -0
  92. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/code_mode.py +0 -0
  93. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/codex_login.py +0 -0
  94. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/command_transcript.py +0 -0
  95. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/commands.py +0 -0
  96. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/compaction.py +0 -0
  97. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/completion.py +0 -0
  98. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/context_breakdown.py +0 -0
  99. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/context_usage.py +0 -0
  100. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/conversation_ids.py +0 -0
  101. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/conversation_tree.py +0 -0
  102. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/edit_transcript.py +0 -0
  103. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/edit_ui.py +0 -0
  104. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/edits.py +0 -0
  105. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/email_remote/__init__.py +0 -0
  106. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/email_remote/cli.py +0 -0
  107. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/email_remote/listener.py +0 -0
  108. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/email_remote/outbound.py +0 -0
  109. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/email_remote/render.py +0 -0
  110. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/email_remote/state.py +0 -0
  111. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/error_report.py +0 -0
  112. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/extensions/sandbox.py +0 -0
  113. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/extensions/session_history.py +0 -0
  114. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/filesystem.py +0 -0
  115. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/frame.py +0 -0
  116. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/gateway.py +0 -0
  117. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/git_diff.py +0 -0
  118. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/history.py +0 -0
  119. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/history_cursor.py +0 -0
  120. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/history_embeddings.py +0 -0
  121. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/host_ui.py +0 -0
  122. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/isolated_delegation.py +0 -0
  123. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/job_notices.py +0 -0
  124. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/jobs_ui.py +0 -0
  125. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/layout_speed.py +0 -0
  126. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/links.py +0 -0
  127. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/links_ui.py +0 -0
  128. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/logins.py +0 -0
  129. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/mcp_notice.py +0 -0
  130. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/meridian_reminders.py +0 -0
  131. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/model_ui.py +0 -0
  132. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/models.py +0 -0
  133. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/native_results.py +0 -0
  134. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/oauth_pages.py +0 -0
  135. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/output_limits.py +0 -0
  136. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/paste.py +0 -0
  137. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/path_completion.py +0 -0
  138. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/plan_sizes.py +0 -0
  139. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/planning.py +0 -0
  140. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/popup_ui.py +0 -0
  141. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/profile_benchmark.py +0 -0
  142. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/remote_print.py +0 -0
  143. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/remote_profile.py +0 -0
  144. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/retries.py +0 -0
  145. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/rpc.py +0 -0
  146. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/runtime.py +0 -0
  147. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/sandbox.py +0 -0
  148. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/session_naming.py +0 -0
  149. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/session_ui.py +0 -0
  150. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/shell.py +0 -0
  151. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/shell_mode.py +0 -0
  152. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/shell_tools.py +0 -0
  153. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/skills.py +0 -0
  154. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/startup_input.py +0 -0
  155. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/stream_display.py +0 -0
  156. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/strict_tools.py +0 -0
  157. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/syntax.py +0 -0
  158. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/syntax_colors.py +0 -0
  159. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/task_prompt.py +0 -0
  160. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/terminal_text.py +0 -0
  161. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/theme_gallery.py +0 -0
  162. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/thinking_markdown.py +0 -0
  163. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/token_accounting.py +0 -0
  164. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/tool_output_limits.py +0 -0
  165. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/tool_panel.py +0 -0
  166. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/transcript_log.py +0 -0
  167. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/transcript_notice.py +0 -0
  168. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/tree_ui.py +0 -0
  169. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/word_wrap.py +0 -0
  170. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/worker_ui.py +0 -0
  171. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/workers.py +0 -0
  172. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/workspace.py +0 -0
  173. {pcode-0.1.2 → pcode-0.1.4}/src/pcode/workspace_filesystem.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pcode
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Terminal-native AI coding agent with background jobs, parallel worktrees, forkable sessions, and multi-model support
5
5
  Project-URL: Homepage, https://github.com/cruxwell/pcode
6
6
  Project-URL: Documentation, https://cruxwell.github.io/pcode/
@@ -16,18 +16,17 @@ Classifier: Intended Audience :: Developers
16
16
  Classifier: Operating System :: MacOS
17
17
  Classifier: Operating System :: POSIX :: Linux
18
18
  Classifier: Programming Language :: Python :: 3
19
- Classifier: Programming Language :: Python :: 3.11
20
- Classifier: Programming Language :: Python :: 3.12
21
- Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Programming Language :: Python :: 3.15
22
21
  Classifier: Topic :: Software Development
23
- Requires-Python: >=3.11
22
+ Requires-Python: >=3.14
24
23
  Requires-Dist: filelock<5,>=4
25
24
  Requires-Dist: genai-prices<0.2,>=0.1.7
26
25
  Requires-Dist: httpx2<3,>=2.13.1
27
26
  Requires-Dist: prompt-toolkit==3.0.53
28
27
  Requires-Dist: psutil<8,>=6
29
- Requires-Dist: pydantic-ai-harness[codemode,coder,exa,playwright]<0.55,>=0.54.0
30
- Requires-Dist: pydantic-ai-slim[anthropic,bedrock,duckduckgo,google,groq,mcp,openai,web-fetch,xai]<3,>=2.54.0
28
+ Requires-Dist: pydantic-ai-harness[codemode,coder,exa,playwright]<0.56,>=0.55.0
29
+ Requires-Dist: pydantic-ai-slim[anthropic,bedrock,duckduckgo,google,groq,mcp,openai,web-fetch,xai]<3,>=2.55.0
31
30
  Requires-Dist: rich<16,>=15
32
31
  Requires-Dist: websockets<17,>=15.0.1
33
32
  Requires-Dist: yappi<2,>=1.7.6
@@ -35,9 +34,9 @@ Provides-Extra: claude
35
34
  Requires-Dist: claude-agent-sdk<0.3,>=0.2.164; extra == 'claude'
36
35
  Description-Content-Type: text/markdown
37
36
 
38
- <img width="1392" height="1522" alt="pcode in a dark terminal" src="docs/assets/screenshots/pcode-dark.png" />
37
+ <img width="1392" height="1522" alt="pcode in a dark terminal" src="docs/assets/screenshots/tabs-dark.png" />
39
38
 
40
- <img width="1392" height="1522" alt="pcode in a light terminal" src="docs/assets/screenshots/pcode-light.png" />
39
+ <img width="1392" height="1522" alt="pcode in a light terminal" src="docs/assets/screenshots/tabs-light.png" />
41
40
 
42
41
  # pcode
43
42
 
@@ -77,7 +76,7 @@ brew install cruxwell/pcode/pcode
77
76
  Or from [PyPI](https://pypi.org/project/pcode/) with [uv](https://docs.astral.sh/uv/):
78
77
 
79
78
  ```sh
80
- uv tool install 'pcode[claude]'
79
+ uv tool install --python 3.14 'pcode[claude]'
81
80
  ```
82
81
 
83
82
  `brew install --HEAD` tracks `master` instead of the latest release.
@@ -1,6 +1,6 @@
1
- <img width="1392" height="1522" alt="pcode in a dark terminal" src="docs/assets/screenshots/pcode-dark.png" />
1
+ <img width="1392" height="1522" alt="pcode in a dark terminal" src="docs/assets/screenshots/tabs-dark.png" />
2
2
 
3
- <img width="1392" height="1522" alt="pcode in a light terminal" src="docs/assets/screenshots/pcode-light.png" />
3
+ <img width="1392" height="1522" alt="pcode in a light terminal" src="docs/assets/screenshots/tabs-light.png" />
4
4
 
5
5
  # pcode
6
6
 
@@ -40,7 +40,7 @@ brew install cruxwell/pcode/pcode
40
40
  Or from [PyPI](https://pypi.org/project/pcode/) with [uv](https://docs.astral.sh/uv/):
41
41
 
42
42
  ```sh
43
- uv tool install 'pcode[claude]'
43
+ uv tool install --python 3.14 'pcode[claude]'
44
44
  ```
45
45
 
46
46
  `brew install --HEAD` tracks `master` instead of the latest release.
@@ -14,12 +14,11 @@ classifiers = [
14
14
  "Operating System :: MacOS",
15
15
  "Operating System :: POSIX :: Linux",
16
16
  "Programming Language :: Python :: 3",
17
- "Programming Language :: Python :: 3.11",
18
- "Programming Language :: Python :: 3.12",
19
- "Programming Language :: Python :: 3.13",
17
+ "Programming Language :: Python :: 3.14",
18
+ "Programming Language :: Python :: 3.15",
20
19
  "Topic :: Software Development",
21
20
  ]
22
- requires-python = ">=3.11"
21
+ requires-python = ">=3.14"
23
22
  dependencies = [
24
23
  "filelock>=4,<5",
25
24
  "genai-prices>=0.1.7,<0.2",
@@ -34,11 +33,11 @@ dependencies = [
34
33
  # itself is downloaded on first use, not at install.
35
34
  # Harness ships from the pydantic-ai repo with every Pydantic AI release and
36
35
  # pins pydantic-ai-slim to the same minor; bump both together.
37
- "pydantic-ai-harness[exa,coder,codemode,playwright]>=0.54.0,<0.55",
36
+ "pydantic-ai-harness[exa,coder,codemode,playwright]>=0.55.0,<0.56",
38
37
  # duckduckgo and web-fetch back the bundled web_research extension when no
39
38
  # provider-native search or Exa key is available.
40
39
  # google covers both google and google-cloud providers; bedrock pulls boto3.
41
- "pydantic-ai-slim[anthropic,bedrock,duckduckgo,google,groq,mcp,openai,web-fetch,xai]>=2.54.0,<3",
40
+ "pydantic-ai-slim[anthropic,bedrock,duckduckgo,google,groq,mcp,openai,web-fetch,xai]>=2.55.0,<3",
42
41
  "rich>=15,<16",
43
42
  # FastMCP 4.x OAuth callback server selects Uvicorn's websockets-sansio protocol.
44
43
  # Capped below 17 by google-genai (still <17 as of 2.29), not by pcode.
@@ -32,6 +32,7 @@ from pydantic_ai_harness.subagents import ModelOption, SubAgent
32
32
  from pydantic_ai_harness.tool_output_limits import ToolOutputLimits
33
33
 
34
34
  from pcode import remote_profile
35
+ from pcode.background_delegation import BackgroundDelegation
35
36
  from pcode.cache_settings import ProviderCacheSettings, model_settings
36
37
  from pcode.cache_warnings import CacheBustReporting
37
38
  from pcode.claude_sdk import ClaudeWorkspace
@@ -359,8 +360,10 @@ def create_coder(
359
360
  """
360
361
  workspace = workspace.resolve()
361
362
  _ensure_bundled_rg()
362
- # Delegation is pcode's own `WorkspaceSubAgents`, added below.
363
- coder = Coder(sub_agents=False)
363
+ # Delegation is pcode's own `WorkspaceSubAgents`, added below. Caching is
364
+ # `ProviderCacheSettings`' per-provider choice: Coder's default `cache=True`
365
+ # would also reach Meridian and OpenAI's explicit-breakpoint models.
366
+ coder = Coder(sub_agents=False, caching=False)
364
367
  output_limits = create_tool_output_limits()
365
368
  # Keep Coder's tool selection, including its persistent shell (as a
366
369
  # `JobShell`). File display and repository discovery remain local adapters;
@@ -402,6 +405,8 @@ def create_coder(
402
405
  capabilities.append(
403
406
  _delegation(workspace, worker, subagents, extensions, output_limits, cache_notices)
404
407
  )
408
+ # Steering mid-delegation reaches the parent; the child keeps working.
409
+ capabilities.append(BackgroundDelegation())
405
410
  # Summarize evidence before discarding it; pcode owns compaction.
406
411
  return CombinedCapability([c for c in capabilities if not isinstance(c, ClearToolResults)])
407
412
 
@@ -403,7 +403,7 @@ async def _receive_code(port: int, state: str, timeout: float) -> str:
403
403
  + body
404
404
  )
405
405
  await writer.drain()
406
- except (OSError, asyncio.TimeoutError, UnicodeError):
406
+ except OSError, asyncio.TimeoutError, UnicodeError:
407
407
  code = ""
408
408
  finally:
409
409
  writer.close()
@@ -24,8 +24,13 @@ from pcode.cli import ask, restore_stdin
24
24
  from pcode.commands import Command, CommandRegistry
25
25
  from pcode.completion import SHELLS as COMPLETION_SHELLS
26
26
  from pcode.completion import complete_with
27
+ from pcode.config import (
28
+ IMMEDIATE_SETTINGS,
29
+ config_argument_descriptions,
30
+ config_arguments,
31
+ configure,
32
+ )
27
33
  from pcode.config import USAGE as CONFIG_USAGE
28
- from pcode.config import config_argument_descriptions, config_arguments, configure
29
34
  from pcode.controller import (
30
35
  MODEL_COMMANDS,
31
36
  TERMINAL_COMMANDS,
@@ -234,6 +239,7 @@ class PreviewApp:
234
239
  self.reviewed_checkpoint = None
235
240
  self.links_requested = False
236
241
  self.copy_requested = False
242
+ self.config_requested = False
237
243
  # Unsaved conversations have no journal to re-read, so keep their changes.
238
244
  self.edits: list[EditCompleted] = []
239
245
  self.session_requested = False
@@ -260,7 +266,7 @@ class PreviewApp:
260
266
  ),
261
267
  Command(
262
268
  "/config",
263
- "Inspect or edit saved defaults: diff / get KEY / set KEY VALUE / unset KEY",
269
+ "Browse saved settings, or use list / diff / get KEY / set KEY VALUE / unset KEY",
264
270
  self.config,
265
271
  free_arguments=True,
266
272
  argument_provider=config_arguments,
@@ -310,7 +316,7 @@ class PreviewApp:
310
316
  ),
311
317
  Command(
312
318
  "/copy",
313
- "Copy the last response, or a quote or code block from it",
319
+ "Choose a response to copy, or a quote or code block from it",
314
320
  self.copy,
315
321
  group="Inspect",
316
322
  ),
@@ -727,7 +733,29 @@ class PreviewApp:
727
733
  return bindings.complete(argument) if bindings is not None else ()
728
734
 
729
735
  def config(self, argument: str) -> None:
730
- args = shlex.split(argument)
736
+ if not argument.strip():
737
+ self.config_requested = True
738
+ return
739
+ self.transcript.note(self.apply_config(shlex.split(argument)))
740
+
741
+ async def browse_config(self, output: TerminalOutput, session) -> None:
742
+ from pcode.config_ui import ConfigBrowser
743
+
744
+ self.config_requested = False
745
+ try:
746
+ async with self.popup(output, session) as modal_input:
747
+ browser = ConfigBrowser(
748
+ save=self.apply_config,
749
+ input=modal_input,
750
+ output=session.app.output,
751
+ style=session.app.style,
752
+ )
753
+ await browser.run()
754
+ except (ValueError, OSError) as error:
755
+ self.transcript.warning(f"Could not open configuration: {error}")
756
+
757
+ def apply_config(self, args: list[str]) -> str:
758
+ """One write and immediate-layout path for commands and the browser."""
731
759
  try:
732
760
  result = configure(args)
733
761
  except OSError as error:
@@ -751,25 +779,9 @@ class PreviewApp:
751
779
  if self.transcript.output is not None:
752
780
  self.transcript.output.app.invalidate()
753
781
  edits = args[1:] if args[:1] == ["project"] else args
754
- if (
755
- len(edits) >= 2
756
- and edits[0] in ("set", "unset")
757
- and edits[1]
758
- in (
759
- "attach_tasks",
760
- "tasks_max_height",
761
- "tasks_min_rows",
762
- "tasks_min_columns",
763
- "task_style",
764
- "tool_glyphs",
765
- "tool_max_lines",
766
- "tool_linger_seconds",
767
- "show_hints",
768
- "live_edits",
769
- )
770
- ):
782
+ if len(edits) >= 2 and edits[0] in ("set", "unset") and edits[1] in IMMEDIATE_SETTINGS:
771
783
  result = result.replace("Applies on next launch.", "Layout settings apply immediately.")
772
- self.transcript.note(result)
784
+ return result
773
785
 
774
786
  def set_show_tasks(self, shown: bool) -> None:
775
787
  self.activity.show_tasks = shown
@@ -825,7 +837,8 @@ class PreviewApp:
825
837
  if self.transcript.output is not None:
826
838
  self.transcript.output.app.invalidate()
827
839
  self.transcript.flash(
828
- f"Show edits: {'on' if shown else 'off'}. Usage: /show-edits [on|off]"
840
+ f"Show edits: {'on' if shown else 'off'}. Usage: /show-edits [on|off] "
841
+ f"({self.shortcut('v')})"
829
842
  )
830
843
 
831
844
  def group_tools(self, argument: str) -> None:
@@ -836,7 +849,8 @@ class PreviewApp:
836
849
  if self.transcript.output is not None:
837
850
  self.transcript.output.app.invalidate()
838
851
  self.transcript.flash(
839
- f"Group tools: {'on' if grouped else 'off'}. Usage: /group-tools [on|off]"
852
+ f"Group tools: {'on' if grouped else 'off'}. Usage: /group-tools [on|off] "
853
+ f"({self.shortcut(']')})"
840
854
  )
841
855
 
842
856
  def set_thinking_mode(self, mode: str) -> None:
@@ -924,7 +938,7 @@ class PreviewApp:
924
938
  def persist_defaults(self, **updates: str) -> None:
925
939
  try:
926
940
  save_preferences(**updates)
927
- except (OSError, ValueError):
941
+ except OSError, ValueError:
928
942
  self.transcript.warning("Could not save defaults; this selection applies only here.")
929
943
 
930
944
  def forget_defaults(self, *keys: str) -> None:
@@ -932,7 +946,7 @@ class PreviewApp:
932
946
 
933
947
  try:
934
948
  update_preferences({}, remove=keys)
935
- except (OSError, ValueError):
949
+ except OSError, ValueError:
936
950
  self.transcript.warning("Could not update defaults; this change applies only here.")
937
951
 
938
952
  @asynccontextmanager
@@ -1056,21 +1070,20 @@ class PreviewApp:
1056
1070
 
1057
1071
  async def choose_copy(self, output: TerminalOutput, session) -> None:
1058
1072
  from pcode.clipboard import copy as copy_to_clipboard
1059
- from pcode.copy_ui import last_response, snippet_dialog, snippets
1060
- from pcode.session_ui import literal
1073
+ from pcode.copy_ui import answers, copy_dialog, snippets
1061
1074
 
1062
1075
  self.copy_requested = False
1063
1076
  tree = getattr(self.runtime, "tree", None)
1064
- # Redacted like /tree's copy: what leaves pcode never carries a secret.
1065
- choices = snippets(literal(last_response(tree))) if tree is not None else []
1066
- if not choices:
1077
+ history = answers(tree) if tree is not None else []
1078
+ if not history:
1067
1079
  self.transcript.note("No response to copy yet.")
1068
1080
  return
1081
+ choices = snippets(history[0].text)
1069
1082
  choice = choices[0]
1070
- if len(choices) > 1:
1083
+ if len(history) > 1 or len(choices) > 1:
1071
1084
  async with self.popup(output, session) as modal_input:
1072
- dialog = snippet_dialog(
1073
- choices,
1085
+ dialog = copy_dialog(
1086
+ history,
1074
1087
  input=modal_input,
1075
1088
  output=session.app.output,
1076
1089
  style=session.app.style,
@@ -1941,7 +1954,7 @@ class PreviewApp:
1941
1954
  timeout=1,
1942
1955
  )
1943
1956
  self.branch = plain(result.stdout.strip(), limit=None) if result.returncode == 0 else ""
1944
- except (OSError, subprocess.TimeoutExpired):
1957
+ except OSError, subprocess.TimeoutExpired:
1945
1958
  self.branch = ""
1946
1959
  return self.branch != previous
1947
1960
 
@@ -2063,6 +2076,8 @@ class PreviewApp:
2063
2076
  self._command_popup_generation = tag
2064
2077
  try:
2065
2078
  self.handle(text)
2079
+ if self.config_requested:
2080
+ await self.browse_config(output, session)
2066
2081
  if self.worker_view_requested:
2067
2082
  await self.read_workers(output, session)
2068
2083
  if self.tree_requested:
@@ -2168,6 +2183,7 @@ class PreviewApp:
2168
2183
  self.activity.busy = False
2169
2184
  else:
2170
2185
  session.app.create_background_task(refresh_metadata())
2186
+ session.app.create_background_task(controller.warn_outdated())
2171
2187
  controller.start_mcp_defaults()
2172
2188
  finally:
2173
2189
  self._startup_pending = False
@@ -2251,7 +2267,7 @@ class PreviewApp:
2251
2267
  self.transcript.note(f"Cleared {prompts} queued message(s).")
2252
2268
  self.activity.busy = False
2253
2269
 
2254
- def submit(text):
2270
+ def submit(text, *, mode_override: str | None = None):
2255
2271
  text = text.strip()
2256
2272
  ready = early is None and getattr(self.controller, "ready", None)
2257
2273
  if (early is not None or (ready and not ready.is_set())) and text in {"/quit", "/exit"}:
@@ -2273,7 +2289,7 @@ class PreviewApp:
2273
2289
  mode = "shell"
2274
2290
  elif text:
2275
2291
  # One send consumes a Ctrl+S pick; the saved default returns.
2276
- mode = self.next_send_mode
2292
+ mode = mode_override or self.next_send_mode
2277
2293
  self.send_mode_once = None
2278
2294
  else:
2279
2295
  return
@@ -2290,6 +2306,7 @@ class PreviewApp:
2290
2306
  transcript=self.transcript,
2291
2307
  workspace=lambda: self.workspace,
2292
2308
  on_submit=submit,
2309
+ on_interrupt_submit=lambda text: submit(text, mode_override="interrupt"),
2293
2310
  on_command=submit,
2294
2311
  on_cancel=cancel,
2295
2312
  on_tasks=self.set_show_tasks,
@@ -2319,7 +2336,7 @@ class PreviewApp:
2319
2336
  )
2320
2337
  try:
2321
2338
  terminal = session.app.output.fileno()
2322
- except (NotImplementedError, OSError, ValueError):
2339
+ except NotImplementedError, OSError, ValueError:
2323
2340
  terminal = None
2324
2341
  if terminal is not None:
2325
2342
  mode = load_preferences().get("terminal_progress", "auto")
@@ -88,7 +88,7 @@ def _gnome_read() -> str | None:
88
88
  text=True,
89
89
  timeout=2,
90
90
  )
91
- except (OSError, subprocess.SubprocessError):
91
+ except OSError, subprocess.SubprocessError:
92
92
  return None
93
93
  if result.returncode != 0:
94
94
  return None
@@ -102,7 +102,7 @@ def desktop_reader():
102
102
  try:
103
103
  if sys.platform == "darwin":
104
104
  return _macos_reader()
105
- except (OSError, AttributeError, ValueError):
105
+ except OSError, AttributeError, ValueError:
106
106
  return None
107
107
  if sys.platform.startswith("linux") and shutil.which("gsettings"):
108
108
  if os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY"):
@@ -33,7 +33,7 @@ from rich.theme import Theme
33
33
 
34
34
  from pcode.aside import Aside, Asides, Bridge
35
35
  from pcode.clipboard import copy as copy_to_clipboard
36
- from pcode.copy_ui import Snippet, SnippetPicker, snippets
36
+ from pcode.copy_ui import Answer, CopyPicker, Snippet, snippets
37
37
  from pcode.frame import Frame
38
38
  from pcode.links import Link, extract_links, open_link, remember_link
39
39
  from pcode.links_ui import LinkPicker
@@ -113,7 +113,7 @@ class AsideBrowser:
113
113
  returning a `Bridge` from `run`; `check_bridge(thread)` raises `ValueError`
114
114
  when it cannot yet.
115
115
 
116
- Prefix `y` copies the newest answer and prefix `o` opens a link from the
116
+ Prefix `y` chooses an answer to copy and prefix `o` opens a link from the
117
117
  thread, with the same pickers as `/copy` and `/links`, over the viewer.
118
118
  The editor takes each action as a slash command too: see `commands`.
119
119
 
@@ -169,7 +169,7 @@ class AsideBrowser:
169
169
  self._ids: list[str] = []
170
170
  self.notice = ""
171
171
  # The copy or link picker over the viewer, while one is open.
172
- self.picker: SnippetPicker | LinkPicker | None = None
172
+ self.picker: CopyPicker | LinkPicker | None = None
173
173
  self.list = TextArea(read_only=True, wrap_lines=False, scrollbar=True)
174
174
  self.list.window.cursorline = Always()
175
175
  self.detail = RichPane(theme=rich_theme, color_system=color_system)
@@ -305,7 +305,7 @@ class AsideBrowser:
305
305
  )
306
306
  picker = Frame(
307
307
  DynamicContainer(lambda: self.picker.container if self.picker else HSplit([])),
308
- title=lambda: "Links" if isinstance(self.picker, LinkPicker) else "Copy",
308
+ title=lambda: self.picker.title if isinstance(self.picker, CopyPicker) else "Links",
309
309
  )
310
310
  overlaid = FloatContainer(
311
311
  root_container,
@@ -386,16 +386,8 @@ class AsideBrowser:
386
386
 
387
387
  def help(self) -> list[tuple[str, str]]:
388
388
  """Describe the active picker, editor, or reader, including its current focus."""
389
- if isinstance(self.picker, LinkPicker):
390
- return self.picker.help()
391
389
  if self.picker is not None:
392
- return [
393
- ("↑/↓", "Select snippet"),
394
- ("PgUp/PgDn", "Page"),
395
- ("Ctrl+U/D", "Half page"),
396
- ("Enter", "Copy selected snippet"),
397
- ("Esc/Ctrl+C", "Cancel"),
398
- ]
390
+ return self.picker.help()
399
391
  interrupt = ("Ctrl+C", "Stop running answers" if self.asides.running else "Close")
400
392
  if self.editing():
401
393
  prompting = self.input.prompting
@@ -444,7 +436,7 @@ class AsideBrowser:
444
436
  self.finish(Bridge(self.selected, kind, focus))
445
437
 
446
438
  commands = [
447
- PopupCommand("/copy", "Copy the newest answer", lambda _: self.copy(self.app.output)),
439
+ PopupCommand("/copy", "Choose an answer to copy", lambda _: self.copy(self.app.output)),
448
440
  PopupCommand("/links", "Open a link from this thread", lambda _: self.choose_link()),
449
441
  ]
450
442
  if self.check_bridge is not None:
@@ -550,28 +542,27 @@ class AsideBrowser:
550
542
  self.refresh()
551
543
 
552
544
  def copy(self, output=None) -> None:
553
- """Copy the thread's newest answer, even while it is still arriving.
554
-
555
- As `/copy` does: the redacted text the pane shows, and an answer
556
- holding quotes or code blocks opens a picker to copy just one.
557
- """
558
- answered = [literal(aside.answer) for aside in self.current()]
559
- answered = [answer for answer in answered if answer]
545
+ """Choose an answer from this thread, newest first, including partial text."""
546
+ answered = [
547
+ Answer(aside.question, aside.answer)
548
+ for aside in reversed(self.current())
549
+ if aside.answer.strip()
550
+ ]
551
+ answered = [answer for answer in answered if answer.text.strip()]
560
552
  if not answered:
561
553
  self.notice = "No answer to copy"
562
554
  return
563
- choices = snippets(answered[-1])
564
- if len(choices) > 1:
555
+ if len(answered) > 1 or len(snippets(answered[0].text)) > 1:
565
556
  self.open_picker(
566
- lambda close: SnippetPicker(
567
- choices,
557
+ lambda close: CopyPicker(
558
+ answered,
568
559
  on_pick=lambda snippet: self.copy_snippet(close, snippet, output),
569
560
  on_cancel=close,
570
561
  shortcuts=self.prefix_keys,
571
562
  )
572
563
  )
573
564
  return
574
- self.copy_text("answer", answered[-1], output)
565
+ self.copy_text("answer", answered[0].text, output)
575
566
 
576
567
  def copy_snippet(self, close, snippet: Snippet, output=None) -> None:
577
568
  close()
@@ -631,7 +622,7 @@ class AsideBrowser:
631
622
  self.app.layout.focus(previous)
632
623
 
633
624
  self.picker = build(close)
634
- # A link picker opens in its search line, so typing filters at once.
625
+ # Searchable pickers open in their search line, so typing filters at once.
635
626
  focus_overlay(self.app, getattr(self.picker, "query", None) or self.picker.list)
636
627
 
637
628
  def details(self, thread: list[Aside]) -> tuple[list, int]:
@@ -56,7 +56,7 @@ def oauth_user_agent() -> str:
56
56
  timeout=VERSION_TIMEOUT_SECONDS,
57
57
  check=True,
58
58
  )
59
- except (OSError, subprocess.SubprocessError):
59
+ except OSError, subprocess.SubprocessError:
60
60
  pass
61
61
  else:
62
62
  local = _parse_version(result.stdout)