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,429 @@
1
+ """
2
+ Diff Tracker - Track file changes during QE for patch generation.
3
+
4
+ Inspired by EveryCode's turn_diff_tracker.rs implementation.
5
+
6
+ Features:
7
+ - Capture baseline snapshots before modifications
8
+ - Generate unified diffs comparing baseline to current
9
+ - Support add, delete, update, rename/move operations
10
+ - Git-compatible diff format for easy review
11
+
12
+ Usage:
13
+ tracker = DiffTracker(project_root)
14
+
15
+ # Before modifying a file
16
+ tracker.capture_baseline(Path("src/main.py"))
17
+
18
+ # After QE session
19
+ patch = tracker.get_unified_diff()
20
+ print(patch)
21
+ """
22
+
23
+ import difflib
24
+ import hashlib
25
+ import os
26
+ from dataclasses import dataclass, field
27
+ from enum import Enum
28
+ from pathlib import Path
29
+ from typing import Any, Dict, List, Optional, Tuple
30
+ import logging
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ class ChangeType(Enum):
36
+ """Type of file change."""
37
+
38
+ ADD = "add"
39
+ DELETE = "delete"
40
+ MODIFY = "modify"
41
+ RENAME = "rename"
42
+
43
+
44
+ @dataclass
45
+ class FileBaseline:
46
+ """Baseline state of a file."""
47
+
48
+ original_path: Path
49
+ content: Optional[bytes] # None = file didn't exist
50
+ mode: int # File mode (permissions)
51
+ oid: str # Content hash (git-style blob SHA)
52
+
53
+ @property
54
+ def exists(self) -> bool:
55
+ return self.content is not None
56
+
57
+
58
+ @dataclass
59
+ class FileChange:
60
+ """Tracked change to a file."""
61
+
62
+ change_type: ChangeType
63
+ original_path: Path
64
+ current_path: Path # May differ for renames
65
+ baseline: FileBaseline
66
+
67
+ # For display
68
+ original_display: str = ""
69
+ current_display: str = ""
70
+
71
+
72
+ class DiffTracker:
73
+ """
74
+ Track file changes during a QE session for patch generation.
75
+
76
+ Maintains baseline snapshots of files before first modification,
77
+ then generates unified diffs comparing baseline to current state.
78
+ """
79
+
80
+ ZERO_OID = "0" * 40
81
+ DEV_NULL = "/dev/null"
82
+
83
+ def __init__(self, project_root: Path):
84
+ self.project_root = project_root.resolve()
85
+
86
+ # Baseline snapshots: path -> baseline
87
+ self._baselines: Dict[Path, FileBaseline] = {}
88
+
89
+ # Path mappings for renames: original -> current
90
+ self._path_mappings: Dict[Path, Path] = {}
91
+
92
+ # Git root for relative paths
93
+ self._git_root: Optional[Path] = None
94
+
95
+ @property
96
+ def git_root(self) -> Path:
97
+ """Find git root for relative path display."""
98
+ if self._git_root is None:
99
+ current = self.project_root
100
+ while current != current.parent:
101
+ if (current / ".git").exists():
102
+ self._git_root = current
103
+ break
104
+ current = current.parent
105
+
106
+ if self._git_root is None:
107
+ self._git_root = self.project_root
108
+
109
+ return self._git_root
110
+
111
+ def capture_baseline(self, file_path: Path) -> None:
112
+ """
113
+ Capture the baseline state of a file before modification.
114
+
115
+ Call this before any file operation (write, delete, rename).
116
+ """
117
+ abs_path = self._resolve_path(file_path)
118
+
119
+ # Only capture first time
120
+ if abs_path in self._baselines:
121
+ return
122
+
123
+ if abs_path.exists():
124
+ try:
125
+ content = abs_path.read_bytes()
126
+ mode = self._get_file_mode(abs_path)
127
+ oid = self._compute_blob_oid(content)
128
+ except (OSError, IOError) as e:
129
+ logger.warning(f"Failed to capture baseline for {file_path}: {e}")
130
+ content = None
131
+ mode = 0o644
132
+ oid = self.ZERO_OID
133
+ else:
134
+ # File doesn't exist - will be treated as add
135
+ content = None
136
+ mode = 0o644
137
+ oid = self.ZERO_OID
138
+
139
+ self._baselines[abs_path] = FileBaseline(
140
+ original_path=abs_path,
141
+ content=content,
142
+ mode=mode,
143
+ oid=oid,
144
+ )
145
+
146
+ # Initialize path mapping
147
+ self._path_mappings[abs_path] = abs_path
148
+
149
+ def record_rename(self, old_path: Path, new_path: Path) -> None:
150
+ """Record a file rename/move."""
151
+ old_abs = self._resolve_path(old_path)
152
+ new_abs = self._resolve_path(new_path)
153
+
154
+ # Ensure baseline is captured
155
+ if old_abs not in self._baselines:
156
+ self.capture_baseline(old_path)
157
+
158
+ # Update path mapping
159
+ self._path_mappings[old_abs] = new_abs
160
+
161
+ def get_unified_diff(self) -> Optional[str]:
162
+ """
163
+ Generate a unified diff of all changes.
164
+
165
+ Returns:
166
+ Git-format unified diff string, or None if no changes
167
+ """
168
+ changes = self._compute_changes()
169
+
170
+ if not changes:
171
+ return None
172
+
173
+ # Sort by path for deterministic output
174
+ changes.sort(key=lambda c: str(c.original_path))
175
+
176
+ diff_parts = []
177
+ for change in changes:
178
+ diff = self._generate_file_diff(change)
179
+ if diff:
180
+ diff_parts.append(diff)
181
+
182
+ if not diff_parts:
183
+ return None
184
+
185
+ return "\n".join(diff_parts)
186
+
187
+ def get_changes_summary(self) -> Dict[str, Any]:
188
+ """Get a summary of all tracked changes."""
189
+ changes = self._compute_changes()
190
+
191
+ adds = [c for c in changes if c.change_type == ChangeType.ADD]
192
+ deletes = [c for c in changes if c.change_type == ChangeType.DELETE]
193
+ modifies = [c for c in changes if c.change_type == ChangeType.MODIFY]
194
+ renames = [c for c in changes if c.change_type == ChangeType.RENAME]
195
+
196
+ return {
197
+ "total_changes": len(changes),
198
+ "additions": len(adds),
199
+ "deletions": len(deletes),
200
+ "modifications": len(modifies),
201
+ "renames": len(renames),
202
+ "files_added": [str(c.current_path) for c in adds],
203
+ "files_deleted": [str(c.original_path) for c in deletes],
204
+ "files_modified": [str(c.current_path) for c in modifies],
205
+ "files_renamed": [(str(c.original_path), str(c.current_path)) for c in renames],
206
+ }
207
+
208
+ def _resolve_path(self, file_path: Path) -> Path:
209
+ """Resolve to absolute path."""
210
+ if file_path.is_absolute():
211
+ return file_path
212
+ return self.project_root / file_path
213
+
214
+ def _relative_path(self, abs_path: Path) -> str:
215
+ """Get path relative to git root for display."""
216
+ try:
217
+ return str(abs_path.relative_to(self.git_root))
218
+ except ValueError:
219
+ return str(abs_path)
220
+
221
+ def _compute_changes(self) -> List[FileChange]:
222
+ """Compute all file changes from baselines to current state."""
223
+ changes = []
224
+
225
+ for original_path, baseline in self._baselines.items():
226
+ current_path = self._path_mappings.get(original_path, original_path)
227
+
228
+ # Determine change type
229
+ current_exists = current_path.exists()
230
+ baseline_exists = baseline.exists
231
+ is_rename = original_path != current_path
232
+
233
+ if not baseline_exists and current_exists:
234
+ change_type = ChangeType.ADD
235
+ elif baseline_exists and not current_exists:
236
+ change_type = ChangeType.DELETE
237
+ elif is_rename:
238
+ change_type = ChangeType.RENAME
239
+ else:
240
+ # Check if content changed
241
+ if current_exists:
242
+ try:
243
+ current_content = current_path.read_bytes()
244
+ if current_content == baseline.content:
245
+ continue # No change
246
+ except (OSError, IOError):
247
+ continue
248
+ change_type = ChangeType.MODIFY
249
+
250
+ changes.append(
251
+ FileChange(
252
+ change_type=change_type,
253
+ original_path=original_path,
254
+ current_path=current_path,
255
+ baseline=baseline,
256
+ original_display=self._relative_path(original_path),
257
+ current_display=self._relative_path(current_path),
258
+ )
259
+ )
260
+
261
+ return changes
262
+
263
+ def _generate_file_diff(self, change: FileChange) -> str:
264
+ """Generate unified diff for a single file change."""
265
+ lines = []
266
+
267
+ # Git diff header
268
+ a_path = f"a/{change.original_display}"
269
+ b_path = f"b/{change.current_display}"
270
+
271
+ lines.append(f"diff --git {a_path} {b_path}")
272
+
273
+ # Handle different change types
274
+ if change.change_type == ChangeType.ADD:
275
+ current_mode = self._get_file_mode(change.current_path)
276
+ lines.append(f"new file mode {current_mode:o}")
277
+
278
+ current_content = self._read_file_safe(change.current_path)
279
+ current_oid = (
280
+ self._compute_blob_oid(current_content) if current_content else self.ZERO_OID
281
+ )
282
+
283
+ lines.append(f"index {self.ZERO_OID}..{current_oid}")
284
+ lines.append(f"--- {self.DEV_NULL}")
285
+ lines.append(f"+++ {b_path}")
286
+
287
+ if current_content:
288
+ lines.extend(self._text_diff("", current_content.decode("utf-8", errors="replace")))
289
+
290
+ elif change.change_type == ChangeType.DELETE:
291
+ lines.append(f"deleted file mode {change.baseline.mode:o}")
292
+ lines.append(f"index {change.baseline.oid}..{self.ZERO_OID}")
293
+ lines.append(f"--- {a_path}")
294
+ lines.append(f"+++ {self.DEV_NULL}")
295
+
296
+ if change.baseline.content:
297
+ lines.extend(
298
+ self._text_diff(change.baseline.content.decode("utf-8", errors="replace"), "")
299
+ )
300
+
301
+ else: # MODIFY or RENAME
302
+ current_content = self._read_file_safe(change.current_path)
303
+ current_oid = (
304
+ self._compute_blob_oid(current_content) if current_content else self.ZERO_OID
305
+ )
306
+ current_mode = self._get_file_mode(change.current_path)
307
+
308
+ # Mode change
309
+ if change.baseline.mode != current_mode:
310
+ lines.append(f"old mode {change.baseline.mode:o}")
311
+ lines.append(f"new mode {current_mode:o}")
312
+
313
+ lines.append(f"index {change.baseline.oid}..{current_oid}")
314
+ lines.append(f"--- {a_path}")
315
+ lines.append(f"+++ {b_path}")
316
+
317
+ # Content diff
318
+ old_text = ""
319
+ new_text = ""
320
+
321
+ if change.baseline.content:
322
+ old_text = change.baseline.content.decode("utf-8", errors="replace")
323
+ if current_content:
324
+ new_text = current_content.decode("utf-8", errors="replace")
325
+
326
+ lines.extend(self._text_diff(old_text, new_text))
327
+
328
+ return "\n".join(lines)
329
+
330
+ def _text_diff(self, old_text: str, new_text: str) -> List[str]:
331
+ """Generate unified diff hunks for text content."""
332
+ old_lines = old_text.splitlines(keepends=True)
333
+ new_lines = new_text.splitlines(keepends=True)
334
+
335
+ diff = difflib.unified_diff(
336
+ old_lines,
337
+ new_lines,
338
+ lineterm="",
339
+ )
340
+
341
+ # Skip the header lines (--- and +++)
342
+ result = []
343
+ for i, line in enumerate(diff):
344
+ if i < 2: # Skip header
345
+ continue
346
+ # Remove trailing newline for clean output
347
+ result.append(line.rstrip("\n\r"))
348
+
349
+ return result
350
+
351
+ def _read_file_safe(self, file_path: Path) -> Optional[bytes]:
352
+ """Safely read file content."""
353
+ try:
354
+ if file_path.exists():
355
+ return file_path.read_bytes()
356
+ except (OSError, IOError):
357
+ pass
358
+ return None
359
+
360
+ def _get_file_mode(self, file_path: Path) -> int:
361
+ """Get file mode (permissions)."""
362
+ try:
363
+ stat = file_path.stat()
364
+ # Check if executable
365
+ if stat.st_mode & 0o111:
366
+ return 0o100755
367
+ return 0o100644
368
+ except (OSError, IOError):
369
+ return 0o100644
370
+
371
+ def _compute_blob_oid(self, content: bytes) -> str:
372
+ """Compute git-style blob SHA-1."""
373
+ # Git blob format: "blob <size>\0<content>"
374
+ header = f"blob {len(content)}\0".encode()
375
+ data = header + content
376
+ return hashlib.sha1(data).hexdigest()
377
+
378
+ def clear(self) -> None:
379
+ """Clear all tracked baselines."""
380
+ self._baselines.clear()
381
+ self._path_mappings.clear()
382
+
383
+
384
+ class DiffTrackerContext:
385
+ """Context manager for automatic diff tracking."""
386
+
387
+ def __init__(self, project_root: Path):
388
+ self.tracker = DiffTracker(project_root)
389
+
390
+ def __enter__(self) -> DiffTracker:
391
+ return self.tracker
392
+
393
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
394
+ pass # Tracker is preserved for getting diff after context
395
+
396
+
397
+ def generate_patch_file(
398
+ project_root: Path,
399
+ tracker: DiffTracker,
400
+ output_path: Optional[Path] = None,
401
+ ) -> Optional[Path]:
402
+ """
403
+ Generate a patch file from tracked changes.
404
+
405
+ Args:
406
+ project_root: Project root directory
407
+ tracker: DiffTracker with captured changes
408
+ output_path: Optional output path for patch file
409
+
410
+ Returns:
411
+ Path to generated patch file, or None if no changes
412
+ """
413
+ diff = tracker.get_unified_diff()
414
+
415
+ if not diff:
416
+ return None
417
+
418
+ if output_path is None:
419
+ from datetime import datetime
420
+
421
+ timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
422
+ output_path = (
423
+ project_root / ".superqode" / "qe-artifacts" / "patches" / f"qe-{timestamp}.patch"
424
+ )
425
+
426
+ output_path.parent.mkdir(parents=True, exist_ok=True)
427
+ output_path.write_text(diff)
428
+
429
+ return output_path