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,574 @@
1
+ """
2
+ SuperQode Undo Manager - Git-Based Undo/Redo System.
3
+
4
+ Provides reliable undo/redo functionality using Git's object database
5
+ for tracking file changes. Each operation creates a checkpoint that
6
+ can be restored.
7
+
8
+ Features:
9
+ - Automatic checkpoint creation before agent operations
10
+ - Named checkpoints for easier navigation
11
+ - Restore specific files or entire state
12
+ - View diff between checkpoints
13
+ - Stack-based undo/redo
14
+
15
+ Usage:
16
+ from superqode.undo_manager import UndoManager
17
+
18
+ undo = UndoManager()
19
+
20
+ # Before agent operation
21
+ checkpoint_id = undo.create_checkpoint("Before edit")
22
+
23
+ # After operation, if user wants to undo
24
+ undo.undo()
25
+
26
+ # Or restore a specific checkpoint
27
+ undo.restore(checkpoint_id)
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import asyncio
33
+ import os
34
+ import subprocess
35
+ from dataclasses import dataclass, field
36
+ from datetime import datetime
37
+ from pathlib import Path
38
+ from typing import Dict, List, Optional, Tuple
39
+
40
+
41
+ # ============================================================================
42
+ # DATA CLASSES
43
+ # ============================================================================
44
+
45
+
46
+ @dataclass
47
+ class Checkpoint:
48
+ """A checkpoint representing a point in time."""
49
+
50
+ id: str # Git commit or stash reference
51
+ name: str
52
+ timestamp: datetime
53
+ message: str = ""
54
+ files_changed: List[str] = field(default_factory=list)
55
+ is_stash: bool = False
56
+
57
+
58
+ @dataclass
59
+ class FileChange:
60
+ """A file change between checkpoints."""
61
+
62
+ path: str
63
+ change_type: str # "added", "modified", "deleted"
64
+ old_content: str = ""
65
+ new_content: str = ""
66
+
67
+
68
+ # ============================================================================
69
+ # UNDO MANAGER
70
+ # ============================================================================
71
+
72
+
73
+ class UndoManager:
74
+ """
75
+ Git-based undo/redo manager.
76
+
77
+ Uses Git's stash and commit system to create reliable checkpoints
78
+ that can be restored.
79
+ """
80
+
81
+ def __init__(self, working_dir: Optional[Path] = None):
82
+ self.working_dir = working_dir or Path.cwd()
83
+ self._checkpoints: List[Checkpoint] = []
84
+ self._redo_stack: List[Checkpoint] = []
85
+ self._current_index: int = -1
86
+ self._initialized = False
87
+
88
+ # ========================================================================
89
+ # INITIALIZATION
90
+ # ========================================================================
91
+
92
+ def initialize(self) -> bool:
93
+ """
94
+ Initialize the undo manager.
95
+
96
+ Checks if we're in a git repo and sets up tracking.
97
+ Returns True if successful.
98
+ """
99
+ if self._initialized:
100
+ return True
101
+
102
+ # Check if git is available
103
+ try:
104
+ result = self._run_git(["rev-parse", "--git-dir"])
105
+ if result.returncode != 0:
106
+ return False
107
+
108
+ self._initialized = True
109
+ return True
110
+ except Exception:
111
+ return False
112
+
113
+ def _run_git(self, args: List[str], capture_output: bool = True) -> subprocess.CompletedProcess:
114
+ """Run a git command."""
115
+ cmd = ["git"] + args
116
+ return subprocess.run(
117
+ cmd,
118
+ cwd=str(self.working_dir),
119
+ capture_output=capture_output,
120
+ text=True,
121
+ )
122
+
123
+ # ========================================================================
124
+ # CHECKPOINT CREATION
125
+ # ========================================================================
126
+
127
+ def create_checkpoint(self, name: str = "", message: str = "") -> Optional[str]:
128
+ """
129
+ Create a checkpoint of the current state.
130
+
131
+ Uses git stash to save changes without affecting the working tree.
132
+ Returns the checkpoint ID if successful.
133
+ """
134
+ if not self.initialize():
135
+ return None
136
+
137
+ try:
138
+ # Get list of changed files
139
+ status = self._run_git(["status", "--porcelain"])
140
+ changed_files = []
141
+ for line in status.stdout.strip().split("\n"):
142
+ if line.strip():
143
+ # Parse status line: "XY filename"
144
+ parts = line.split(maxsplit=1)
145
+ if len(parts) >= 2:
146
+ changed_files.append(parts[1].strip('"'))
147
+
148
+ if not changed_files:
149
+ # No changes to checkpoint
150
+ return None
151
+
152
+ # Create a stash with all changes (including untracked)
153
+ stash_msg = f"superqode-checkpoint: {name or 'Checkpoint'}"
154
+ if message:
155
+ stash_msg += f" - {message}"
156
+
157
+ # Stage all changes including untracked
158
+ self._run_git(["add", "-A"])
159
+
160
+ # Create stash
161
+ result = self._run_git(["stash", "push", "-m", stash_msg, "--include-untracked"])
162
+
163
+ if result.returncode != 0:
164
+ # Unstage changes
165
+ self._run_git(["reset"])
166
+ return None
167
+
168
+ # Get the stash reference
169
+ stash_list = self._run_git(["stash", "list", "-1"])
170
+ if not stash_list.stdout.strip():
171
+ return None
172
+
173
+ # Parse stash reference (e.g., "stash@{0}: ...")
174
+ stash_ref = stash_list.stdout.split(":")[0].strip()
175
+
176
+ # Immediately restore working directory (we just want the checkpoint)
177
+ self._run_git(["stash", "pop", "--quiet"])
178
+
179
+ # Create checkpoint record
180
+ checkpoint = Checkpoint(
181
+ id=stash_ref,
182
+ name=name or f"Checkpoint {len(self._checkpoints) + 1}",
183
+ timestamp=datetime.now(),
184
+ message=message,
185
+ files_changed=changed_files,
186
+ is_stash=True,
187
+ )
188
+
189
+ # Clear redo stack when creating new checkpoint
190
+ self._redo_stack.clear()
191
+
192
+ self._checkpoints.append(checkpoint)
193
+ self._current_index = len(self._checkpoints) - 1
194
+
195
+ return checkpoint.id
196
+
197
+ except Exception:
198
+ return None
199
+
200
+ def create_commit_checkpoint(self, name: str = "", message: str = "") -> Optional[str]:
201
+ """
202
+ Create a checkpoint using a commit.
203
+
204
+ More permanent than stash-based checkpoints.
205
+ """
206
+ if not self.initialize():
207
+ return None
208
+
209
+ try:
210
+ # Get changed files
211
+ status = self._run_git(["status", "--porcelain"])
212
+ changed_files = []
213
+ for line in status.stdout.strip().split("\n"):
214
+ if line.strip():
215
+ parts = line.split(maxsplit=1)
216
+ if len(parts) >= 2:
217
+ changed_files.append(parts[1].strip('"'))
218
+
219
+ if not changed_files:
220
+ return None
221
+
222
+ # Stage all changes
223
+ self._run_git(["add", "-A"])
224
+
225
+ # Create commit
226
+ commit_msg = f"[superqode] {name or 'Checkpoint'}"
227
+ if message:
228
+ commit_msg += f": {message}"
229
+
230
+ result = self._run_git(["commit", "-m", commit_msg])
231
+ if result.returncode != 0:
232
+ return None
233
+
234
+ # Get commit hash
235
+ hash_result = self._run_git(["rev-parse", "HEAD"])
236
+ commit_hash = hash_result.stdout.strip()[:8]
237
+
238
+ checkpoint = Checkpoint(
239
+ id=commit_hash,
240
+ name=name or f"Checkpoint {len(self._checkpoints) + 1}",
241
+ timestamp=datetime.now(),
242
+ message=message,
243
+ files_changed=changed_files,
244
+ is_stash=False,
245
+ )
246
+
247
+ self._redo_stack.clear()
248
+ self._checkpoints.append(checkpoint)
249
+ self._current_index = len(self._checkpoints) - 1
250
+
251
+ return commit_hash
252
+
253
+ except Exception:
254
+ return None
255
+
256
+ # ========================================================================
257
+ # UNDO / REDO
258
+ # ========================================================================
259
+
260
+ def undo(self) -> Optional[Checkpoint]:
261
+ """
262
+ Undo to the previous checkpoint.
263
+
264
+ Returns the checkpoint that was restored, or None if nothing to undo.
265
+ """
266
+ if not self._checkpoints or self._current_index < 0:
267
+ return None
268
+
269
+ try:
270
+ # Save current state to redo stack
271
+ current_state = self._capture_current_state()
272
+ if current_state:
273
+ self._redo_stack.append(current_state)
274
+
275
+ # Get checkpoint to restore
276
+ checkpoint = self._checkpoints[self._current_index]
277
+
278
+ # Restore based on type
279
+ if checkpoint.is_stash:
280
+ # For stash-based, we need to reverse the changes
281
+ # This is tricky - we'll use git checkout
282
+ for file_path in checkpoint.files_changed:
283
+ self._run_git(["checkout", "HEAD", "--", file_path])
284
+ else:
285
+ # For commit-based, reset to previous commit
286
+ if self._current_index > 0:
287
+ prev_checkpoint = self._checkpoints[self._current_index - 1]
288
+ self._run_git(["reset", "--hard", prev_checkpoint.id])
289
+
290
+ self._current_index -= 1
291
+ return checkpoint
292
+
293
+ except Exception:
294
+ return None
295
+
296
+ def redo(self) -> Optional[Checkpoint]:
297
+ """
298
+ Redo the previously undone checkpoint.
299
+
300
+ Returns the checkpoint that was restored, or None if nothing to redo.
301
+ """
302
+ if not self._redo_stack:
303
+ return None
304
+
305
+ try:
306
+ checkpoint = self._redo_stack.pop()
307
+
308
+ # Apply the changes
309
+ for file_path in checkpoint.files_changed:
310
+ # This is simplified - full implementation would restore content
311
+ pass
312
+
313
+ self._current_index += 1
314
+ return checkpoint
315
+
316
+ except Exception:
317
+ return None
318
+
319
+ def _capture_current_state(self) -> Optional[Checkpoint]:
320
+ """Capture the current state as a checkpoint for redo."""
321
+ try:
322
+ status = self._run_git(["status", "--porcelain"])
323
+ changed_files = []
324
+ for line in status.stdout.strip().split("\n"):
325
+ if line.strip():
326
+ parts = line.split(maxsplit=1)
327
+ if len(parts) >= 2:
328
+ changed_files.append(parts[1].strip('"'))
329
+
330
+ return Checkpoint(
331
+ id="current",
332
+ name="Current state",
333
+ timestamp=datetime.now(),
334
+ files_changed=changed_files,
335
+ )
336
+ except Exception:
337
+ return None
338
+
339
+ # ========================================================================
340
+ # RESTORE
341
+ # ========================================================================
342
+
343
+ def restore(self, checkpoint_id: str) -> bool:
344
+ """
345
+ Restore to a specific checkpoint.
346
+
347
+ Returns True if successful.
348
+ """
349
+ if not self.initialize():
350
+ return False
351
+
352
+ # Find the checkpoint
353
+ checkpoint = None
354
+ index = -1
355
+ for i, cp in enumerate(self._checkpoints):
356
+ if cp.id == checkpoint_id:
357
+ checkpoint = cp
358
+ index = i
359
+ break
360
+
361
+ if not checkpoint:
362
+ return False
363
+
364
+ try:
365
+ if checkpoint.is_stash:
366
+ # Find and apply the stash
367
+ result = self._run_git(["stash", "apply", checkpoint.id])
368
+ return result.returncode == 0
369
+ else:
370
+ # Reset to commit
371
+ result = self._run_git(["reset", "--hard", checkpoint.id])
372
+ if result.returncode == 0:
373
+ self._current_index = index
374
+ return True
375
+ return False
376
+ except Exception:
377
+ return False
378
+
379
+ def restore_file(self, checkpoint_id: str, file_path: str) -> bool:
380
+ """
381
+ Restore a specific file from a checkpoint.
382
+
383
+ Returns True if successful.
384
+ """
385
+ if not self.initialize():
386
+ return False
387
+
388
+ # Find the checkpoint
389
+ checkpoint = None
390
+ for cp in self._checkpoints:
391
+ if cp.id == checkpoint_id:
392
+ checkpoint = cp
393
+ break
394
+
395
+ if not checkpoint:
396
+ return False
397
+
398
+ try:
399
+ if checkpoint.is_stash:
400
+ # Restore file from stash
401
+ result = self._run_git(["checkout", checkpoint.id, "--", file_path])
402
+ else:
403
+ # Restore file from commit
404
+ result = self._run_git(["checkout", checkpoint.id, "--", file_path])
405
+
406
+ return result.returncode == 0
407
+ except Exception:
408
+ return False
409
+
410
+ # ========================================================================
411
+ # QUERY
412
+ # ========================================================================
413
+
414
+ def get_checkpoints(self, limit: int = 20) -> List[Checkpoint]:
415
+ """Get list of checkpoints."""
416
+ return self._checkpoints[-limit:]
417
+
418
+ def get_current_checkpoint(self) -> Optional[Checkpoint]:
419
+ """Get the current checkpoint."""
420
+ if 0 <= self._current_index < len(self._checkpoints):
421
+ return self._checkpoints[self._current_index]
422
+ return None
423
+
424
+ def get_changes_since(self, checkpoint_id: str) -> List[FileChange]:
425
+ """Get list of changes since a checkpoint."""
426
+ if not self.initialize():
427
+ return []
428
+
429
+ try:
430
+ # Get diff
431
+ result = self._run_git(["diff", checkpoint_id, "--name-status"])
432
+
433
+ changes = []
434
+ for line in result.stdout.strip().split("\n"):
435
+ if not line.strip():
436
+ continue
437
+
438
+ parts = line.split("\t")
439
+ if len(parts) >= 2:
440
+ status = parts[0]
441
+ path = parts[1]
442
+
443
+ change_type = {
444
+ "A": "added",
445
+ "M": "modified",
446
+ "D": "deleted",
447
+ }.get(status[0], "modified")
448
+
449
+ changes.append(
450
+ FileChange(
451
+ path=path,
452
+ change_type=change_type,
453
+ )
454
+ )
455
+
456
+ return changes
457
+
458
+ except Exception:
459
+ return []
460
+
461
+ def get_file_diff(self, checkpoint_id: str, file_path: str) -> Tuple[str, str]:
462
+ """
463
+ Get the old and new content of a file relative to checkpoint.
464
+
465
+ Returns (old_content, new_content).
466
+ """
467
+ if not self.initialize():
468
+ return ("", "")
469
+
470
+ try:
471
+ # Get old content
472
+ old_result = self._run_git(["show", f"{checkpoint_id}:{file_path}"])
473
+ old_content = old_result.stdout if old_result.returncode == 0 else ""
474
+
475
+ # Get current content
476
+ file_path_obj = self.working_dir / file_path
477
+ if file_path_obj.exists():
478
+ new_content = file_path_obj.read_text(encoding="utf-8", errors="ignore")
479
+ else:
480
+ new_content = ""
481
+
482
+ return (old_content, new_content)
483
+
484
+ except Exception:
485
+ return ("", "")
486
+
487
+ def can_undo(self) -> bool:
488
+ """Check if undo is possible."""
489
+ return len(self._checkpoints) > 0 and self._current_index >= 0
490
+
491
+ def can_redo(self) -> bool:
492
+ """Check if redo is possible."""
493
+ return len(self._redo_stack) > 0
494
+
495
+ # ========================================================================
496
+ # CLEANUP
497
+ # ========================================================================
498
+
499
+ def clear_old_checkpoints(self, keep_count: int = 50) -> int:
500
+ """
501
+ Clear old checkpoints to save space.
502
+
503
+ Returns number of checkpoints cleared.
504
+ """
505
+ if len(self._checkpoints) <= keep_count:
506
+ return 0
507
+
508
+ to_remove = self._checkpoints[:-keep_count]
509
+ removed = 0
510
+
511
+ for checkpoint in to_remove:
512
+ if checkpoint.is_stash:
513
+ # Drop the stash
514
+ try:
515
+ self._run_git(["stash", "drop", checkpoint.id])
516
+ removed += 1
517
+ except Exception:
518
+ pass
519
+
520
+ self._checkpoints = self._checkpoints[-keep_count:]
521
+ self._current_index = min(self._current_index, len(self._checkpoints) - 1)
522
+
523
+ return removed
524
+
525
+
526
+ # ============================================================================
527
+ # ASYNC VERSION
528
+ # ============================================================================
529
+
530
+
531
+ class AsyncUndoManager:
532
+ """Async wrapper for UndoManager."""
533
+
534
+ def __init__(self, working_dir: Optional[Path] = None):
535
+ self._sync_manager = UndoManager(working_dir)
536
+
537
+ async def initialize(self) -> bool:
538
+ """Initialize the undo manager."""
539
+ loop = asyncio.get_event_loop()
540
+ return await loop.run_in_executor(None, self._sync_manager.initialize)
541
+
542
+ async def create_checkpoint(self, name: str = "", message: str = "") -> Optional[str]:
543
+ """Create a checkpoint."""
544
+ loop = asyncio.get_event_loop()
545
+ return await loop.run_in_executor(
546
+ None, lambda: self._sync_manager.create_checkpoint(name, message)
547
+ )
548
+
549
+ async def undo(self) -> Optional[Checkpoint]:
550
+ """Undo to previous checkpoint."""
551
+ loop = asyncio.get_event_loop()
552
+ return await loop.run_in_executor(None, self._sync_manager.undo)
553
+
554
+ async def redo(self) -> Optional[Checkpoint]:
555
+ """Redo previously undone checkpoint."""
556
+ loop = asyncio.get_event_loop()
557
+ return await loop.run_in_executor(None, self._sync_manager.redo)
558
+
559
+ async def restore(self, checkpoint_id: str) -> bool:
560
+ """Restore to a specific checkpoint."""
561
+ loop = asyncio.get_event_loop()
562
+ return await loop.run_in_executor(None, lambda: self._sync_manager.restore(checkpoint_id))
563
+
564
+
565
+ # ============================================================================
566
+ # EXPORTS
567
+ # ============================================================================
568
+
569
+ __all__ = [
570
+ "Checkpoint",
571
+ "FileChange",
572
+ "UndoManager",
573
+ "AsyncUndoManager",
574
+ ]
@@ -0,0 +1,5 @@
1
+ """SuperQode utilities."""
2
+
3
+ from superqode.utils.fuzzy import FuzzySearch
4
+
5
+ __all__ = ["FuzzySearch"]