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
superqode/approval.py ADDED
@@ -0,0 +1,312 @@
1
+ """
2
+ SuperQode Approval System - Accept/Reject File Changes
3
+
4
+ A beautiful approval UI for coding agent file modifications.
5
+ Features gradient styling and keyboard shortcuts.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from enum import Enum
12
+ from typing import List, Optional, Callable, Any
13
+ from datetime import datetime
14
+
15
+ from rich.console import Console
16
+ from rich.panel import Panel
17
+ from rich.text import Text
18
+ from rich.table import Table
19
+ from rich.box import ROUNDED, HEAVY, DOUBLE
20
+
21
+
22
+ class ApprovalAction(Enum):
23
+ """Possible approval actions."""
24
+
25
+ PENDING = "pending"
26
+ APPROVED = "approved"
27
+ APPROVED_ALWAYS = "approved_always"
28
+ REJECTED = "rejected"
29
+ REJECTED_ALWAYS = "rejected_always"
30
+ SKIPPED = "skipped"
31
+
32
+
33
+ @dataclass
34
+ class ApprovalRequest:
35
+ """A request for user approval."""
36
+
37
+ id: str
38
+ title: str
39
+ description: str
40
+ file_path: Optional[str] = None
41
+ old_content: Optional[str] = None
42
+ new_content: Optional[str] = None
43
+ command: Optional[str] = None
44
+ danger_level: int = 0 # 0=safe, 1=unknown, 2=dangerous, 3=destructive
45
+ action: ApprovalAction = ApprovalAction.PENDING
46
+ timestamp: datetime = field(default_factory=datetime.now)
47
+ metadata: dict = field(default_factory=dict)
48
+
49
+
50
+ # SuperQode approval colors
51
+ APPROVAL_COLORS = {
52
+ "approve": "#22c55e",
53
+ "approve_bg": "#22c55e20",
54
+ "reject": "#ef4444",
55
+ "reject_bg": "#ef444420",
56
+ "pending": "#f59e0b",
57
+ "pending_bg": "#f59e0b20",
58
+ "header": "#a855f7",
59
+ "border": "#2a2a2a",
60
+ "muted": "#71717a",
61
+ }
62
+
63
+ # Icons for approval UI
64
+ APPROVAL_ICONS = {
65
+ "pending": "⏳",
66
+ "approved": "✅",
67
+ "rejected": "❌",
68
+ "file": "📄",
69
+ "command": "💻",
70
+ "warning": "⚠️",
71
+ "danger": "🚨",
72
+ "question": "❓",
73
+ "approve": "👍",
74
+ "reject": "👎",
75
+ "skip": "⏭️",
76
+ "view": "👁️",
77
+ "diff": "📊",
78
+ }
79
+
80
+
81
+ class ApprovalManager:
82
+ """Manages approval requests and user decisions."""
83
+
84
+ def __init__(self, console: Console):
85
+ self.console = console
86
+ self.requests: List[ApprovalRequest] = []
87
+ self.always_approve: set = set() # Patterns to always approve
88
+ self.always_reject: set = set() # Patterns to always reject
89
+ self.history: List[ApprovalRequest] = []
90
+
91
+ def add_request(self, request: ApprovalRequest) -> None:
92
+ """Add a new approval request."""
93
+ # Check if auto-approve/reject applies
94
+ if request.file_path:
95
+ if any(p in request.file_path for p in self.always_approve):
96
+ request.action = ApprovalAction.APPROVED_ALWAYS
97
+ self.history.append(request)
98
+ return
99
+ if any(p in request.file_path for p in self.always_reject):
100
+ request.action = ApprovalAction.REJECTED_ALWAYS
101
+ self.history.append(request)
102
+ return
103
+
104
+ self.requests.append(request)
105
+
106
+ def get_pending(self) -> List[ApprovalRequest]:
107
+ """Get all pending requests."""
108
+ return [r for r in self.requests if r.action == ApprovalAction.PENDING]
109
+
110
+ def approve(self, request_id: str, always: bool = False) -> bool:
111
+ """Approve a request."""
112
+ for req in self.requests:
113
+ if req.id == request_id:
114
+ req.action = ApprovalAction.APPROVED_ALWAYS if always else ApprovalAction.APPROVED
115
+ if always and req.file_path:
116
+ # Add pattern for future auto-approve
117
+ self.always_approve.add(req.file_path)
118
+ self.history.append(req)
119
+ self.requests.remove(req)
120
+ return True
121
+ return False
122
+
123
+ def reject(self, request_id: str, always: bool = False) -> bool:
124
+ """Reject a request."""
125
+ for req in self.requests:
126
+ if req.id == request_id:
127
+ req.action = ApprovalAction.REJECTED_ALWAYS if always else ApprovalAction.REJECTED
128
+ if always and req.file_path:
129
+ self.always_reject.add(req.file_path)
130
+ self.history.append(req)
131
+ self.requests.remove(req)
132
+ return True
133
+ return False
134
+
135
+ def skip(self, request_id: str) -> bool:
136
+ """Skip a request (defer decision)."""
137
+ for req in self.requests:
138
+ if req.id == request_id:
139
+ req.action = ApprovalAction.SKIPPED
140
+ return True
141
+ return False
142
+
143
+ def approve_all(self) -> int:
144
+ """Approve all pending requests."""
145
+ count = 0
146
+ for req in list(self.requests):
147
+ if req.action == ApprovalAction.PENDING:
148
+ req.action = ApprovalAction.APPROVED
149
+ self.history.append(req)
150
+ self.requests.remove(req)
151
+ count += 1
152
+ return count
153
+
154
+ def reject_all(self) -> int:
155
+ """Reject all pending requests."""
156
+ count = 0
157
+ for req in list(self.requests):
158
+ if req.action == ApprovalAction.PENDING:
159
+ req.action = ApprovalAction.REJECTED
160
+ self.history.append(req)
161
+ self.requests.remove(req)
162
+ count += 1
163
+ return count
164
+
165
+
166
+ def render_approval_request(
167
+ request: ApprovalRequest, console: Console, show_diff: bool = False
168
+ ) -> None:
169
+ """Render a single approval request."""
170
+ # Header with gradient effect
171
+ header = Text()
172
+
173
+ # Icon based on type
174
+ if request.command:
175
+ icon = APPROVAL_ICONS["command"]
176
+ type_label = "Command"
177
+ else:
178
+ icon = APPROVAL_ICONS["file"]
179
+ type_label = "File Change"
180
+
181
+ # Danger indicator
182
+ danger_icons = ["", "", APPROVAL_ICONS["warning"], APPROVAL_ICONS["danger"]]
183
+ danger_icon = danger_icons[min(request.danger_level, 3)]
184
+
185
+ header.append(f" {icon} ", style="bold")
186
+ header.append(type_label, style="bold white")
187
+ if danger_icon:
188
+ header.append(f" {danger_icon}", style="")
189
+
190
+ # Status badge
191
+ status_colors = {
192
+ ApprovalAction.PENDING: ("⏳ Pending", APPROVAL_COLORS["pending"]),
193
+ ApprovalAction.APPROVED: ("✅ Approved", APPROVAL_COLORS["approve"]),
194
+ ApprovalAction.REJECTED: ("❌ Rejected", APPROVAL_COLORS["reject"]),
195
+ }
196
+ status_text, status_color = status_colors.get(
197
+ request.action, ("❓ Unknown", APPROVAL_COLORS["muted"])
198
+ )
199
+ header.append(" ", style="")
200
+ header.append(f"[{status_text}]", style=f"bold {status_color}")
201
+
202
+ # Build content
203
+ content = Text()
204
+ content.append(f"\n{request.title}\n", style="bold white")
205
+
206
+ if request.description:
207
+ content.append(f"{request.description}\n", style=APPROVAL_COLORS["muted"])
208
+
209
+ if request.file_path:
210
+ content.append(f"\n📁 ", style="")
211
+ content.append(request.file_path, style="bold cyan")
212
+ content.append("\n", style="")
213
+
214
+ if request.command:
215
+ content.append(f"\n💻 ", style="")
216
+ content.append(request.command, style="bold yellow")
217
+ content.append("\n", style="")
218
+
219
+ # Action hints
220
+ content.append("\n", style="")
221
+ content.append(" [A]", style=f"bold {APPROVAL_COLORS['approve']}")
222
+ content.append(" Approve ", style=APPROVAL_COLORS["approve"])
223
+ content.append("[Shift+A]", style=f"bold {APPROVAL_COLORS['approve']}")
224
+ content.append(" Always ", style="dim")
225
+ content.append("[R]", style=f"bold {APPROVAL_COLORS['reject']}")
226
+ content.append(" Reject ", style=APPROVAL_COLORS["reject"])
227
+ content.append("[Shift+R]", style=f"bold {APPROVAL_COLORS['reject']}")
228
+ content.append(" Never ", style="dim")
229
+ content.append("[V]", style="bold cyan")
230
+ content.append(" View Diff", style="cyan")
231
+
232
+ # Determine border color based on danger level
233
+ border_colors = ["#22c55e", "#f59e0b", "#f97316", "#ef4444"]
234
+ border_color = border_colors[min(request.danger_level, 3)]
235
+
236
+ console.print(
237
+ Panel(
238
+ content,
239
+ title=header,
240
+ title_align="left",
241
+ border_style=border_color,
242
+ box=ROUNDED,
243
+ padding=(1, 2),
244
+ )
245
+ )
246
+
247
+
248
+ def render_approval_list(requests: List[ApprovalRequest], console: Console) -> None:
249
+ """Render a list of approval requests."""
250
+ if not requests:
251
+ console.print(" [dim]No pending approvals[/dim]")
252
+ return
253
+
254
+ # Summary header
255
+ pending = sum(1 for r in requests if r.action == ApprovalAction.PENDING)
256
+
257
+ header = Text()
258
+ header.append(f" {APPROVAL_ICONS['pending']} ", style="bold")
259
+ header.append(f"{pending} Pending Approval(s)", style="bold white")
260
+
261
+ console.print(
262
+ Panel(header, border_style=APPROVAL_COLORS["header"], box=ROUNDED, padding=(0, 1))
263
+ )
264
+ console.print()
265
+
266
+ # Render each request
267
+ for i, request in enumerate(requests):
268
+ if request.action == ApprovalAction.PENDING:
269
+ console.print(f" [dim]#{i + 1}[/dim]")
270
+ render_approval_request(request, console)
271
+ console.print()
272
+
273
+
274
+ def render_approval_summary(manager: ApprovalManager, console: Console) -> None:
275
+ """Render a summary of approval history."""
276
+ if not manager.history:
277
+ console.print(" [dim]No approval history[/dim]")
278
+ return
279
+
280
+ table = Table(
281
+ show_header=True,
282
+ header_style="bold magenta",
283
+ box=ROUNDED,
284
+ border_style=APPROVAL_COLORS["border"],
285
+ )
286
+
287
+ table.add_column("Status", width=10)
288
+ table.add_column("Type", width=8)
289
+ table.add_column("Target", style="cyan")
290
+ table.add_column("Time", style="dim", width=10)
291
+
292
+ for req in manager.history[-10:]: # Last 10
293
+ # Status
294
+ if req.action in (ApprovalAction.APPROVED, ApprovalAction.APPROVED_ALWAYS):
295
+ status = f"[green]✅ Yes[/green]"
296
+ else:
297
+ status = f"[red]❌ No[/red]"
298
+
299
+ # Type
300
+ req_type = "Cmd" if req.command else "File"
301
+
302
+ # Target
303
+ target = req.command[:30] if req.command else (req.file_path or "Unknown")
304
+ if len(target) > 30:
305
+ target = target[:27] + "..."
306
+
307
+ # Time
308
+ time_str = req.timestamp.strftime("%H:%M:%S")
309
+
310
+ table.add_row(status, req_type, target, time_str)
311
+
312
+ console.print(table)
superqode/atomic.py ADDED
@@ -0,0 +1,296 @@
1
+ """
2
+ SuperQode Atomic File Operations - Safe File Writing
3
+
4
+ Provides atomic file operations to prevent data corruption:
5
+ - Writes to temp file first, then renames
6
+ - Supports undo/rollback
7
+ - Tracks file history for recovery
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import shutil
14
+ import tempfile
15
+ from dataclasses import dataclass, field
16
+ from datetime import datetime
17
+ from pathlib import Path
18
+ from typing import Optional, List, Dict
19
+
20
+
21
+ class AtomicWriteError(Exception):
22
+ """An atomic write operation failed."""
23
+
24
+ pass
25
+
26
+
27
+ class AtomicReadError(Exception):
28
+ """An atomic read operation failed."""
29
+
30
+ pass
31
+
32
+
33
+ @dataclass
34
+ class FileVersion:
35
+ """A version of a file for history tracking."""
36
+
37
+ path: str
38
+ content: str
39
+ timestamp: datetime = field(default_factory=datetime.now)
40
+ operation: str = "write" # "write", "create", "delete", "modify"
41
+ backup_path: Optional[str] = None
42
+
43
+
44
+ @dataclass
45
+ class FileChange:
46
+ """A pending file change."""
47
+
48
+ path: str
49
+ old_content: Optional[str]
50
+ new_content: str
51
+ operation: str = "write"
52
+
53
+
54
+ class AtomicFileManager:
55
+ """Manages atomic file operations with history and undo support."""
56
+
57
+ def __init__(self, project_dir: str = "."):
58
+ self.project_dir = Path(project_dir).resolve()
59
+ self.history: List[FileVersion] = []
60
+ self.pending_changes: List[FileChange] = []
61
+ self.max_history = 50
62
+ self._backup_dir: Optional[Path] = None
63
+
64
+ @property
65
+ def backup_dir(self) -> Path:
66
+ """Get or create the backup directory."""
67
+ if self._backup_dir is None:
68
+ self._backup_dir = self.project_dir / ".superqode" / "backups"
69
+ self._backup_dir.mkdir(parents=True, exist_ok=True)
70
+ return self._backup_dir
71
+
72
+ def read(self, path: str) -> str:
73
+ """Read a file safely."""
74
+ file_path = self._resolve_path(path)
75
+ try:
76
+ with open(file_path, "r", encoding="utf-8") as f:
77
+ return f.read()
78
+ except FileNotFoundError:
79
+ raise AtomicReadError(f"File not found: {path}")
80
+ except Exception as e:
81
+ raise AtomicReadError(f"Failed to read {path}: {e}")
82
+
83
+ def write(self, path: str, content: str, create_backup: bool = True) -> FileVersion:
84
+ """Write a file atomically with optional backup."""
85
+ file_path = self._resolve_path(path)
86
+ dir_path = file_path.parent
87
+
88
+ # Read existing content for backup
89
+ old_content = None
90
+ if file_path.exists() and create_backup:
91
+ try:
92
+ old_content = self.read(path)
93
+ except AtomicReadError:
94
+ pass
95
+
96
+ # Create directory if needed
97
+ dir_path.mkdir(parents=True, exist_ok=True)
98
+
99
+ # Create backup if file exists
100
+ backup_path = None
101
+ if old_content is not None:
102
+ backup_path = self._create_backup(path, old_content)
103
+
104
+ # Write to temp file first
105
+ try:
106
+ with tempfile.NamedTemporaryFile(
107
+ mode="w",
108
+ encoding="utf-8",
109
+ delete=False,
110
+ dir=str(dir_path),
111
+ prefix=f".{file_path.name}_tmp_",
112
+ suffix=".tmp",
113
+ ) as tmp_file:
114
+ tmp_file.write(content)
115
+ temp_path = tmp_file.name
116
+ except (OSError, IOError) as e:
117
+ raise AtomicWriteError(f"Failed to create temp file for {path}: {e}")
118
+
119
+ # Atomic rename
120
+ try:
121
+ os.replace(temp_path, str(file_path))
122
+ except OSError as e:
123
+ # Clean up temp file
124
+ try:
125
+ os.unlink(temp_path)
126
+ except OSError:
127
+ pass # Best effort cleanup
128
+ raise AtomicWriteError(f"Failed to write {path}: {e}")
129
+
130
+ # Record in history
131
+ operation = "create" if old_content is None else "modify"
132
+ version = FileVersion(
133
+ path=path, content=old_content or "", operation=operation, backup_path=backup_path
134
+ )
135
+ self._add_to_history(version)
136
+
137
+ return version
138
+
139
+ def delete(self, path: str, create_backup: bool = True) -> FileVersion:
140
+ """Delete a file with backup."""
141
+ file_path = self._resolve_path(path)
142
+
143
+ if not file_path.exists():
144
+ raise AtomicWriteError(f"File not found: {path}")
145
+
146
+ # Read content for backup
147
+ old_content = self.read(path)
148
+
149
+ # Create backup
150
+ backup_path = None
151
+ if create_backup:
152
+ backup_path = self._create_backup(path, old_content)
153
+
154
+ # Delete file
155
+ try:
156
+ os.unlink(str(file_path))
157
+ except Exception as e:
158
+ raise AtomicWriteError(f"Failed to delete {path}: {e}")
159
+
160
+ # Record in history
161
+ version = FileVersion(
162
+ path=path, content=old_content, operation="delete", backup_path=backup_path
163
+ )
164
+ self._add_to_history(version)
165
+
166
+ return version
167
+
168
+ def undo(self) -> Optional[FileVersion]:
169
+ """Undo the last file operation."""
170
+ if not self.history:
171
+ return None
172
+
173
+ version = self.history.pop()
174
+ file_path = self._resolve_path(version.path)
175
+
176
+ if version.operation == "create":
177
+ # Undo create = delete the file
178
+ if file_path.exists():
179
+ os.unlink(str(file_path))
180
+ elif version.operation == "delete":
181
+ # Undo delete = restore from backup
182
+ if version.backup_path and Path(version.backup_path).exists():
183
+ shutil.copy2(version.backup_path, str(file_path))
184
+ elif version.content:
185
+ self.write(version.path, version.content, create_backup=False)
186
+ elif version.operation == "modify":
187
+ # Undo modify = restore previous content
188
+ if version.backup_path and Path(version.backup_path).exists():
189
+ shutil.copy2(version.backup_path, str(file_path))
190
+ elif version.content:
191
+ self.write(version.path, version.content, create_backup=False)
192
+
193
+ return version
194
+
195
+ def stage_change(self, path: str, new_content: str) -> FileChange:
196
+ """Stage a file change without applying it."""
197
+ old_content = None
198
+ try:
199
+ old_content = self.read(path)
200
+ except AtomicReadError:
201
+ pass
202
+
203
+ change = FileChange(
204
+ path=path,
205
+ old_content=old_content,
206
+ new_content=new_content,
207
+ operation="create" if old_content is None else "modify",
208
+ )
209
+ self.pending_changes.append(change)
210
+ return change
211
+
212
+ def apply_staged(self) -> List[FileVersion]:
213
+ """Apply all staged changes."""
214
+ versions = []
215
+ for change in self.pending_changes:
216
+ version = self.write(change.path, change.new_content)
217
+ versions.append(version)
218
+ self.pending_changes.clear()
219
+ return versions
220
+
221
+ def discard_staged(self) -> int:
222
+ """Discard all staged changes."""
223
+ count = len(self.pending_changes)
224
+ self.pending_changes.clear()
225
+ return count
226
+
227
+ def get_history(self, limit: int = 10) -> List[FileVersion]:
228
+ """Get recent file history."""
229
+ return self.history[-limit:]
230
+
231
+ def _resolve_path(self, path: str) -> Path:
232
+ """Resolve a path relative to project directory."""
233
+ p = Path(path)
234
+ if p.is_absolute():
235
+ return p
236
+ return self.project_dir / p
237
+
238
+ def _create_backup(self, path: str, content: str) -> str:
239
+ """Create a backup of file content."""
240
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
241
+ safe_name = Path(path).name.replace("/", "_").replace("\\", "_")
242
+ backup_name = f"{safe_name}.{timestamp}.bak"
243
+ backup_path = self.backup_dir / backup_name
244
+
245
+ with open(backup_path, "w", encoding="utf-8") as f:
246
+ f.write(content)
247
+
248
+ return str(backup_path)
249
+
250
+ def _add_to_history(self, version: FileVersion) -> None:
251
+ """Add a version to history, maintaining max size."""
252
+ self.history.append(version)
253
+ if len(self.history) > self.max_history:
254
+ # Remove oldest entries
255
+ self.history = self.history[-self.max_history :]
256
+
257
+
258
+ # Convenience functions
259
+ def atomic_write(path: str, content: str) -> None:
260
+ """Write a file atomically (simple interface)."""
261
+ file_path = Path(path).resolve()
262
+ dir_path = file_path.parent
263
+
264
+ dir_path.mkdir(parents=True, exist_ok=True)
265
+
266
+ try:
267
+ with tempfile.NamedTemporaryFile(
268
+ mode="w",
269
+ encoding="utf-8",
270
+ delete=False,
271
+ dir=str(dir_path),
272
+ prefix=f".{file_path.name}_tmp_",
273
+ suffix=".tmp",
274
+ ) as tmp_file:
275
+ tmp_file.write(content)
276
+ temp_path = tmp_file.name
277
+ except Exception as e:
278
+ raise AtomicWriteError(f"Failed to create temp file: {e}")
279
+
280
+ try:
281
+ os.replace(temp_path, str(file_path))
282
+ except OSError as e:
283
+ try:
284
+ os.unlink(temp_path)
285
+ except OSError:
286
+ pass # Best effort cleanup
287
+ raise AtomicWriteError(f"Failed to write file: {e}")
288
+
289
+
290
+ def atomic_read(path: str) -> str:
291
+ """Read a file safely."""
292
+ try:
293
+ with open(path, "r", encoding="utf-8") as f:
294
+ return f.read()
295
+ except Exception as e:
296
+ raise AtomicReadError(f"Failed to read file: {e}")
@@ -0,0 +1 @@
1
+ """SuperQode CLI commands package."""