superqode 0.1.5__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 (288) hide show
  1. superqode/__init__.py +33 -0
  2. superqode/acp/__init__.py +23 -0
  3. superqode/acp/client.py +913 -0
  4. superqode/acp/permission_screen.py +457 -0
  5. superqode/acp/types.py +480 -0
  6. superqode/acp_discovery.py +856 -0
  7. superqode/agent/__init__.py +22 -0
  8. superqode/agent/edit_strategies.py +334 -0
  9. superqode/agent/loop.py +892 -0
  10. superqode/agent/qe_report_templates.py +39 -0
  11. superqode/agent/system_prompts.py +353 -0
  12. superqode/agent_output.py +721 -0
  13. superqode/agent_stream.py +953 -0
  14. superqode/agents/__init__.py +59 -0
  15. superqode/agents/acp_registry.py +305 -0
  16. superqode/agents/client.py +249 -0
  17. superqode/agents/data/augmentcode.com.toml +51 -0
  18. superqode/agents/data/cagent.dev.toml +51 -0
  19. superqode/agents/data/claude.com.toml +60 -0
  20. superqode/agents/data/codeassistant.dev.toml +51 -0
  21. superqode/agents/data/codex.openai.com.toml +57 -0
  22. superqode/agents/data/fastagent.ai.toml +66 -0
  23. superqode/agents/data/geminicli.com.toml +77 -0
  24. superqode/agents/data/goose.block.xyz.toml +54 -0
  25. superqode/agents/data/junie.jetbrains.com.toml +56 -0
  26. superqode/agents/data/kimi.moonshot.cn.toml +57 -0
  27. superqode/agents/data/llmlingagent.dev.toml +51 -0
  28. superqode/agents/data/molt.bot.toml +49 -0
  29. superqode/agents/data/opencode.ai.toml +60 -0
  30. superqode/agents/data/stakpak.dev.toml +51 -0
  31. superqode/agents/data/vtcode.dev.toml +51 -0
  32. superqode/agents/discovery.py +266 -0
  33. superqode/agents/messaging.py +160 -0
  34. superqode/agents/persona.py +166 -0
  35. superqode/agents/registry.py +421 -0
  36. superqode/agents/schema.py +72 -0
  37. superqode/agents/unified.py +367 -0
  38. superqode/app/__init__.py +111 -0
  39. superqode/app/constants.py +314 -0
  40. superqode/app/css.py +366 -0
  41. superqode/app/models.py +118 -0
  42. superqode/app/suggester.py +125 -0
  43. superqode/app/widgets.py +1591 -0
  44. superqode/app_enhanced.py +399 -0
  45. superqode/app_main.py +17187 -0
  46. superqode/approval.py +312 -0
  47. superqode/atomic.py +296 -0
  48. superqode/commands/__init__.py +1 -0
  49. superqode/commands/acp.py +965 -0
  50. superqode/commands/agents.py +180 -0
  51. superqode/commands/auth.py +278 -0
  52. superqode/commands/config.py +374 -0
  53. superqode/commands/init.py +826 -0
  54. superqode/commands/providers.py +819 -0
  55. superqode/commands/qe.py +1145 -0
  56. superqode/commands/roles.py +380 -0
  57. superqode/commands/serve.py +172 -0
  58. superqode/commands/suggestions.py +127 -0
  59. superqode/commands/superqe.py +460 -0
  60. superqode/config/__init__.py +51 -0
  61. superqode/config/loader.py +812 -0
  62. superqode/config/schema.py +498 -0
  63. superqode/core/__init__.py +111 -0
  64. superqode/core/roles.py +281 -0
  65. superqode/danger.py +386 -0
  66. superqode/data/superqode-template.yaml +1522 -0
  67. superqode/design_system.py +1080 -0
  68. superqode/dialogs/__init__.py +6 -0
  69. superqode/dialogs/base.py +39 -0
  70. superqode/dialogs/model.py +130 -0
  71. superqode/dialogs/provider.py +870 -0
  72. superqode/diff_view.py +919 -0
  73. superqode/enterprise.py +21 -0
  74. superqode/evaluation/__init__.py +25 -0
  75. superqode/evaluation/adapters.py +93 -0
  76. superqode/evaluation/behaviors.py +89 -0
  77. superqode/evaluation/engine.py +209 -0
  78. superqode/evaluation/scenarios.py +96 -0
  79. superqode/execution/__init__.py +36 -0
  80. superqode/execution/linter.py +538 -0
  81. superqode/execution/modes.py +347 -0
  82. superqode/execution/resolver.py +283 -0
  83. superqode/execution/runner.py +642 -0
  84. superqode/file_explorer.py +811 -0
  85. superqode/file_viewer.py +471 -0
  86. superqode/flash.py +183 -0
  87. superqode/guidance/__init__.py +58 -0
  88. superqode/guidance/config.py +203 -0
  89. superqode/guidance/prompts.py +71 -0
  90. superqode/harness/__init__.py +54 -0
  91. superqode/harness/accelerator.py +291 -0
  92. superqode/harness/config.py +319 -0
  93. superqode/harness/validator.py +147 -0
  94. superqode/history.py +279 -0
  95. superqode/integrations/superopt_runner.py +124 -0
  96. superqode/logging/__init__.py +49 -0
  97. superqode/logging/adapters.py +219 -0
  98. superqode/logging/formatter.py +923 -0
  99. superqode/logging/integration.py +341 -0
  100. superqode/logging/sinks.py +170 -0
  101. superqode/logging/unified_log.py +417 -0
  102. superqode/lsp/__init__.py +26 -0
  103. superqode/lsp/client.py +544 -0
  104. superqode/main.py +1069 -0
  105. superqode/mcp/__init__.py +89 -0
  106. superqode/mcp/auth_storage.py +380 -0
  107. superqode/mcp/client.py +1236 -0
  108. superqode/mcp/config.py +319 -0
  109. superqode/mcp/integration.py +337 -0
  110. superqode/mcp/oauth.py +436 -0
  111. superqode/mcp/oauth_callback.py +385 -0
  112. superqode/mcp/types.py +290 -0
  113. superqode/memory/__init__.py +31 -0
  114. superqode/memory/feedback.py +342 -0
  115. superqode/memory/store.py +522 -0
  116. superqode/notifications.py +369 -0
  117. superqode/optimization/__init__.py +5 -0
  118. superqode/optimization/config.py +33 -0
  119. superqode/permissions/__init__.py +25 -0
  120. superqode/permissions/rules.py +488 -0
  121. superqode/plan.py +323 -0
  122. superqode/providers/__init__.py +33 -0
  123. superqode/providers/gateway/__init__.py +165 -0
  124. superqode/providers/gateway/base.py +228 -0
  125. superqode/providers/gateway/litellm_gateway.py +1170 -0
  126. superqode/providers/gateway/openresponses_gateway.py +436 -0
  127. superqode/providers/health.py +297 -0
  128. superqode/providers/huggingface/__init__.py +74 -0
  129. superqode/providers/huggingface/downloader.py +472 -0
  130. superqode/providers/huggingface/endpoints.py +442 -0
  131. superqode/providers/huggingface/hub.py +531 -0
  132. superqode/providers/huggingface/inference.py +394 -0
  133. superqode/providers/huggingface/transformers_runner.py +516 -0
  134. superqode/providers/local/__init__.py +100 -0
  135. superqode/providers/local/base.py +438 -0
  136. superqode/providers/local/discovery.py +418 -0
  137. superqode/providers/local/lmstudio.py +256 -0
  138. superqode/providers/local/mlx.py +457 -0
  139. superqode/providers/local/ollama.py +486 -0
  140. superqode/providers/local/sglang.py +268 -0
  141. superqode/providers/local/tgi.py +260 -0
  142. superqode/providers/local/tool_support.py +477 -0
  143. superqode/providers/local/vllm.py +258 -0
  144. superqode/providers/manager.py +1338 -0
  145. superqode/providers/models.py +1016 -0
  146. superqode/providers/models_dev.py +578 -0
  147. superqode/providers/openresponses/__init__.py +87 -0
  148. superqode/providers/openresponses/converters/__init__.py +17 -0
  149. superqode/providers/openresponses/converters/messages.py +343 -0
  150. superqode/providers/openresponses/converters/tools.py +268 -0
  151. superqode/providers/openresponses/schema/__init__.py +56 -0
  152. superqode/providers/openresponses/schema/models.py +585 -0
  153. superqode/providers/openresponses/streaming/__init__.py +5 -0
  154. superqode/providers/openresponses/streaming/parser.py +338 -0
  155. superqode/providers/openresponses/tools/__init__.py +21 -0
  156. superqode/providers/openresponses/tools/apply_patch.py +352 -0
  157. superqode/providers/openresponses/tools/code_interpreter.py +290 -0
  158. superqode/providers/openresponses/tools/file_search.py +333 -0
  159. superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
  160. superqode/providers/registry.py +716 -0
  161. superqode/providers/usage.py +332 -0
  162. superqode/pure_mode.py +384 -0
  163. superqode/qr/__init__.py +23 -0
  164. superqode/qr/dashboard.py +781 -0
  165. superqode/qr/generator.py +1018 -0
  166. superqode/qr/templates.py +135 -0
  167. superqode/safety/__init__.py +41 -0
  168. superqode/safety/sandbox.py +413 -0
  169. superqode/safety/warnings.py +256 -0
  170. superqode/server/__init__.py +33 -0
  171. superqode/server/lsp_server.py +775 -0
  172. superqode/server/web.py +250 -0
  173. superqode/session/__init__.py +25 -0
  174. superqode/session/persistence.py +580 -0
  175. superqode/session/sharing.py +477 -0
  176. superqode/session.py +475 -0
  177. superqode/sidebar.py +2991 -0
  178. superqode/stream_view.py +648 -0
  179. superqode/styles/__init__.py +3 -0
  180. superqode/superqe/__init__.py +184 -0
  181. superqode/superqe/acp_runner.py +1064 -0
  182. superqode/superqe/constitution/__init__.py +62 -0
  183. superqode/superqe/constitution/evaluator.py +308 -0
  184. superqode/superqe/constitution/loader.py +432 -0
  185. superqode/superqe/constitution/schema.py +250 -0
  186. superqode/superqe/events.py +591 -0
  187. superqode/superqe/frameworks/__init__.py +65 -0
  188. superqode/superqe/frameworks/base.py +234 -0
  189. superqode/superqe/frameworks/e2e.py +263 -0
  190. superqode/superqe/frameworks/executor.py +237 -0
  191. superqode/superqe/frameworks/javascript.py +409 -0
  192. superqode/superqe/frameworks/python.py +373 -0
  193. superqode/superqe/frameworks/registry.py +92 -0
  194. superqode/superqe/mcp_tools/__init__.py +47 -0
  195. superqode/superqe/mcp_tools/core_tools.py +418 -0
  196. superqode/superqe/mcp_tools/registry.py +230 -0
  197. superqode/superqe/mcp_tools/testing_tools.py +167 -0
  198. superqode/superqe/noise.py +89 -0
  199. superqode/superqe/orchestrator.py +778 -0
  200. superqode/superqe/roles.py +609 -0
  201. superqode/superqe/session.py +713 -0
  202. superqode/superqe/skills/__init__.py +57 -0
  203. superqode/superqe/skills/base.py +106 -0
  204. superqode/superqe/skills/core_skills.py +899 -0
  205. superqode/superqe/skills/registry.py +90 -0
  206. superqode/superqe/verifier.py +101 -0
  207. superqode/superqe_cli.py +76 -0
  208. superqode/tool_call.py +358 -0
  209. superqode/tools/__init__.py +93 -0
  210. superqode/tools/agent_tools.py +496 -0
  211. superqode/tools/base.py +324 -0
  212. superqode/tools/batch_tool.py +133 -0
  213. superqode/tools/diagnostics.py +311 -0
  214. superqode/tools/edit_tools.py +653 -0
  215. superqode/tools/enhanced_base.py +515 -0
  216. superqode/tools/file_tools.py +269 -0
  217. superqode/tools/file_tracking.py +45 -0
  218. superqode/tools/lsp_tools.py +610 -0
  219. superqode/tools/network_tools.py +350 -0
  220. superqode/tools/permissions.py +400 -0
  221. superqode/tools/question_tool.py +324 -0
  222. superqode/tools/search_tools.py +598 -0
  223. superqode/tools/shell_tools.py +259 -0
  224. superqode/tools/todo_tools.py +121 -0
  225. superqode/tools/validation.py +80 -0
  226. superqode/tools/web_tools.py +639 -0
  227. superqode/tui.py +1152 -0
  228. superqode/tui_integration.py +875 -0
  229. superqode/tui_widgets/__init__.py +27 -0
  230. superqode/tui_widgets/widgets/__init__.py +18 -0
  231. superqode/tui_widgets/widgets/progress.py +185 -0
  232. superqode/tui_widgets/widgets/tool_display.py +188 -0
  233. superqode/undo_manager.py +574 -0
  234. superqode/utils/__init__.py +5 -0
  235. superqode/utils/error_handling.py +323 -0
  236. superqode/utils/fuzzy.py +257 -0
  237. superqode/widgets/__init__.py +477 -0
  238. superqode/widgets/agent_collab.py +390 -0
  239. superqode/widgets/agent_store.py +936 -0
  240. superqode/widgets/agent_switcher.py +395 -0
  241. superqode/widgets/animation_manager.py +284 -0
  242. superqode/widgets/code_context.py +356 -0
  243. superqode/widgets/command_palette.py +412 -0
  244. superqode/widgets/connection_status.py +537 -0
  245. superqode/widgets/conversation_history.py +470 -0
  246. superqode/widgets/diff_indicator.py +155 -0
  247. superqode/widgets/enhanced_status_bar.py +385 -0
  248. superqode/widgets/enhanced_toast.py +476 -0
  249. superqode/widgets/file_browser.py +809 -0
  250. superqode/widgets/file_reference.py +585 -0
  251. superqode/widgets/issue_timeline.py +340 -0
  252. superqode/widgets/leader_key.py +264 -0
  253. superqode/widgets/mode_switcher.py +445 -0
  254. superqode/widgets/model_picker.py +234 -0
  255. superqode/widgets/permission_preview.py +1205 -0
  256. superqode/widgets/prompt.py +358 -0
  257. superqode/widgets/provider_connect.py +725 -0
  258. superqode/widgets/pty_shell.py +587 -0
  259. superqode/widgets/qe_dashboard.py +321 -0
  260. superqode/widgets/resizable_sidebar.py +377 -0
  261. superqode/widgets/response_changes.py +218 -0
  262. superqode/widgets/response_display.py +528 -0
  263. superqode/widgets/rich_tool_display.py +613 -0
  264. superqode/widgets/sidebar_panels.py +1180 -0
  265. superqode/widgets/slash_complete.py +356 -0
  266. superqode/widgets/split_view.py +612 -0
  267. superqode/widgets/status_bar.py +273 -0
  268. superqode/widgets/superqode_display.py +786 -0
  269. superqode/widgets/thinking_display.py +815 -0
  270. superqode/widgets/throbber.py +87 -0
  271. superqode/widgets/toast.py +206 -0
  272. superqode/widgets/unified_output.py +1073 -0
  273. superqode/workspace/__init__.py +75 -0
  274. superqode/workspace/artifacts.py +472 -0
  275. superqode/workspace/coordinator.py +353 -0
  276. superqode/workspace/diff_tracker.py +429 -0
  277. superqode/workspace/git_guard.py +373 -0
  278. superqode/workspace/git_snapshot.py +526 -0
  279. superqode/workspace/manager.py +750 -0
  280. superqode/workspace/snapshot.py +357 -0
  281. superqode/workspace/watcher.py +535 -0
  282. superqode/workspace/worktree.py +440 -0
  283. superqode-0.1.5.dist-info/METADATA +204 -0
  284. superqode-0.1.5.dist-info/RECORD +288 -0
  285. superqode-0.1.5.dist-info/WHEEL +5 -0
  286. superqode-0.1.5.dist-info/entry_points.txt +3 -0
  287. superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
  288. superqode-0.1.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,457 @@
1
+ """
2
+ Permission Screen for ACP permission requests.
3
+
4
+ Shows a modal dialog when the agent requests permission to perform an action.
5
+
6
+ Enhanced Features:
7
+ - Support for multi-file permission requests
8
+ - j/k navigation between requests
9
+ - Multiple diff view modes
10
+ - Integration with enhanced permission preview
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import asyncio
16
+ from typing import Callable, Awaitable, List, Optional
17
+
18
+ from textual.app import ComposeResult
19
+ from textual.binding import Binding
20
+ from textual.containers import Container, Vertical, Horizontal
21
+ from textual.screen import ModalScreen
22
+ from textual.widgets import Static, Button, Label
23
+ from textual.reactive import reactive
24
+
25
+ from rich.text import Text
26
+ from rich.panel import Panel
27
+
28
+ from superqode.acp.types import PermissionOption, ToolCall
29
+
30
+
31
+ # Theme colors
32
+ THEME = {
33
+ "purple": "#a855f7",
34
+ "pink": "#ec4899",
35
+ "success": "#22c55e",
36
+ "error": "#ef4444",
37
+ "warning": "#f59e0b",
38
+ "text": "#e4e4e7",
39
+ "muted": "#71717a",
40
+ "dim": "#52525b",
41
+ "bg": "#000000",
42
+ }
43
+
44
+
45
+ class PermissionScreen(ModalScreen[str]):
46
+ """
47
+ Modal screen for handling ACP permission requests.
48
+
49
+ Returns the selected option ID.
50
+
51
+ Enhanced keyboard shortcuts:
52
+ - a: Allow once
53
+ - A: Allow always
54
+ - r: Reject once
55
+ - R: Reject always
56
+ - j: Next request (when multiple)
57
+ - k: Previous request (when multiple)
58
+ - v: Toggle diff view mode
59
+ - ?: Show help
60
+ """
61
+
62
+ BINDINGS = [
63
+ Binding("a", "allow_once", "Allow once", priority=True),
64
+ Binding("A", "allow_always", "Allow always", priority=True),
65
+ Binding("r", "reject_once", "Reject once", priority=True),
66
+ Binding("R", "reject_always", "Reject always", priority=True),
67
+ Binding("j", "next_request", "Next", priority=True, show=False),
68
+ Binding("k", "prev_request", "Previous", priority=True, show=False),
69
+ Binding("v", "toggle_view", "Toggle view", priority=True, show=False),
70
+ Binding("?", "show_help", "Help", priority=True, show=False),
71
+ Binding("escape", "cancel", "Cancel", priority=True),
72
+ ]
73
+
74
+ CSS = """
75
+ PermissionScreen {
76
+ align: center middle;
77
+ }
78
+
79
+ #permission-dialog {
80
+ width: 80;
81
+ height: auto;
82
+ max-height: 30;
83
+ background: #0a0a0a;
84
+ border: tall #a855f7;
85
+ padding: 1 2;
86
+ }
87
+
88
+ #permission-title {
89
+ text-align: center;
90
+ text-style: bold;
91
+ color: #f59e0b;
92
+ margin-bottom: 1;
93
+ }
94
+
95
+ #permission-tool {
96
+ margin-bottom: 1;
97
+ }
98
+
99
+ #permission-content {
100
+ height: auto;
101
+ max-height: 15;
102
+ overflow-y: auto;
103
+ margin-bottom: 1;
104
+ padding: 1;
105
+ background: #000000;
106
+ border: round #1a1a1a;
107
+ }
108
+
109
+ #permission-buttons {
110
+ height: auto;
111
+ align: center middle;
112
+ }
113
+
114
+ .permission-btn {
115
+ margin: 0 1;
116
+ }
117
+
118
+ .allow-btn {
119
+ background: #22c55e;
120
+ }
121
+
122
+ .reject-btn {
123
+ background: #ef4444;
124
+ }
125
+
126
+ #permission-hints {
127
+ text-align: center;
128
+ color: #52525b;
129
+ margin-top: 1;
130
+ }
131
+ """
132
+
133
+ def __init__(
134
+ self,
135
+ options: list[PermissionOption],
136
+ tool_call: ToolCall,
137
+ name: str | None = None,
138
+ id: str | None = None,
139
+ classes: str | None = None,
140
+ ):
141
+ super().__init__(name=name, id=id, classes=classes)
142
+ self.options = options
143
+ self.tool_call = tool_call
144
+ self._option_map: dict[str, str] = {} # kind -> optionId
145
+
146
+ for opt in options:
147
+ kind = opt.get("kind", "")
148
+ option_id = opt.get("optionId", "")
149
+ self._option_map[kind] = option_id
150
+
151
+ def compose(self) -> ComposeResult:
152
+ with Container(id="permission-dialog"):
153
+ yield Static("⚠️ Permission Request", id="permission-title")
154
+ yield Static(self._format_tool_info(), id="permission-tool")
155
+ yield Static(self._format_content(), id="permission-content")
156
+
157
+ with Horizontal(id="permission-buttons"):
158
+ yield Button("Allow [a]", id="btn-allow", classes="permission-btn allow-btn")
159
+ yield Button("Always [A]", id="btn-always", classes="permission-btn allow-btn")
160
+ yield Button("Reject [r]", id="btn-reject", classes="permission-btn reject-btn")
161
+ yield Button("Never [R]", id="btn-never", classes="permission-btn reject-btn")
162
+
163
+ yield Static(
164
+ "[a] Allow once [A] Allow always [r] Reject [R] Reject always [Esc] Cancel",
165
+ id="permission-hints",
166
+ )
167
+
168
+ def _format_tool_info(self) -> Text:
169
+ """Format the tool call information."""
170
+ t = Text()
171
+
172
+ title = self.tool_call.get("title", "Unknown operation")
173
+ kind = self.tool_call.get("kind", "other")
174
+
175
+ # Icon based on kind
176
+ icons = {
177
+ "read": "📖",
178
+ "edit": "✏️",
179
+ "delete": "🗑️",
180
+ "move": "📦",
181
+ "search": "🔍",
182
+ "execute": "💻",
183
+ "think": "🧠",
184
+ "fetch": "🌐",
185
+ "other": "🔧",
186
+ }
187
+ icon = icons.get(kind, "🔧")
188
+
189
+ t.append(f"{icon} ", style=f"bold {THEME['warning']}")
190
+ t.append(f"{title}\n", style=f"bold {THEME['text']}")
191
+ t.append(f"Type: {kind}", style=THEME["muted"])
192
+
193
+ return t
194
+
195
+ def _format_content(self) -> Text:
196
+ """Format the tool call content (diff, command, etc.)."""
197
+ t = Text()
198
+
199
+ content_list = self.tool_call.get("content", [])
200
+ raw_input = self.tool_call.get("rawInput", {})
201
+
202
+ for content in content_list:
203
+ content_type = content.get("type", "")
204
+
205
+ if content_type == "diff":
206
+ path = content.get("path", "")
207
+ old_text = content.get("oldText", "")
208
+ new_text = content.get("newText", "")
209
+
210
+ t.append(f"📄 File: {path}\n", style=f"bold {THEME['purple']}")
211
+
212
+ if old_text:
213
+ t.append("--- Old:\n", style=THEME["error"])
214
+ # Show first few lines
215
+ lines = old_text.split("\n")[:5]
216
+ for line in lines:
217
+ t.append(f" {line}\n", style=THEME["dim"])
218
+ if len(old_text.split("\n")) > 5:
219
+ t.append(" ...\n", style=THEME["dim"])
220
+
221
+ t.append("+++ New:\n", style=THEME["success"])
222
+ lines = new_text.split("\n")[:10]
223
+ for line in lines:
224
+ t.append(f" {line}\n", style=THEME["text"])
225
+ if len(new_text.split("\n")) > 10:
226
+ t.append(" ...\n", style=THEME["dim"])
227
+
228
+ elif content_type == "terminal":
229
+ terminal_id = content.get("terminalId", "")
230
+ t.append(f"💻 Terminal: {terminal_id}\n", style=f"bold {THEME['purple']}")
231
+
232
+ # Show raw input if no content
233
+ if not content_list and raw_input:
234
+ for key, value in raw_input.items():
235
+ if isinstance(value, str) and len(value) > 100:
236
+ value = value[:100] + "..."
237
+ t.append(f"{key}: ", style=THEME["muted"])
238
+ t.append(f"{value}\n", style=THEME["text"])
239
+
240
+ if not t.plain:
241
+ t.append("No details available", style=THEME["dim"])
242
+
243
+ return t
244
+
245
+ def action_allow_once(self) -> None:
246
+ """Allow this operation once."""
247
+ option_id = self._option_map.get("allow_once", "")
248
+ if option_id:
249
+ self.dismiss(option_id)
250
+ else:
251
+ # Fallback to first allow option
252
+ for opt in self.options:
253
+ if "allow" in opt.get("kind", ""):
254
+ self.dismiss(opt.get("optionId", ""))
255
+ return
256
+
257
+ def action_allow_always(self) -> None:
258
+ """Allow this operation always."""
259
+ option_id = self._option_map.get("allow_always", "")
260
+ if option_id:
261
+ self.dismiss(option_id)
262
+ else:
263
+ self.action_allow_once()
264
+
265
+ def action_reject_once(self) -> None:
266
+ """Reject this operation once."""
267
+ option_id = self._option_map.get("reject_once", "")
268
+ if not option_id:
269
+ option_id = self._option_map.get("reject", "")
270
+ if option_id:
271
+ self.dismiss(option_id)
272
+ else:
273
+ # Fallback to first reject option
274
+ for opt in self.options:
275
+ if "reject" in opt.get("kind", ""):
276
+ self.dismiss(opt.get("optionId", ""))
277
+ return
278
+
279
+ def action_reject_always(self) -> None:
280
+ """Reject this operation always."""
281
+ option_id = self._option_map.get("reject_always", "")
282
+ if option_id:
283
+ self.dismiss(option_id)
284
+ else:
285
+ self.action_reject_once()
286
+
287
+ def action_cancel(self) -> None:
288
+ """Cancel the permission request."""
289
+ self.dismiss("")
290
+
291
+ def on_button_pressed(self, event: Button.Pressed) -> None:
292
+ """Handle button presses."""
293
+ button_id = event.button.id
294
+
295
+ if button_id == "btn-allow":
296
+ self.action_allow_once()
297
+ elif button_id == "btn-always":
298
+ self.action_allow_always()
299
+ elif button_id == "btn-reject":
300
+ self.action_reject_once()
301
+ elif button_id == "btn-never":
302
+ self.action_reject_always()
303
+
304
+ def action_next_request(self) -> None:
305
+ """Navigate to next request (for multi-file support)."""
306
+ # This is a placeholder for multi-request navigation
307
+ # Will be used when batch permissions are implemented
308
+ pass
309
+
310
+ def action_prev_request(self) -> None:
311
+ """Navigate to previous request (for multi-file support)."""
312
+ # This is a placeholder for multi-request navigation
313
+ # Will be used when batch permissions are implemented
314
+ pass
315
+
316
+ def action_toggle_view(self) -> None:
317
+ """Toggle between unified and split diff view."""
318
+ # This is a placeholder for diff view mode toggle
319
+ # Will integrate with enhanced permission preview
320
+ pass
321
+
322
+ def action_show_help(self) -> None:
323
+ """Show help for permission screen."""
324
+ # Could show a help overlay with keyboard shortcuts
325
+ pass
326
+
327
+
328
+ class MultiPermissionScreen(ModalScreen[List[str]]):
329
+ """
330
+ Modal screen for handling multiple ACP permission requests.
331
+
332
+ Returns a list of (request_id, action) tuples for each request.
333
+ Uses the enhanced permission preview with navigator.
334
+ """
335
+
336
+ BINDINGS = [
337
+ Binding("a", "allow_once", "Allow", priority=True),
338
+ Binding("A", "allow_always", "Allow always", priority=True),
339
+ Binding("r", "reject_once", "Reject", priority=True),
340
+ Binding("R", "reject_always", "Reject always", priority=True),
341
+ Binding("j", "next_request", "Next", priority=True),
342
+ Binding("k", "prev_request", "Previous", priority=True),
343
+ Binding("v", "toggle_view", "Toggle view", priority=True),
344
+ Binding("enter", "confirm", "Confirm all", priority=True),
345
+ Binding("escape", "cancel", "Cancel", priority=True),
346
+ ]
347
+
348
+ CSS = """
349
+ MultiPermissionScreen {
350
+ align: center middle;
351
+ }
352
+
353
+ #multi-permission-dialog {
354
+ width: 90%;
355
+ height: 80%;
356
+ max-width: 120;
357
+ background: #0a0a0a;
358
+ border: tall #a855f7;
359
+ }
360
+ """
361
+
362
+ def __init__(
363
+ self,
364
+ requests: List[tuple], # List of (options, tool_call) tuples
365
+ name: str | None = None,
366
+ id: str | None = None,
367
+ classes: str | None = None,
368
+ ):
369
+ super().__init__(name=name, id=id, classes=classes)
370
+ self.requests = requests
371
+ self._decisions: dict[int, str] = {} # index -> action
372
+ self._current_index = 0
373
+
374
+ def compose(self) -> ComposeResult:
375
+ with Container(id="multi-permission-dialog"):
376
+ yield Static(
377
+ f" Multiple Permission Requests ({len(self.requests)} pending)",
378
+ id="permission-title",
379
+ )
380
+
381
+ # Will integrate with EnhancedPermissionPreviewScreen
382
+ yield Static("Permission requests will be shown here", id="request-content")
383
+
384
+ with Horizontal(id="permission-buttons"):
385
+ yield Button("Allow [a]", id="btn-allow", classes="permission-btn allow-btn")
386
+ yield Button("Always [A]", id="btn-always", classes="permission-btn allow-btn")
387
+ yield Button("Reject [r]", id="btn-reject", classes="permission-btn reject-btn")
388
+ yield Button("Never [R]", id="btn-never", classes="permission-btn reject-btn")
389
+
390
+ yield Static(
391
+ "[j/k] Navigate [a/A] Allow [r/R] Reject [Enter] Confirm all [Esc] Cancel",
392
+ id="permission-hints",
393
+ )
394
+
395
+ def action_allow_once(self) -> None:
396
+ """Allow current request once."""
397
+ self._decisions[self._current_index] = "allow_once"
398
+ self._advance()
399
+
400
+ def action_allow_always(self) -> None:
401
+ """Allow current request always."""
402
+ self._decisions[self._current_index] = "allow_always"
403
+ self._advance()
404
+
405
+ def action_reject_once(self) -> None:
406
+ """Reject current request."""
407
+ self._decisions[self._current_index] = "reject_once"
408
+ self._advance()
409
+
410
+ def action_reject_always(self) -> None:
411
+ """Reject current request always."""
412
+ self._decisions[self._current_index] = "reject_always"
413
+ self._advance()
414
+
415
+ def action_next_request(self) -> None:
416
+ """Go to next request."""
417
+ if self._current_index < len(self.requests) - 1:
418
+ self._current_index += 1
419
+ self._update_display()
420
+
421
+ def action_prev_request(self) -> None:
422
+ """Go to previous request."""
423
+ if self._current_index > 0:
424
+ self._current_index -= 1
425
+ self._update_display()
426
+
427
+ def action_toggle_view(self) -> None:
428
+ """Toggle diff view mode."""
429
+ pass
430
+
431
+ def action_confirm(self) -> None:
432
+ """Confirm all decisions."""
433
+ # Build result list
434
+ results = []
435
+ for i in range(len(self.requests)):
436
+ action = self._decisions.get(i, "reject_once") # Default to reject
437
+ results.append(action)
438
+ self.dismiss(results)
439
+
440
+ def action_cancel(self) -> None:
441
+ """Cancel all requests."""
442
+ self.dismiss([])
443
+
444
+ def _advance(self) -> None:
445
+ """Advance to next request or finish."""
446
+ if self._current_index < len(self.requests) - 1:
447
+ self._current_index += 1
448
+ self._update_display()
449
+ elif len(self._decisions) == len(self.requests):
450
+ # All decisions made
451
+ self.action_confirm()
452
+
453
+ def _update_display(self) -> None:
454
+ """Update the display for current request."""
455
+ # Update title with current position
456
+ title = self.query_one("#permission-title", Static)
457
+ title.update(f" Request {self._current_index + 1} of {len(self.requests)}")