patch-chat 0.1.0__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 (146) hide show
  1. patch/__init__.py +20 -0
  2. patch/__main__.py +4 -0
  3. patch/_version.py +24 -0
  4. patch/analytics.py +260 -0
  5. patch/args.py +955 -0
  6. patch/args_formatter.py +228 -0
  7. patch/coders/__init__.py +32 -0
  8. patch/coders/architect_coder.py +47 -0
  9. patch/coders/architect_prompts.py +40 -0
  10. patch/coders/ask_coder.py +9 -0
  11. patch/coders/ask_prompts.py +41 -0
  12. patch/coders/base_coder.py +2548 -0
  13. patch/coders/base_prompts.py +60 -0
  14. patch/coders/chat_chunks.py +64 -0
  15. patch/coders/context_coder.py +53 -0
  16. patch/coders/context_prompts.py +75 -0
  17. patch/coders/edit_apply.py +6 -0
  18. patch/coders/editblock_coder.py +620 -0
  19. patch/coders/editblock_fenced_coder.py +10 -0
  20. patch/coders/editblock_fenced_prompts.py +143 -0
  21. patch/coders/editblock_func_coder.py +141 -0
  22. patch/coders/editblock_func_prompts.py +27 -0
  23. patch/coders/editblock_prompts.py +172 -0
  24. patch/coders/editor_diff_fenced_coder.py +9 -0
  25. patch/coders/editor_diff_fenced_prompts.py +11 -0
  26. patch/coders/editor_editblock_coder.py +8 -0
  27. patch/coders/editor_editblock_prompts.py +18 -0
  28. patch/coders/editor_whole_coder.py +8 -0
  29. patch/coders/editor_whole_prompts.py +10 -0
  30. patch/coders/patch_coder.py +731 -0
  31. patch/coders/patch_prompts.py +159 -0
  32. patch/coders/search_replace.py +757 -0
  33. patch/coders/shell.py +37 -0
  34. patch/coders/single_wholefile_func_coder.py +102 -0
  35. patch/coders/single_wholefile_func_prompts.py +27 -0
  36. patch/coders/udiff_coder.py +419 -0
  37. patch/coders/udiff_prompts.py +113 -0
  38. patch/coders/udiff_simple.py +14 -0
  39. patch/coders/udiff_simple_prompts.py +25 -0
  40. patch/coders/wholefile_coder.py +144 -0
  41. patch/coders/wholefile_func_coder.py +134 -0
  42. patch/coders/wholefile_func_prompts.py +27 -0
  43. patch/coders/wholefile_prompts.py +64 -0
  44. patch/commands.py +1668 -0
  45. patch/copypaste.py +70 -0
  46. patch/deprecated.py +126 -0
  47. patch/diffs.py +128 -0
  48. patch/dump.py +29 -0
  49. patch/editor.py +147 -0
  50. patch/exceptions.py +113 -0
  51. patch/format_settings.py +47 -0
  52. patch/gui.py +545 -0
  53. patch/history.py +143 -0
  54. patch/io.py +1209 -0
  55. patch/linter.py +304 -0
  56. patch/llm.py +47 -0
  57. patch/main.py +1466 -0
  58. patch/mdstream.py +243 -0
  59. patch/models.py +1342 -0
  60. patch/onboarding.py +428 -0
  61. patch/openrouter.py +128 -0
  62. patch/prompts.py +61 -0
  63. patch/queries/tree-sitter-language-pack/README.md +7 -0
  64. patch/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
  65. patch/queries/tree-sitter-language-pack/bash-tags.scm +8 -0
  66. patch/queries/tree-sitter-language-pack/c-tags.scm +9 -0
  67. patch/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
  68. patch/queries/tree-sitter-language-pack/clojure-tags.scm +7 -0
  69. patch/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
  70. patch/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
  71. patch/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
  72. patch/queries/tree-sitter-language-pack/d-tags.scm +26 -0
  73. patch/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
  74. patch/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
  75. patch/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
  76. patch/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
  77. patch/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
  78. patch/queries/tree-sitter-language-pack/go-tags.scm +42 -0
  79. patch/queries/tree-sitter-language-pack/java-tags.scm +20 -0
  80. patch/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
  81. patch/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
  82. patch/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
  83. patch/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
  84. patch/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
  85. patch/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
  86. patch/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
  87. patch/queries/tree-sitter-language-pack/python-tags.scm +14 -0
  88. patch/queries/tree-sitter-language-pack/r-tags.scm +21 -0
  89. patch/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
  90. patch/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
  91. patch/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
  92. patch/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
  93. patch/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
  94. patch/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
  95. patch/queries/tree-sitter-languages/README.md +24 -0
  96. patch/queries/tree-sitter-languages/bash-tags.scm +8 -0
  97. patch/queries/tree-sitter-languages/c-tags.scm +9 -0
  98. patch/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
  99. patch/queries/tree-sitter-languages/cpp-tags.scm +15 -0
  100. patch/queries/tree-sitter-languages/dart-tags.scm +91 -0
  101. patch/queries/tree-sitter-languages/elisp-tags.scm +8 -0
  102. patch/queries/tree-sitter-languages/elixir-tags.scm +54 -0
  103. patch/queries/tree-sitter-languages/elm-tags.scm +19 -0
  104. patch/queries/tree-sitter-languages/fortran-tags.scm +15 -0
  105. patch/queries/tree-sitter-languages/go-tags.scm +30 -0
  106. patch/queries/tree-sitter-languages/haskell-tags.scm +3 -0
  107. patch/queries/tree-sitter-languages/hcl-tags.scm +77 -0
  108. patch/queries/tree-sitter-languages/java-tags.scm +20 -0
  109. patch/queries/tree-sitter-languages/javascript-tags.scm +88 -0
  110. patch/queries/tree-sitter-languages/julia-tags.scm +60 -0
  111. patch/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
  112. patch/queries/tree-sitter-languages/matlab-tags.scm +10 -0
  113. patch/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
  114. patch/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
  115. patch/queries/tree-sitter-languages/php-tags.scm +26 -0
  116. patch/queries/tree-sitter-languages/python-tags.scm +12 -0
  117. patch/queries/tree-sitter-languages/ql-tags.scm +26 -0
  118. patch/queries/tree-sitter-languages/ruby-tags.scm +64 -0
  119. patch/queries/tree-sitter-languages/rust-tags.scm +60 -0
  120. patch/queries/tree-sitter-languages/scala-tags.scm +65 -0
  121. patch/queries/tree-sitter-languages/typescript-tags.scm +41 -0
  122. patch/queries/tree-sitter-languages/zig-tags.scm +3 -0
  123. patch/reasoning_tags.py +82 -0
  124. patch/repo.py +612 -0
  125. patch/repomap.py +859 -0
  126. patch/report.py +200 -0
  127. patch/resources/__init__.py +3 -0
  128. patch/resources/model-metadata.json +715 -0
  129. patch/resources/model-settings.yml +3128 -0
  130. patch/run_cmd.py +146 -0
  131. patch/scrape.py +316 -0
  132. patch/sendchat.py +61 -0
  133. patch/special.py +203 -0
  134. patch/urls.py +17 -0
  135. patch/utils.py +365 -0
  136. patch/versioncheck.py +113 -0
  137. patch/voice.py +200 -0
  138. patch/waiting.py +222 -0
  139. patch/watch.py +318 -0
  140. patch/watch_prompts.py +12 -0
  141. patch_chat-0.1.0.dist-info/METADATA +375 -0
  142. patch_chat-0.1.0.dist-info/RECORD +146 -0
  143. patch_chat-0.1.0.dist-info/WHEEL +5 -0
  144. patch_chat-0.1.0.dist-info/entry_points.txt +2 -0
  145. patch_chat-0.1.0.dist-info/licenses/LICENSE.txt +202 -0
  146. patch_chat-0.1.0.dist-info/top_level.txt +1 -0
patch/watch.py ADDED
@@ -0,0 +1,318 @@
1
+ import re
2
+ import threading
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ from grep_ast import TreeContext
7
+ from pathspec import PathSpec
8
+ from pathspec.patterns import GitWildMatchPattern
9
+ from watchfiles import watch
10
+
11
+ from patch.dump import dump # noqa
12
+ from patch.watch_prompts import watch_ask_prompt, watch_code_prompt
13
+
14
+
15
+ def load_gitignores(gitignore_paths: list[Path]) -> Optional[PathSpec]:
16
+ """Load and parse multiple .gitignore files into a single PathSpec"""
17
+ if not gitignore_paths:
18
+ return None
19
+
20
+ patterns = [
21
+ ".patch*",
22
+ ".git",
23
+ # Common editor backup/temp files
24
+ "*~", # Emacs/vim backup
25
+ "*.bak", # Generic backup
26
+ "*.swp", # Vim swap
27
+ "*.swo", # Vim swap
28
+ "\\#*\\#", # Emacs auto-save
29
+ ".#*", # Emacs lock files
30
+ "*.tmp", # Generic temp files
31
+ "*.temp", # Generic temp files
32
+ "*.orig", # Merge conflict originals
33
+ "*.pyc", # Python bytecode
34
+ "__pycache__/", # Python cache dir
35
+ ".DS_Store", # macOS metadata
36
+ "Thumbs.db", # Windows thumbnail cache
37
+ "*.svg",
38
+ "*.pdf",
39
+ # IDE files
40
+ ".idea/", # JetBrains IDEs
41
+ ".vscode/", # VS Code
42
+ "*.sublime-*", # Sublime Text
43
+ ".project", # Eclipse
44
+ ".settings/", # Eclipse
45
+ "*.code-workspace", # VS Code workspace
46
+ # Environment files
47
+ ".env", # Environment variables
48
+ ".venv/", # Python virtual environments
49
+ "node_modules/", # Node.js dependencies
50
+ "vendor/", # Various dependencies
51
+ # Logs and caches
52
+ "*.log", # Log files
53
+ ".cache/", # Cache directories
54
+ ".pytest_cache/", # Python test cache
55
+ "coverage/", # Code coverage reports
56
+ ] # Always ignore
57
+ for path in gitignore_paths:
58
+ if path.exists():
59
+ with open(path) as f:
60
+ patterns.extend(f.readlines())
61
+
62
+ return PathSpec.from_lines(GitWildMatchPattern, patterns) if patterns else None
63
+
64
+
65
+ class FileWatcher:
66
+ """Watches source files for changes and AI comments"""
67
+
68
+ # Compiled regex pattern for AI comments
69
+ ai_comment_pattern = re.compile(
70
+ r"(?:#|//|--|;+) *(ai\b.*|ai\b.*|.*\bai[?!]?) *$", re.IGNORECASE
71
+ )
72
+
73
+ def __init__(self, coder, gitignores=None, verbose=False, analytics=None, root=None):
74
+ self.coder = coder
75
+ self.io = coder.io
76
+ self.root = Path(root) if root else Path(coder.root)
77
+ self.verbose = verbose
78
+ self.analytics = analytics
79
+ self.stop_event = None
80
+ self.watcher_thread = None
81
+ self.changed_files = set()
82
+ self.gitignores = gitignores
83
+
84
+ self.gitignore_spec = load_gitignores(
85
+ [Path(g) for g in self.gitignores] if self.gitignores else []
86
+ )
87
+
88
+ coder.io.file_watcher = self
89
+
90
+ def filter_func(self, change_type, path):
91
+ """Filter function for the file watcher"""
92
+ path_obj = Path(path)
93
+ path_abs = path_obj.absolute()
94
+
95
+ if not path_abs.is_relative_to(self.root.absolute()):
96
+ return False
97
+
98
+ rel_path = path_abs.relative_to(self.root)
99
+ if self.verbose:
100
+ print("Changed", rel_path)
101
+
102
+ if self.gitignore_spec and self.gitignore_spec.match_file(
103
+ rel_path.as_posix() + ("/" if path_abs.is_dir() else "")
104
+ ):
105
+ return False
106
+
107
+ # Check file size before reading content
108
+ if path_abs.is_file() and path_abs.stat().st_size > 1 * 1024 * 1024: # 1MB limit
109
+ return False
110
+
111
+ if self.verbose:
112
+ print("Checking", rel_path)
113
+
114
+ # Check if file contains AI markers
115
+ try:
116
+ comments, _, _ = self.get_ai_comments(str(path_abs))
117
+ return bool(comments)
118
+ except Exception:
119
+ return
120
+
121
+ def get_roots_to_watch(self):
122
+ """Determine which root paths to watch based on gitignore rules"""
123
+ if self.gitignore_spec:
124
+ roots = [
125
+ str(path)
126
+ for path in self.root.iterdir()
127
+ if not self.gitignore_spec.match_file(
128
+ path.relative_to(self.root).as_posix() + ("/" if path.is_dir() else "")
129
+ )
130
+ ]
131
+ # Fallback to watching root if all top-level items are filtered out
132
+ return roots if roots else [str(self.root)]
133
+ return [str(self.root)]
134
+
135
+ def handle_changes(self, changes):
136
+ """Process the detected changes and update state"""
137
+ if not changes:
138
+ return False
139
+
140
+ changed_files = {str(Path(change[1])) for change in changes}
141
+ self.changed_files.update(changed_files)
142
+ self.io.interrupt_input()
143
+ return True
144
+
145
+ def watch_files(self):
146
+ """Watch for file changes and process them"""
147
+ try:
148
+ roots_to_watch = self.get_roots_to_watch()
149
+
150
+ for changes in watch(
151
+ *roots_to_watch,
152
+ watch_filter=self.filter_func,
153
+ stop_event=self.stop_event,
154
+ ignore_permission_denied=True,
155
+ ):
156
+ if self.handle_changes(changes):
157
+ return
158
+
159
+ except Exception as e:
160
+ if self.verbose:
161
+ dump(f"File watcher error: {e}")
162
+ raise e
163
+
164
+ def start(self):
165
+ """Start watching for file changes"""
166
+ self.stop_event = threading.Event()
167
+ self.changed_files = set()
168
+
169
+ self.watcher_thread = threading.Thread(target=self.watch_files, daemon=True)
170
+ self.watcher_thread.start()
171
+
172
+ def stop(self):
173
+ """Stop watching for file changes"""
174
+ if self.stop_event:
175
+ self.stop_event.set()
176
+ if self.watcher_thread:
177
+ self.watcher_thread.join()
178
+ self.watcher_thread = None
179
+ self.stop_event = None
180
+
181
+ def process_changes(self):
182
+ """Get any detected file changes"""
183
+
184
+ has_action = None
185
+ added = False
186
+ for fname in self.changed_files:
187
+ _, _, action = self.get_ai_comments(fname)
188
+ if action in ("!", "?"):
189
+ has_action = action
190
+
191
+ if fname in self.coder.abs_fnames:
192
+ continue
193
+ if self.analytics:
194
+ self.analytics.event("ai-comments file-add")
195
+ self.coder.abs_fnames.add(fname)
196
+ rel_fname = self.coder.get_rel_fname(fname)
197
+ if not added:
198
+ self.io.tool_output()
199
+ added = True
200
+ self.io.tool_output(f"Added {rel_fname} to the chat")
201
+
202
+ if not has_action:
203
+ if added:
204
+ self.io.tool_output(
205
+ "End your comment with AI! to request changes or AI? to ask questions"
206
+ )
207
+ return ""
208
+
209
+ if self.analytics:
210
+ self.analytics.event("ai-comments execute")
211
+ self.io.tool_output("Processing your request...")
212
+
213
+ if has_action == "!":
214
+ res = watch_code_prompt
215
+ elif has_action == "?":
216
+ res = watch_ask_prompt
217
+
218
+ # Refresh all AI comments from tracked files
219
+ for fname in self.coder.abs_fnames:
220
+ line_nums, comments, _action = self.get_ai_comments(fname)
221
+ if not line_nums:
222
+ continue
223
+
224
+ code = self.io.read_text(fname)
225
+ if not code:
226
+ continue
227
+
228
+ rel_fname = self.coder.get_rel_fname(fname)
229
+ res += f"\n{rel_fname}:\n"
230
+
231
+ # Convert comment line numbers to line indices (0-based)
232
+ lois = [ln - 1 for ln, _ in zip(line_nums, comments) if ln > 0]
233
+
234
+ try:
235
+ context = TreeContext(
236
+ rel_fname,
237
+ code,
238
+ color=False,
239
+ line_number=False,
240
+ child_context=False,
241
+ last_line=False,
242
+ margin=0,
243
+ mark_lois=True,
244
+ loi_pad=3,
245
+ show_top_of_file_parent_scope=False,
246
+ )
247
+ context.lines_of_interest = set()
248
+ context.add_lines_of_interest(lois)
249
+ context.add_context()
250
+ res += context.format()
251
+ except ValueError:
252
+ for ln, comment in zip(line_nums, comments):
253
+ res += f" Line {ln}: {comment}\n"
254
+
255
+ return res
256
+
257
+ def get_ai_comments(self, filepath):
258
+ """Extract AI comment line numbers, comments and action status from a file"""
259
+ line_nums = []
260
+ comments = []
261
+ has_action = None # None, "!" or "?"
262
+ content = self.io.read_text(filepath, silent=True)
263
+ if not content:
264
+ return None, None, None
265
+
266
+ for i, line in enumerate(content.splitlines(), 1):
267
+ if match := self.ai_comment_pattern.search(line):
268
+ comment = match.group(0).strip()
269
+ if comment:
270
+ line_nums.append(i)
271
+ comments.append(comment)
272
+ comment = comment.lower()
273
+ comment = comment.lstrip("/#-;") # Added semicolon for Lisp comments
274
+ comment = comment.strip()
275
+ if comment.startswith("ai!") or comment.endswith("ai!"):
276
+ has_action = "!"
277
+ elif comment.startswith("ai?") or comment.endswith("ai?"):
278
+ has_action = "?"
279
+ if not line_nums:
280
+ return None, None, None
281
+ return line_nums, comments, has_action
282
+
283
+
284
+ def main():
285
+ """Example usage of the file watcher"""
286
+ import argparse
287
+
288
+ parser = argparse.ArgumentParser(description="Watch source files for changes")
289
+ parser.add_argument("directory", help="Directory to watch")
290
+ parser.add_argument(
291
+ "--gitignore",
292
+ action="append",
293
+ help="Path to .gitignore file (can be specified multiple times)",
294
+ )
295
+ args = parser.parse_args()
296
+
297
+ directory = args.directory
298
+ print(f"Watching source files in {directory}...")
299
+
300
+ # Example ignore function that ignores files with "test" in the name
301
+ def ignore_test_files(path):
302
+ return "test" in path.name.lower()
303
+
304
+ watcher = FileWatcher(directory, gitignores=args.gitignore)
305
+ try:
306
+ watcher.start()
307
+ while True:
308
+ if changes := watcher.get_changes():
309
+ for file in sorted(changes.keys()):
310
+ print(file)
311
+ watcher.changed_files = None
312
+ except KeyboardInterrupt:
313
+ print("\nStopped watching files")
314
+ watcher.stop()
315
+
316
+
317
+ if __name__ == "__main__":
318
+ main()
patch/watch_prompts.py ADDED
@@ -0,0 +1,12 @@
1
+ watch_code_prompt = """
2
+ I've written your instructions in comments in the code and marked them with "ai"
3
+ You can see the "AI" comments shown below (marked with █).
4
+ Find them in the code files I've shared with you, and follow their instructions.
5
+
6
+ After completing those instructions, also be sure to remove all the "AI" comments from the code too.
7
+ """
8
+
9
+ watch_ask_prompt = """/ask
10
+ Find the "AI" comments below (marked with █) in the code files I've shared with you.
11
+ They contain my questions that I need you to answer and other instructions for you.
12
+ """