ayechat-dev 0.36.9.20260204003405__py3-none-any.whl → 0.36.9.20260204011001__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.
@@ -145,6 +145,25 @@ def handle_debug_command(tokens: list):
145
145
  rprint(f"[yellow]Debug mode is {current.title()}[/]")
146
146
 
147
147
 
148
+ def handle_autodiff_command(tokens: list):
149
+ """Handle the 'autodiff' command for toggling automatic diff display.
150
+
151
+ When autodiff is enabled, diffs are automatically displayed for every
152
+ file modified by an LLM response.
153
+ """
154
+ if len(tokens) > 1:
155
+ val = tokens[1].lower()
156
+ if val in ("on", "off"):
157
+ set_user_config("autodiff", val)
158
+ rprint(f"[green]Autodiff set to {val.title()}[/]")
159
+ else:
160
+ rprint("[red]Usage: autodiff on|off[/]")
161
+ else:
162
+ current = get_user_config("autodiff", "off")
163
+ rprint(f"[yellow]Autodiff is {current.title()}[/]")
164
+ rprint("[dim]When on, diffs are shown automatically after each LLM file update.[/]")
165
+
166
+
148
167
  def handle_completion_command(tokens: list) -> Optional[str]:
149
168
  """Handle the 'completion' command for switching completion styles.
150
169
 
@@ -264,9 +283,9 @@ def handle_llm_command(session: Optional[PromptSession], tokens: list[str]) -> N
264
283
 
265
284
  # Show status message
266
285
  if final_url and final_key:
267
- rprint("\n[green] OpenAI-compatible endpoint is configured and active.[/]")
286
+ rprint("\n[green] OpenAI-compatible endpoint is configured and active.[/]")
268
287
  else:
269
- rprint("\n[yellow] Both URL and KEY are required for the local LLM endpoint to be active.[/]")
288
+ rprint("\n[yellow] Both URL and KEY are required for the local LLM endpoint to be active.[/]")
270
289
 
271
290
 
272
291
  def _expand_file_patterns(patterns: list[str], conf: Any) -> list[str]:
@@ -1,5 +1,5 @@
1
1
  from pathlib import Path
2
- from typing import Any, Optional
2
+ from typing import Any, Optional, List
3
3
 
4
4
  from rich import print as rprint
5
5
  from rich.console import Console
@@ -11,10 +11,12 @@ from aye.presenter.repl_ui import (
11
11
  print_files_updated,
12
12
  print_error
13
13
  )
14
- from aye.model.snapshot import apply_updates
14
+ from aye.presenter import diff_presenter
15
+ from aye.model.snapshot import apply_updates, get_diff_base_for_file
15
16
  from aye.model.file_processor import filter_unchanged_files, make_paths_relative
16
17
  from aye.model.models import LLMResponse
17
18
  from aye.model.auth import get_user_config
19
+ from aye.model.autodiff_config import is_autodiff_enabled
18
20
  from aye.model.write_validator import (
19
21
  check_files_against_ignore_patterns,
20
22
  is_strict_mode_enabled,
@@ -52,6 +54,51 @@ def _maybe_print_restore_tip(conf: Any, console: Console) -> None:
52
54
  console.print(Padding(msg, (0, 4, 0, 4)))
53
55
 
54
56
 
57
+ def _run_autodiff(updated_files: List[dict], batch_id: str, conf: Any, console: Console) -> None:
58
+ """Display diffs for all updated files against their snapshot versions.
59
+
60
+ Args:
61
+ updated_files: List of file dicts with 'file_name' keys
62
+ batch_id: The batch identifier from apply_updates()
63
+ conf: Configuration object with root path
64
+ console: Rich console for output
65
+ """
66
+ verbose = getattr(conf, 'verbose', False)
67
+ debug = get_user_config("debug", "off").lower() == "on"
68
+
69
+ console.print(Padding("[dim]───── Auto-diff (autodiff=on) ─────[/]", (1, 0, 0, 0)))
70
+
71
+ for item in updated_files:
72
+ file_name = item.get("file_name")
73
+ if not file_name:
74
+ continue
75
+
76
+ file_path = Path(file_name)
77
+
78
+ # Get the snapshot reference for this file
79
+ diff_base = get_diff_base_for_file(batch_id, file_path)
80
+
81
+ if diff_base is None:
82
+ if verbose or debug:
83
+ rprint(f"[yellow]Warning: Could not find snapshot for {file_name}, skipping autodiff[/]")
84
+ continue
85
+
86
+ snapshot_ref, is_git_ref = diff_base
87
+
88
+ # Print file header
89
+ console.print(f"\n[bold cyan]{file_name}[/]")
90
+
91
+ try:
92
+ # show_diff expects: (current_file, snapshot_ref, is_stash_ref)
93
+ # For autodiff, we diff the current (new) file against the snapshot (old)
94
+ diff_presenter.show_diff(file_path, snapshot_ref, is_stash_ref=is_git_ref)
95
+ except Exception as e:
96
+ if verbose or debug:
97
+ rprint(f"[yellow]Warning: Could not show diff for {file_name}: {e}[/]")
98
+
99
+ console.print(Padding("[dim]───── End auto-diff ─────[/]", (1, 0, 0, 0)))
100
+
101
+
55
102
  def process_llm_response(
56
103
  response: LLMResponse,
57
104
  conf: Any,
@@ -118,12 +165,17 @@ def process_llm_response(
118
165
  else:
119
166
  # Apply updates to the model (Model update)
120
167
  try:
121
- apply_updates(updated_files, prompt)
168
+ batch_id = apply_updates(updated_files, prompt)
122
169
  file_names = [item.get("file_name") for item in updated_files if "file_name" in item]
123
170
  if file_names:
124
171
  # Update the view
125
172
  print_files_updated(console, file_names)
126
173
  _maybe_print_restore_tip(conf, console)
174
+
175
+ # Run autodiff if enabled
176
+ if is_autodiff_enabled():
177
+ _run_autodiff(updated_files, batch_id, conf, console)
178
+
127
179
  except Exception as e:
128
180
  rprint(f"[red]Error applying updates:[/] {e}")
129
181
 
aye/controller/repl.py CHANGED
@@ -42,6 +42,7 @@ from aye.controller.command_handlers import (
42
42
  handle_with_command,
43
43
  handle_blog_command,
44
44
  handle_llm_command,
45
+ handle_autodiff_command,
45
46
  )
46
47
 
47
48
  DEBUG = False
@@ -285,7 +286,7 @@ def _execute_forced_shell_command(command: str, args: List[str], conf: Any) -> N
285
286
  def chat_repl(conf: Any) -> None:
286
287
  is_first_run = run_first_time_tutorial_if_needed()
287
288
 
288
- BUILTIN_COMMANDS = ["with", "blog", "new", "history", "diff", "restore", "undo", "keep", "model", "verbose", "debug", "completion", "exit", "quit", ":q", "help", "cd", "db", "llm"]
289
+ BUILTIN_COMMANDS = ["with", "blog", "new", "history", "diff", "restore", "undo", "keep", "model", "verbose", "debug", "autodiff", "completion", "exit", "quit", ":q", "help", "cd", "db", "llm"]
289
290
 
290
291
  # Get the completion style setting
291
292
  completion_style = get_user_config("completion_style", "readline").lower()
@@ -416,6 +417,9 @@ def chat_repl(conf: Any) -> None:
416
417
  elif lowered_first == "debug":
417
418
  telemetry.record_command("debug", has_args=len(tokens) > 1, prefix=_AYE_PREFIX)
418
419
  handle_debug_command(tokens)
420
+ elif lowered_first == "autodiff":
421
+ telemetry.record_command("autodiff", has_args=len(tokens) > 1, prefix=_AYE_PREFIX)
422
+ handle_autodiff_command(tokens)
419
423
  elif lowered_first == "completion":
420
424
  telemetry.record_command("completion", has_args=len(tokens) > 1, prefix=_AYE_PREFIX)
421
425
  new_style = handle_completion_command(tokens)
@@ -0,0 +1,32 @@
1
+ """Autodiff configuration for automatic diff display after LLM changes.
2
+
3
+ This module provides functionality to check if autodiff mode is enabled.
4
+ When enabled, diffs are automatically displayed for every file modified
5
+ by an LLM response.
6
+
7
+ See: autodiff.md for the full design plan.
8
+ """
9
+
10
+ from aye.model.auth import get_user_config
11
+
12
+
13
+ # Config key for autodiff mode
14
+ AUTODIFF_KEY = "autodiff"
15
+
16
+
17
+ def is_autodiff_enabled() -> bool:
18
+ """Check if autodiff mode is enabled.
19
+
20
+ When enabled, diffs are automatically displayed for every file
21
+ modified by an LLM response, immediately after the optimistic
22
+ write is applied.
23
+
24
+ Can be set via:
25
+ - Environment variable: AYE_AUTODIFF=on
26
+ - Config file (~/.ayecfg): autodiff=on
27
+
28
+ Returns:
29
+ True if autodiff mode is enabled, False otherwise (default)
30
+ """
31
+ value = get_user_config(AUTODIFF_KEY, "off")
32
+ return str(value).lower() in ("on", "true", "1", "yes")
@@ -7,6 +7,7 @@ Note:
7
7
  - Git stash snapshots (GitStashBackend) are intentionally NOT used.
8
8
  """
9
9
 
10
+ import json
10
11
  import subprocess
11
12
  from pathlib import Path
12
13
  from typing import Dict, List, Optional, Tuple, Union
@@ -32,6 +33,7 @@ __all__ = [
32
33
  "delete_snapshot",
33
34
  "prune_snapshots",
34
35
  "cleanup_snapshots",
36
+ "get_diff_base_for_file",
35
37
  # Utilities
36
38
  "get_backend",
37
39
  "reset_backend",
@@ -145,6 +147,102 @@ def cleanup_snapshots(older_than_days: int = 30) -> int:
145
147
  return get_backend().cleanup_snapshots(older_than_days)
146
148
 
147
149
 
150
+ def get_diff_base_for_file(batch_id: str, file_path: Path) -> Optional[Tuple[str, bool]]:
151
+ """Return the snapshot reference for a file in a given batch.
152
+
153
+ This provides a backend-agnostic way to get a reference suitable for
154
+ diff_presenter.show_diff().
155
+
156
+ Args:
157
+ batch_id: The batch identifier returned by apply_updates()
158
+ file_path: The file path to get the snapshot reference for
159
+
160
+ Returns:
161
+ Tuple of (snapshot_ref, is_git_ref) where:
162
+ - snapshot_ref: For FileBasedBackend, a filesystem path to the snapshot file.
163
+ For GitRefBackend, a 'refname:repo_rel_path' string.
164
+ - is_git_ref: True if snapshot_ref is a git ref format, False for filesystem path.
165
+ Returns None if the file is not found in the snapshot.
166
+ """
167
+ backend = get_backend()
168
+
169
+ if isinstance(backend, FileBasedBackend):
170
+ return _get_diff_base_file_backend(backend, batch_id, file_path)
171
+ elif isinstance(backend, GitRefBackend):
172
+ return _get_diff_base_git_backend(backend, batch_id, file_path)
173
+
174
+ return None
175
+
176
+
177
+ def _get_diff_base_file_backend(
178
+ backend: FileBasedBackend, batch_id: str, file_path: Path
179
+ ) -> Optional[Tuple[str, bool]]:
180
+ """Get diff base for FileBasedBackend.
181
+
182
+ Reads the metadata.json from the snapshot batch directory to find
183
+ the snapshot path for the given file.
184
+ """
185
+ # Find the batch directory matching the batch_id
186
+ # batch_id format is like "001_20231201T120000"
187
+ batch_dir = None
188
+ if backend.snap_root.is_dir():
189
+ for dir_path in backend.snap_root.iterdir():
190
+ if dir_path.is_dir() and dir_path.name == batch_id:
191
+ batch_dir = dir_path
192
+ break
193
+
194
+ if batch_dir is None:
195
+ return None
196
+
197
+ meta_file = batch_dir / "metadata.json"
198
+ if not meta_file.is_file():
199
+ return None
200
+
201
+ try:
202
+ meta = json.loads(meta_file.read_text(encoding="utf-8"))
203
+ except (json.JSONDecodeError, OSError):
204
+ return None
205
+
206
+ # Resolve the target file path for comparison
207
+ file_resolved = file_path.resolve()
208
+
209
+ # Find the matching entry in metadata
210
+ for entry in meta.get("files", []):
211
+ original_path = entry.get("original")
212
+ snapshot_path = entry.get("snapshot")
213
+
214
+ if not original_path or not snapshot_path:
215
+ continue
216
+
217
+ if Path(original_path).resolve() == file_resolved:
218
+ # Return the snapshot path as a string, not a git ref
219
+ return (snapshot_path, False)
220
+
221
+ return None
222
+
223
+
224
+ def _get_diff_base_git_backend(
225
+ backend: GitRefBackend, batch_id: str, file_path: Path
226
+ ) -> Optional[Tuple[str, bool]]:
227
+ """Get diff base for GitRefBackend.
228
+
229
+ Returns a ref:path format suitable for git show.
230
+ """
231
+ # Construct the refname from batch_id
232
+ refname = f"{backend.REF_NAMESPACE}/{batch_id}"
233
+
234
+ # Get repo-relative path
235
+ try:
236
+ file_resolved = file_path.resolve()
237
+ rel_path = file_resolved.relative_to(backend.git_root).as_posix()
238
+ except ValueError:
239
+ # File is outside git root
240
+ return None
241
+
242
+ # Return the git ref format
243
+ return (f"{refname}:{rel_path}", True)
244
+
245
+
148
246
  # ------------------------------------------------------------------
149
247
  # Legacy helper functions (for backward compatibility with tests)
150
248
  # ------------------------------------------------------------------
aye/presenter/repl_ui.py CHANGED
@@ -88,14 +88,15 @@ def print_help_message():
88
88
  # Some commands are intentionally undocumented: keep them as such.
89
89
  ("@filename", "Include a file in your prompt inline (e.g., \"explain @main.py\"). Supports wildcards (e.g., @*.py, @src/*.js)."),
90
90
  ("!command", "Force shell execution (e.g., \"!echo hello\")."),
91
+ ("new", "Start a new chat session (if you want to change the subject)"),
92
+ (r"restore, undo \[id] \[file]", "Revert changes to the last state, a specific snapshot `id`, or for a single `file`."),
93
+ ("history", "Show snapshot history"),
94
+ (r"diff <file> \[snapshot_id]", "Show diff of file with the latest snapshot, or a specified snapshot"),
91
95
  ("model", "Select a different model. Selection will persist between sessions."),
96
+ (r"autodiff \[on|off]", "Toggle automatic diff display after LLM file updates (off by default, persists between sessions)"),
92
97
  ("llm", "Configure OpenAI-compatible LLM endpoint (URL, key, model). Use 'llm clear' to reset."),
93
98
  (r"verbose \[on|off]", "Toggle verbose mode to increase or decrease chattiness (on/off, persists between sessions)"),
94
99
  (r"completion \[readline|multi]", "Switch auto-completion style (readline or multi, persists between sessions)"),
95
- ("new", "Start a new chat session (if you want to change the subject)"),
96
- ("history", "Show snapshot history"),
97
- (r"diff <file> \[snapshot_id]", "Show diff of file with the latest snapshot, or a specified snapshot"),
98
- (r"restore, undo \[id] \[file]", "Revert changes to the last state, a specific snapshot `id`, or for a single `file`."),
99
100
  ("keep [N]", "Keep only N most recent snapshots (10 by default)"),
100
101
  ("exit, quit, Ctrl+D", "Exit the chat session"),
101
102
  ("help", "Show this help message"),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ayechat-dev
3
- Version: 0.36.9.20260204003405
3
+ Version: 0.36.9.20260204011001
4
4
  Summary: Aye Chat: Terminal-first AI Code Generator
5
5
  Author-email: "Acrotron, Inc." <info@acrotron.com>
6
6
  License: MIT
@@ -3,18 +3,19 @@ aye/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  aye/__main__.py,sha256=fj7pl0i_mLvpKAbpKtotU3zboLBTivsILSLKuH5M5Sg,5377
4
4
  aye/__main_chat__.py,sha256=R6RaidxG3Px5TaYxcoWAuIleE5KUZlceneUB6u_9UVU,1066
5
5
  aye/controller/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- aye/controller/command_handlers.py,sha256=72dA9x4vHOxsSuWb1wEX2xTxL4uCFuiSPL4-VpEPwyA,16728
6
+ aye/controller/command_handlers.py,sha256=S_HOYY91lbLkGtCkby28T8qUFPNtfy-CbOGn9VGZtTo,17446
7
7
  aye/controller/commands.py,sha256=sXmK_sgNBrw9Fs7mKcr93-wsu740ZlvWSisQfS-1EUE,12278
8
- aye/controller/llm_handler.py,sha256=BSBab6onF9BYiFndYW1647eEy377Vt52trzu0Qjm4bQ,5075
8
+ aye/controller/llm_handler.py,sha256=gY3X2rHcvhPp8iqp_Vor7QpbwHltmcm_Uu9MWF1Z598,7120
9
9
  aye/controller/llm_invoker.py,sha256=p_Vk2a3YrWKwDupLfSVRinR5llDfq1Fb_f7WrYozK6M,14127
10
10
  aye/controller/plugin_manager.py,sha256=9ZuITyA5sQJJJU-IntLQ1SsxXsDnbgZKPOF4e9VmsEU,3018
11
- aye/controller/repl.py,sha256=fTYEdX1PAds49pRBVgvyi-4lIfArR8gjBTp_shwx-K0,26782
11
+ aye/controller/repl.py,sha256=nSlzAHD8MQlQ7vnpBTLVzA1jQ8jkNjxUnUoqE31chxY,27028
12
12
  aye/controller/tutorial.py,sha256=lc92jOcJOYCVrrjTEF0Suk4-8jn-ku98kTJEIL8taUA,7254
13
13
  aye/controller/util.py,sha256=gBmktDEaY63OKhgzZHA2IFrgcWUN_Iphn3e1daEeUBI,2828
14
14
  aye/model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
15
  aye/model/api.py,sha256=HhSMQQ_szdC2ZPOSfNsJRbs1FRwb6WyYIeLejB2ScbA,13272
16
16
  aye/model/ast_chunker.py,sha256=rVcDdynVUXXyxWVgtUcsee_STqB7SAwP776ktWTlYig,4462
17
17
  aye/model/auth.py,sha256=ozV_uQxdqXtUoWO3nZwpzVnDOIfnRAmSMC6W0N724vE,4800
18
+ aye/model/autodiff_config.py,sha256=b8pyudkJFYXF5JWPxft0bH5uazeCab9i-A11dNt1d7U,931
18
19
  aye/model/config.py,sha256=o6bQhj5gqhSqtWD6DLow7NNy6Hdaede02h_xb7uPLXo,9280
19
20
  aye/model/download_plugins.py,sha256=6omyFGdxlEIb7tKPLq4rRVrRYeCPUUCE8aZHvJAKGSc,4442
20
21
  aye/model/file_processor.py,sha256=b7YGvHAmhGto9JbtzcfrsdkFtksHbosYt-42EnR22Uo,2131
@@ -34,7 +35,7 @@ aye/model/index_manager/index_manager_executor.py,sha256=rSI8OX9bFld5ta-ajfT8eWU
34
35
  aye/model/index_manager/index_manager_file_ops.py,sha256=petgGU0ZtXOvLkHAQuzSbzqNIew5HygBOAy4yG6rTlk,7656
35
36
  aye/model/index_manager/index_manager_state.py,sha256=LflyC8UbE8fT7Tyag1ueh7TBisPa15cRhs0JI1-jVEw,17587
36
37
  aye/model/index_manager/index_manager_utils.py,sha256=B4NCYuScxQcG46WoUEx8mxfWKDjTlrSf3coRQCydiBM,4615
37
- aye/model/snapshot/__init__.py,sha256=Lqq-W5RBGxu3_7O5DzW_qY5iOF9MhogA46BAqEdu6Mw,6958
38
+ aye/model/snapshot/__init__.py,sha256=XyEaYF0WS0wuGZI9t2vcA1UUxWbwj5CEkwKFYUSQwKc,10152
38
39
  aye/model/snapshot/base.py,sha256=5fzxM_85avxHwocv-w6PwlxDPzvMdTbuoUE_9P6D844,2009
39
40
  aye/model/snapshot/file_backend.py,sha256=hTlqaBCiMe9YHFZs7gcyXMVRTsR0pY23LQN15D0ZOY0,10757
40
41
  aye/model/snapshot/git_ref_backend.py,sha256=mY4yJiZ0ER4pgMzCgiTPQiKoVGCgWravNrPFue_zwLw,21356
@@ -50,12 +51,12 @@ aye/plugins/slash_completer.py,sha256=MyrDTC_KwVWhtD_kpHbu0WjSjmSAWp36PwOBQczSuX
50
51
  aye/presenter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
52
  aye/presenter/cli_ui.py,sha256=8oHqQiMHO8hHXCTdqWoquMkJBshl2_3-YWN6SQnlbKg,8449
52
53
  aye/presenter/diff_presenter.py,sha256=cbxfOEqGomPTDvQpKdybfYeNUD2DYVAl85j1uy5--Ww,12512
53
- aye/presenter/repl_ui.py,sha256=5nAv8qLo3azDuoGYAxGdK2SEwowXPoHSEruupvS6jy8,8023
54
+ aye/presenter/repl_ui.py,sha256=PVENlAQM_tm_k2dANsmQH6I8ATMVXhrdj_hNzc38pSw,8156
54
55
  aye/presenter/streaming_ui.py,sha256=_3tBEuNH9UQ9Gyq2yuvRfX4SWVkcGMYirEUGj-MXVJ0,12768
55
56
  aye/presenter/ui_utils.py,sha256=6KXR4_ZZZUdF5pCHrPqO8yywlQk7AOzWe-2B4Wj_-ZQ,5441
56
- ayechat_dev-0.36.9.20260204003405.dist-info/licenses/LICENSE,sha256=U1ou6lkMKmPo16-E9YowIu3goU7sOWKUprGo0AOA72s,1065
57
- ayechat_dev-0.36.9.20260204003405.dist-info/METADATA,sha256=EHTmVsIzzf2ki5cb1N9-x4eWLHLFbp3eWxtU3pUKe4g,7718
58
- ayechat_dev-0.36.9.20260204003405.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
59
- ayechat_dev-0.36.9.20260204003405.dist-info/entry_points.txt,sha256=KGsOma6szoefNN6vHozg3Pbf1fjZ7ZbmwrOiVwBd0Ik,41
60
- ayechat_dev-0.36.9.20260204003405.dist-info/top_level.txt,sha256=7WZL0LOx4-GKKvgU1mtI5s4Dhk2OdieVZZvVnxFJHr8,4
61
- ayechat_dev-0.36.9.20260204003405.dist-info/RECORD,,
57
+ ayechat_dev-0.36.9.20260204011001.dist-info/licenses/LICENSE,sha256=U1ou6lkMKmPo16-E9YowIu3goU7sOWKUprGo0AOA72s,1065
58
+ ayechat_dev-0.36.9.20260204011001.dist-info/METADATA,sha256=c6g026ZNTxo1IUHsgXnRCWdyJZnX_ddUTwwo4i4mvtc,7718
59
+ ayechat_dev-0.36.9.20260204011001.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
60
+ ayechat_dev-0.36.9.20260204011001.dist-info/entry_points.txt,sha256=KGsOma6szoefNN6vHozg3Pbf1fjZ7ZbmwrOiVwBd0Ik,41
61
+ ayechat_dev-0.36.9.20260204011001.dist-info/top_level.txt,sha256=7WZL0LOx4-GKKvgU1mtI5s4Dhk2OdieVZZvVnxFJHr8,4
62
+ ayechat_dev-0.36.9.20260204011001.dist-info/RECORD,,