auto-coder-web 0.1.81__py3-none-any.whl → 0.1.83__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 (35) hide show
  1. auto_coder_web/auto_coder_runner_wrapper.py +1 -2
  2. auto_coder_web/file_manager.py +9 -7
  3. auto_coder_web/init_project.py +9 -0
  4. auto_coder_web/proxy.py +2 -2
  5. auto_coder_web/routers/auto_router.py +4 -5
  6. auto_coder_web/routers/chat_router.py +4 -4
  7. auto_coder_web/routers/coding_router.py +3 -3
  8. auto_coder_web/routers/direct_chat_router.py +42 -0
  9. auto_coder_web/routers/rules_router.py +601 -0
  10. auto_coder_web/version.py +1 -1
  11. auto_coder_web/web/assets/HistoryPanel-B59ay4X7.js +1 -0
  12. auto_coder_web/web/assets/{cssMode-CQYz0o1d.js → cssMode-dfzmIRw_.js} +1 -1
  13. auto_coder_web/web/assets/{freemarker2-BJU3cSen.js → freemarker2-CD79_SV-.js} +1 -1
  14. auto_coder_web/web/assets/{handlebars-VxUdciXQ.js → handlebars-B_inQ7Ga.js} +2 -2
  15. auto_coder_web/web/assets/{html-DEgskwXL.js → html-s8IXsV2v.js} +1 -1
  16. auto_coder_web/web/assets/{htmlMode-58cXxJlI.js → htmlMode-BNdCO4Um.js} +1 -1
  17. auto_coder_web/web/assets/{javascript-DbZlm-ig.js → javascript-D_ZBRKl9.js} +1 -1
  18. auto_coder_web/web/assets/{jsonMode-C2HKgmE0.js → jsonMode-lWocgRYn.js} +1 -1
  19. auto_coder_web/web/assets/{liquid-BFft-XkQ.js → liquid-CPaH0eVF.js} +1 -1
  20. auto_coder_web/web/assets/{main-x88Ziufd.css → main-B3_hzhoO.css} +2 -2
  21. auto_coder_web/web/assets/main.js +298 -298
  22. auto_coder_web/web/assets/{mdx-DZdDhrJW.js → mdx-DRx1b0qs.js} +1 -1
  23. auto_coder_web/web/assets/{python-DOHUGzLU.js → python-5rTPwuda.js} +2 -2
  24. auto_coder_web/web/assets/{razor-Dh5mSHi2.js → razor-zn6WfQ9V.js} +1 -1
  25. auto_coder_web/web/assets/{tsMode-tyI6CeIR.js → tsMode-CkhihvQ0.js} +1 -1
  26. auto_coder_web/web/assets/{typescript-DZzM_VgT.js → typescript-DH2E4fY1.js} +2 -2
  27. auto_coder_web/web/assets/{xml-Dl6413Na.js → xml-kipCCfsB.js} +2 -2
  28. auto_coder_web/web/assets/{yaml-DML583wh.js → yaml-BI9EB_1X.js} +1 -1
  29. auto_coder_web/web/index.html +1 -1
  30. {auto_coder_web-0.1.81.dist-info → auto_coder_web-0.1.83.dist-info}/METADATA +3 -2
  31. {auto_coder_web-0.1.81.dist-info → auto_coder_web-0.1.83.dist-info}/RECORD +34 -32
  32. auto_coder_web/web/assets/HistoryPanel-gl864o6H.js +0 -1
  33. {auto_coder_web-0.1.81.dist-info → auto_coder_web-0.1.83.dist-info}/WHEEL +0 -0
  34. {auto_coder_web-0.1.81.dist-info → auto_coder_web-0.1.83.dist-info}/entry_points.txt +0 -0
  35. {auto_coder_web-0.1.81.dist-info → auto_coder_web-0.1.83.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,601 @@
1
+ import asyncio
2
+ import json
3
+ import os
4
+ import fnmatch
5
+ import pathspec
6
+ from threading import Thread
7
+ from concurrent.futures import ThreadPoolExecutor
8
+ from fastapi import APIRouter, HTTPException, Request, Depends, Query
9
+ from fastapi.responses import StreamingResponse
10
+ from pydantic import BaseModel, Field
11
+ from typing import Dict, Any, Optional, List
12
+
13
+ from autocoder.common.rulefiles.autocoderrules_utils import get_rules, AutocoderRulesManager, RuleFile, parse_rule_file
14
+ from autocoder.agent.auto_learn import AutoLearn
15
+ from autocoder.common import SourceCode, SourceCodeList
16
+ from autocoder.auto_coder_runner import get_final_config, get_single_llm, get_memory
17
+ from autocoder.chat_auto_coder_lang import get_message, get_message_with_format
18
+ from autocoder.rag.token_counter import count_tokens
19
+ from autocoder.events.event_manager_singleton import get_event_manager, gengerate_event_file_path, get_event_file_path
20
+ from autocoder.events import event_content as EventContentCreator
21
+ from autocoder.events.event_types import EventType
22
+ from autocoder.common.global_cancel import global_cancel, CancelRequestedException
23
+ from autocoder.command_parser import CommandParser
24
+ # Add import for AutoCoderRunnerWrapper
25
+ from auto_coder_web.auto_coder_runner_wrapper import AutoCoderRunnerWrapper
26
+ from loguru import logger
27
+
28
+ router = APIRouter()
29
+
30
+ # Thread pool for cancellation, similar to coding_router
31
+ cancel_thread_pool = ThreadPoolExecutor(max_workers=5)
32
+
33
+ # --- Pydantic Models ---
34
+
35
+ class RuleInfo(BaseModel):
36
+ file_path: str
37
+ token_count: int
38
+ description: Optional[str] = None
39
+ globs: Optional[List[str]] = None
40
+ always_apply: Optional[bool] = None
41
+ content: Optional[str] = None # Optionally include content for 'get'
42
+
43
+ class RuleListResponse(BaseModel):
44
+ rules: List[RuleInfo]
45
+
46
+ class RulePatternRequest(BaseModel):
47
+ pattern: str
48
+
49
+ class RuleAnalyzeRequest(BaseModel):
50
+ query: Optional[str] = ""
51
+ # Add other relevant parameters if needed, e.g., specific files to analyze
52
+ # files: Optional[List[str]] = None
53
+
54
+ class RuleCommitRequest(BaseModel):
55
+ commit_id: str
56
+ query: str
57
+
58
+ class RuleGeneralResponse(BaseModel):
59
+ status: str
60
+ message: str
61
+
62
+ class RuleHelpResponse(BaseModel):
63
+ help_text: str
64
+
65
+ class AsyncTaskResponse(BaseModel):
66
+ event_file_id: str
67
+
68
+ class UserResponseRequest(BaseModel):
69
+ event_id: str
70
+ event_file_id: str
71
+ response: str # For potential future use if analyze/commit become interactive
72
+
73
+ class CancelTaskRequest(BaseModel):
74
+ event_file_id: str
75
+
76
+ # --- Dependencies ---
77
+
78
+ async def get_project_path(request: Request) -> str:
79
+ """
80
+ Gets the project path from the FastAPI request state.
81
+ """
82
+ path = request.app.state.project_path
83
+ logger.debug(f"Retrieved project path: {path}")
84
+ if not path or not os.path.isdir(path):
85
+ logger.error(f"Invalid project path configured: {path}")
86
+ raise HTTPException(status_code=500, detail="Server configuration error: Project path is invalid or not set.")
87
+ # Ensure rules manager uses the correct project root for this request context
88
+ # This might require adjusting AutocoderRulesManager if it's a strict singleton
89
+ # For now, we rely on get_rules taking project_root
90
+ return path
91
+
92
+ # --- Helper Functions ---
93
+
94
+ def _get_rules_logic(project_root: str, pattern: str = "*") -> List[RuleInfo]:
95
+ """Shared logic for listing/getting rules."""
96
+ logger.info(f"Fetching rules for project: {project_root} with pattern: {pattern}")
97
+ # Use the project_root specific function
98
+ rules_content = get_rules(project_root=project_root)
99
+ if not rules_content:
100
+ logger.warning(f"No rules found in project: {project_root}")
101
+ return []
102
+
103
+ matched_rules = []
104
+ rule_files_info = []
105
+
106
+ # Normalize pattern for pathspec
107
+ if pattern == "*":
108
+ # Match all .md files within the rules directories
109
+ spec = pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, ["*.md"])
110
+ else:
111
+ # Use the provided pattern
112
+ spec = pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, [pattern])
113
+
114
+ for file_path, content in rules_content.items():
115
+ try:
116
+ # Use relative path for matching if possible
117
+ if os.path.isabs(file_path):
118
+ try:
119
+ rel_path = os.path.relpath(file_path, project_root)
120
+ except ValueError:
121
+ # Handle cases where file_path is not under project_root (e.g., different drive on Windows)
122
+ # In such cases, maybe match against the full path or just the filename?
123
+ # Matching against filename seems safer for wildcard patterns.
124
+ rel_path = os.path.basename(file_path)
125
+ else:
126
+ rel_path = file_path
127
+
128
+ # Perform matching
129
+ if spec.match_file(rel_path):
130
+ matched_rules.append((file_path, content))
131
+ except Exception as e:
132
+ logger.error(f"Error matching pattern '{pattern}' against file '{file_path}': {str(e)}")
133
+ # Continue to next file
134
+
135
+ if not matched_rules:
136
+ logger.info(f"No rules matched pattern '{pattern}' in project: {project_root}")
137
+ return []
138
+
139
+ # Parse matched files and gather info
140
+ for file_path, content in sorted(matched_rules):
141
+ try:
142
+ # Use the project_root specific function
143
+ parsed_rule = parse_rule_file(file_path, project_root=project_root)
144
+ token_count = count_tokens(content) # Count tokens from raw content
145
+ rule_info = RuleInfo(
146
+ file_path=file_path,
147
+ token_count=token_count,
148
+ description=parsed_rule.description,
149
+ globs=parsed_rule.globs,
150
+ always_apply=parsed_rule.always_apply,
151
+ content=content # Include content for 'get' scenarios
152
+ )
153
+ rule_files_info.append(rule_info)
154
+ except Exception as e:
155
+ logger.error(f"Error processing rule file {file_path}: {e}")
156
+ # Optionally append a rule with an error state
157
+
158
+ logger.info(f"Found {len(rule_files_info)} rules matching pattern '{pattern}'")
159
+ return rule_files_info
160
+
161
+
162
+ # --- API Endpoints ---
163
+
164
+ @router.get("/api/rules/list", response_model=RuleListResponse)
165
+ async def list_rules(
166
+ pattern: Optional[str] = Query("*", description="Wildcard pattern to filter rule files (e.g., '*.md', 'common*')"),
167
+ project_path: str = Depends(get_project_path)
168
+ ):
169
+ """
170
+ Lists rule files, optionally filtered by a wildcard pattern.
171
+ Returns basic info (path, token count, metadata).
172
+ """
173
+ try:
174
+ rules_info = _get_rules_logic(project_path, pattern)
175
+ # For list, we don't need the full content
176
+ for rule in rules_info:
177
+ rule.content = None
178
+ return RuleListResponse(rules=rules_info)
179
+ except Exception as e:
180
+ logger.exception(f"Error listing rules with pattern '{pattern}': {e}")
181
+ raise HTTPException(status_code=500, detail=f"Failed to list rules: {str(e)}")
182
+
183
+ @router.get("/api/rules/get", response_model=RuleListResponse)
184
+ async def get_rule_content(
185
+ pattern: str = Query(..., description="Wildcard pattern to match rule files for content retrieval."),
186
+ project_path: str = Depends(get_project_path)
187
+ ):
188
+ """
189
+ Gets the content of rule files matching the specified pattern.
190
+ """
191
+ if not pattern:
192
+ raise HTTPException(status_code=400, detail="Pattern parameter is required.")
193
+ try:
194
+ # _get_rules_logic includes content by default
195
+ rules_info = _get_rules_logic(project_path, pattern)
196
+ if not rules_info:
197
+ raise HTTPException(status_code=404, detail=f"No rule files found matching pattern: {pattern}")
198
+ return RuleListResponse(rules=rules_info)
199
+ except HTTPException as e:
200
+ raise e # Re-raise HTTP exceptions
201
+ except Exception as e:
202
+ logger.exception(f"Error getting rule content with pattern '{pattern}': {e}")
203
+ raise HTTPException(status_code=500, detail=f"Failed to get rule content: {str(e)}")
204
+
205
+
206
+ @router.delete("/api/rules/remove", response_model=RuleGeneralResponse)
207
+ async def remove_rules(
208
+ pattern: str = Query(..., description="Wildcard pattern identifying rules to remove."),
209
+ project_path: str = Depends(get_project_path)
210
+ ):
211
+ """
212
+ Removes rule files matching the specified pattern.
213
+ """
214
+ if not pattern:
215
+ raise HTTPException(status_code=400, detail="Pattern parameter is required.")
216
+
217
+ logger.info(f"Attempting to remove rules matching pattern '{pattern}' in project: {project_path}")
218
+
219
+ # Get rules manager instance specific to the project path if possible, or re-init logic
220
+ # For simplicity, we'll re-fetch rules based on project_path
221
+ rules_content = get_rules(project_root=project_path)
222
+ if not rules_content:
223
+ raise HTTPException(status_code=404, detail="No rule files found to remove.")
224
+
225
+ files_to_remove = []
226
+ try:
227
+ spec = pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, [pattern])
228
+ for file_path in rules_content.keys():
229
+ if os.path.isabs(file_path):
230
+ try:
231
+ rel_path = os.path.relpath(file_path, project_path)
232
+ except ValueError:
233
+ rel_path = os.path.basename(file_path) # Fallback for cross-drive paths etc.
234
+ else:
235
+ rel_path = file_path
236
+
237
+ if spec.match_file(rel_path):
238
+ files_to_remove.append(file_path)
239
+
240
+ except Exception as e:
241
+ logger.error(f"Error matching pattern '{pattern}' for removal: {str(e)}")
242
+ raise HTTPException(status_code=400, detail=f"Invalid pattern '{pattern}': {str(e)}")
243
+
244
+ if not files_to_remove:
245
+ raise HTTPException(status_code=404, detail=f"No rule files found matching pattern '{pattern}' to remove.")
246
+
247
+ removed_count = 0
248
+ errors = []
249
+ for file_path in files_to_remove:
250
+ try:
251
+ os.remove(file_path)
252
+ removed_count += 1
253
+ logger.info(f"Removed rule file: {file_path}")
254
+ except Exception as e:
255
+ error_msg = f"Failed to remove file {file_path}: {str(e)}"
256
+ logger.error(error_msg)
257
+ errors.append(error_msg)
258
+
259
+ # Force reload rules in the manager if it's a shared instance (tricky)
260
+ # Or simply rely on next get_rules call to reflect the change
261
+ # AutocoderRulesManager()._load_rules() # This might affect other requests if singleton
262
+
263
+ if errors:
264
+ raise HTTPException(status_code=500, detail=f"Removed {removed_count} file(s) but encountered errors: {'; '.join(errors)}")
265
+
266
+ return RuleGeneralResponse(status="success", message=f"Successfully removed {removed_count} rule file(s) matching pattern '{pattern}'.")
267
+
268
+ @router.post("/api/rules/analyze", response_model=AsyncTaskResponse)
269
+ async def analyze_rules(
270
+ request_data: RuleAnalyzeRequest,
271
+ project_path: str = Depends(get_project_path)
272
+ ):
273
+ """
274
+ Analyzes current files (from memory/context) based on rules and an optional query.
275
+ Runs as a background task.
276
+ """
277
+ event_file, file_id = gengerate_event_file_path(base_dir=project_path)
278
+ logger.info(f"Starting rule analysis task {file_id} for project: {project_path}")
279
+
280
+ def run_analysis_in_thread():
281
+ try:
282
+ global_cancel.register_token(event_file)
283
+ event_manager = get_event_manager(event_file)
284
+ event_manager.write_event(EventContentCreator.create_task_start(
285
+ "Rule Analysis Started", {"query": request_data.query}
286
+ ).to_dict())
287
+
288
+ # --- Analysis Logic ---
289
+ args = get_final_config() # Gets config potentially influenced by project settings
290
+ llm = get_single_llm(args.model, product_mode=args.product_mode)
291
+ auto_learn = AutoLearn(llm=llm, args=args, project_root=project_path) # Pass project_path
292
+
293
+ # TODO: Determine how to get 'current_files' in the web context.
294
+ # This might need state management or explicit file list in request.
295
+ # Using a placeholder or assuming indexer provides files for now.
296
+ # Let's try fetching from memory first, might be empty in web context.
297
+ memory = get_memory(project_path) # Get memory specific to project
298
+ files = memory.get("current_files", {}).get("files", [])
299
+
300
+ if not files:
301
+ # If no files in memory, maybe analyze all project files? Or require explicit list?
302
+ # For now, report error if no files are targeted.
303
+ raise ValueError("No active files found in memory for analysis. Specify files or use a different mechanism.")
304
+
305
+
306
+ sources = SourceCodeList([])
307
+ event_manager.write_event(EventContentCreator.create_message(f"Reading {len(files)} files for analysis...").to_dict())
308
+ for file in files:
309
+ file_abs_path = os.path.join(project_path, file) if not os.path.isabs(file) else file
310
+ if not os.path.exists(file_abs_path):
311
+ logger.warning(f"File not found: {file_abs_path}, skipping.")
312
+ event_manager.write_event(EventContentCreator.create_warning(f"File not found: {file}").to_dict())
313
+ continue
314
+ try:
315
+ with open(file_abs_path, "r", encoding="utf-8") as f:
316
+ source_code = f.read()
317
+ sources.sources.append(SourceCode(module_name=file, source_code=source_code)) # Use relative path as module name
318
+ except Exception as e:
319
+ logger.error(f"Error reading file {file_abs_path}: {e}")
320
+ event_manager.write_event(EventContentCreator.create_error(
321
+ error_code="FILE_READ_ERROR",
322
+ error_message=f"Error reading file {file}: {e}"
323
+ ).to_dict())
324
+ # Decide whether to continue or fail task
325
+
326
+ if not sources.sources:
327
+ raise ValueError("No valid source files could be read for analysis.")
328
+
329
+ event_manager.write_event(EventContentCreator.create_message(f"Analyzing {len(sources.sources)} files with query: '{request_data.query or 'Default analysis'}'").to_dict())
330
+
331
+ # Generate the analysis prompt text
332
+ prompt_text = auto_learn.analyze_modules.prompt(sources=sources, query=request_data.query)
333
+
334
+ # --- Use AutoCoderRunnerWrapper to execute the generated prompt ---
335
+ wrapper = AutoCoderRunnerWrapper(project_path)
336
+ wrapper.configure_wrapper(f"event_file:{event_file}")
337
+ # The coding_wapper will handle writing events, including completion/error
338
+ result = wrapper.coding_wapper(prompt_text)
339
+ # The wrapper logs completion internally, no need for manual completion event here.
340
+ # logger.info(f"Rule analysis task {file_id} completed successfully.") # Logged by wrapper
341
+
342
+ except CancelRequestedException:
343
+ logger.info(f"Rule analysis task {file_id} cancelled.")
344
+ get_event_manager(event_file).write_error(
345
+ EventContentCreator.create_error(error_code="499", error_message="Task cancelled by user.").to_dict()
346
+ )
347
+ except Exception as e:
348
+ logger.exception(f"Error during rule analysis task {file_id}: {e}")
349
+ get_event_manager(event_file).write_error(
350
+ EventContentCreator.create_error(error_code="500", error_message=str(e), details={}).to_dict()
351
+ )
352
+ finally:
353
+ global_cancel.unregister_token(event_file) # Clean up token
354
+
355
+ # Start the background thread
356
+ thread = Thread(target=run_analysis_in_thread, daemon=True)
357
+ thread.start()
358
+
359
+ return AsyncTaskResponse(event_file_id=file_id)
360
+
361
+
362
+ @router.post("/api/rules/commit", response_model=AsyncTaskResponse)
363
+ async def analyze_commit_rules(
364
+ request_data: RuleCommitRequest,
365
+ project_path: str = Depends(get_project_path)
366
+ ):
367
+ """
368
+ Analyzes a specific git commit based on rules and a query.
369
+ Runs as a background task.
370
+ """
371
+ event_file, file_id = gengerate_event_file_path(base_dir=project_path)
372
+ logger.info(f"Starting commit analysis task {file_id} for commit {request_data.commit_id} in project: {project_path}")
373
+
374
+ def run_commit_analysis_in_thread():
375
+ try:
376
+ global_cancel.register_token(event_file)
377
+ event_manager = get_event_manager(event_file)
378
+ event_manager.write_event(EventContentCreator.create_task_start(
379
+ "Commit Analysis Started", {"commit_id": request_data.commit_id, "query": request_data.query}
380
+ ).to_dict())
381
+
382
+ # --- Commit Analysis Logic ---
383
+ args = get_final_config()
384
+ llm = get_single_llm(args.model, product_mode=args.product_mode)
385
+ # Ensure AutoLearn uses the correct project path context
386
+ auto_learn = AutoLearn(llm=llm, args=args, project_root=project_path)
387
+
388
+ event_manager.write_event(EventContentCreator.create_message(f"Fetching changes for commit: {request_data.commit_id}").to_dict())
389
+ changes, _ = auto_learn.get_commit_changes(request_data.commit_id)
390
+
391
+ if not changes:
392
+ raise ValueError(f"Could not retrieve changes for commit ID: {request_data.commit_id}. Ensure it's a valid commit.")
393
+
394
+ event_manager.write_event(EventContentCreator.create_message(f"Analyzing commit {request_data.commit_id} with query: '{request_data.query}'").to_dict())
395
+
396
+ # Generate the commit analysis prompt text
397
+ prompt_text = auto_learn.analyze_commit.prompt(
398
+ querie_with_urls_and_changes=changes,
399
+ new_query=request_data.query
400
+ )
401
+
402
+ # --- Use AutoCoderRunnerWrapper to execute the generated prompt ---
403
+ wrapper = AutoCoderRunnerWrapper(project_path)
404
+ wrapper.configure_wrapper(f"event_file:{event_file}")
405
+ # The coding_wapper will handle writing events, including completion/error
406
+ result = wrapper.coding_wapper(prompt_text)
407
+ # The wrapper logs completion internally, no need for manual completion event here.
408
+ # logger.info(f"Commit analysis task {file_id} completed successfully.") # Logged by wrapper
409
+
410
+ except CancelRequestedException:
411
+ logger.info(f"Commit analysis task {file_id} cancelled.")
412
+ get_event_manager(event_file).write_error(
413
+ EventContentCreator.create_error(error_code="499", error_message="Task cancelled by user.").to_dict()
414
+ )
415
+ except Exception as e:
416
+ logger.exception(f"Error during commit analysis task {file_id}: {e}")
417
+ get_event_manager(event_file).write_error(
418
+ EventContentCreator.create_error(error_code="500", error_message=str(e), details={}).to_dict()
419
+ )
420
+ finally:
421
+ global_cancel.unregister_token(event_file) # Clean up token
422
+
423
+ # Start the background thread
424
+ thread = Thread(target=run_commit_analysis_in_thread, daemon=True)
425
+ thread.start()
426
+
427
+ return AsyncTaskResponse(event_file_id=file_id)
428
+
429
+
430
+ @router.get("/api/rules/help", response_model=RuleHelpResponse)
431
+ async def get_help():
432
+ """
433
+ Returns the help text for the /rules command.
434
+ """
435
+ # Attempt to get localized help text
436
+ help_text = get_message("rules_help_text")
437
+ if not help_text or help_text == "rules_help_text": # Check if translation failed
438
+ # Provide a default English help text if localization fails
439
+ help_text = """
440
+ Available /rules API Endpoints:
441
+ GET /api/rules/list?pattern=<wildcard> - List rule files (basic info), optionally filter by pattern.
442
+ GET /api/rules/get?pattern=<wildcard> - Get content of rule files matching the pattern.
443
+ DELETE /api/rules/remove?pattern=<wildcard> - Remove rule files matching the pattern.
444
+ POST /api/rules/analyze - Analyze current files (requires files in context/memory) with optional query in body. Runs async.
445
+ Body: { "query": "Optional analysis query" }
446
+ POST /api/rules/commit - Analyze a specific git commit with a query. Runs async.
447
+ Body: { "commit_id": "your_commit_hash", "query": "Your analysis query" }
448
+ GET /api/rules/help - Show this help message.
449
+ GET /api/rules/events?event_file_id=<id> - Stream events for async tasks (analyze, commit).
450
+ POST /api/rules/cancel - Cancel an ongoing async task.
451
+ Body: { "event_file_id": "task_event_id" }
452
+ """
453
+ return RuleHelpResponse(help_text=help_text.strip())
454
+
455
+
456
+ @router.get("/api/rules/events")
457
+ async def poll_rule_events(event_file_id: str, project_path: str = Depends(get_project_path)):
458
+ """
459
+ SSE endpoint to stream events for background rule tasks (analyze, commit).
460
+ """
461
+ async def event_stream():
462
+ event_file = get_event_file_path(event_file_id, project_path)
463
+ if not event_file or not os.path.exists(os.path.dirname(event_file)):
464
+ logger.error(f"Event file path directory does not exist for ID {event_file_id} in project {project_path}")
465
+ # Send an error event and close
466
+ error_event = EventContentCreator.create_error("404", "Event stream not found or invalid ID.").to_dict()
467
+ yield f"data: {json.dumps(error_event)}\n\n"
468
+ return
469
+
470
+ logger.info(f"Starting SSE stream for event file: {event_file}")
471
+ event_manager = get_event_manager(event_file)
472
+ while True:
473
+ try:
474
+ # Use asyncio.to_thread for blocking read_events
475
+ events = await asyncio.to_thread(event_manager.read_events, block=False)
476
+
477
+ if not events:
478
+ # Check if the task is globally cancelled
479
+ if global_cancel.is_set(token=event_file):
480
+ logger.info(f"SSE stream {event_file_id}: Task cancellation detected, closing stream.")
481
+ # Send a final cancellation event if not already sent by the task thread
482
+ cancel_event = EventContentCreator.create_error("499", "Task cancelled.").to_dict()
483
+ yield f"data: {json.dumps(cancel_event)}\n\n"
484
+ break
485
+ await asyncio.sleep(0.2) # Slightly longer sleep if no events
486
+ continue
487
+
488
+ current_event = None
489
+ for event in events:
490
+ current_event = event
491
+ event_json = event.to_json()
492
+ yield f"data: {event_json}\n\n"
493
+ await asyncio.sleep(0.01) # Small delay to allow client processing
494
+
495
+ if current_event is not None:
496
+ if current_event.event_type in [EventType.ERROR, EventType.COMPLETION]:
497
+ logger.info(f"SSE stream {event_file_id}: Terminal event received ({current_event.event_type.name}), closing stream.")
498
+ break
499
+ # Add check for explicit CANCELLED event type if implemented
500
+ # elif current_event.event_type == EventType.CANCELLED:
501
+ # logger.info(f"SSE stream {event_file_id}: Cancelled event received, closing stream.")
502
+ # break
503
+
504
+ except CancelRequestedException:
505
+ logger.info(f"SSE stream {event_file_id}: Cancellation detected during event read, closing stream.")
506
+ yield f"data: {EventContentCreator.create_error('499', 'Task cancelled.').to_json()}\n\n"
507
+ break
508
+ except Exception as e:
509
+ logger.error(f"Error in SSE stream {event_file_id}: {str(e)}")
510
+ # Check if it's a file not found error after task completion/cleanup
511
+ if isinstance(e, FileNotFoundError) or "No such file or directory" in str(e):
512
+ logger.warning(f"SSE stream {event_file_id}: Event file likely removed after task completion. Closing stream.")
513
+ yield f"data: {EventContentCreator.create_error('410', 'Task finished and event log expired.').to_json()}\n\n"
514
+ else:
515
+ logger.exception(e) # Log full traceback for unexpected errors
516
+ yield f"data: {EventContentCreator.create_error('500', f'SSE stream error: {str(e)}').to_json()}\n\n"
517
+ break
518
+
519
+ return StreamingResponse(
520
+ event_stream(),
521
+ media_type="text/event-stream",
522
+ headers={
523
+ "Cache-Control": "no-cache, no-transform",
524
+ "Connection": "keep-alive",
525
+ "Content-Type": "text/event-stream",
526
+ "X-Accel-Buffering": "no",
527
+ "Transfer-Encoding": "chunked",
528
+ },
529
+ )
530
+
531
+ # Note: User response endpoint might not be directly applicable to analyze/commit unless they become interactive.
532
+ # Including it for consistency with coding_router pattern.
533
+ @router.post("/api/rules/response")
534
+ async def response_user_rules(request: UserResponseRequest, project_path: str = Depends(get_project_path)):
535
+ """
536
+ Handles user responses if rule tasks become interactive (currently unlikely).
537
+ """
538
+ logger.warning(f"Received user response for rule task {request.event_file_id}, but rule tasks are not currently interactive.")
539
+ # Implement similar logic to coding_router if interaction is needed in the future.
540
+ # For now, just acknowledge or return an informative message.
541
+ return {"status": "received", "message": "Rule tasks are not currently interactive."}
542
+ # Example future implementation:
543
+ # try:
544
+ # event_file = get_event_file_path(file_id=request.event_file_id, project_path=project_path)
545
+ # event_manager = get_event_manager(event_file)
546
+ # response_event = event_manager.respond_to_user(request.event_id, request.response)
547
+ # return {"status": "success", "message": "Response sent", "event_id": response_event.event_id}
548
+ # except Exception as e:
549
+ # logger.error(f"Error sending user response for rule task {request.event_file_id}: {str(e)}")
550
+ # raise HTTPException(status_code=500, detail=f"Failed to send response: {str(e)}")
551
+
552
+ @router.post("/api/rules/cancel")
553
+ async def cancel_rule_task(request: CancelTaskRequest, project_path: str = Depends(get_project_path)):
554
+ """
555
+ Cancels a running background rule task (analyze, commit).
556
+ """
557
+ try:
558
+ event_file = get_event_file_path(file_id=request.event_file_id, project_path=project_path)
559
+ logger.info(f"Received cancel request for task {request.event_file_id} associated with file: {event_file}")
560
+
561
+ if not event_file:
562
+ raise HTTPException(status_code=404, detail=f"Event file ID {request.event_file_id} not found or invalid.")
563
+
564
+ # Check if the event file exists - indicates if the task might still be running or recently finished
565
+ event_manager = get_event_manager(event_file) # This implicitly checks existence somewhat
566
+
567
+ def cancel_in_thread():
568
+ try:
569
+ logger.info(f"Setting cancellation flag for token: {event_file}")
570
+ global_cancel.set(token=event_file)
571
+
572
+ # Attempt to write a cancel event immediately for faster feedback via SSE
573
+ # This might fail if the task already cleaned up the file, which is acceptable.
574
+ try:
575
+ event_manager.write_error(
576
+ EventContentCreator.create_error(
577
+ error_code="499", error_message="Cancel request received", details={}
578
+ ).to_dict()
579
+ )
580
+ logger.info(f"Cancellation event written for task {request.event_file_id}")
581
+ except Exception as write_err:
582
+ logger.warning(f"Could not write immediate cancel event for task {request.event_file_id} (might be already finished): {write_err}")
583
+
584
+ except Exception as e:
585
+ # Log errors during the cancellation signaling process itself
586
+ logger.error(f"Error during cancellation signaling for task {request.event_file_id}: {str(e)}")
587
+ # Don't raise HTTPException here, as the request to cancel was received.
588
+ # The client will see the task end via SSE or timeout.
589
+
590
+ # Use the shared cancel thread pool
591
+ cancel_thread_pool.submit(cancel_in_thread)
592
+
593
+ return {"status": "success", "message": "Cancel request sent. Task termination depends on the task's current state."}
594
+ except Exception as e:
595
+ logger.error(f"Error processing cancel request for {request.event_file_id}: {str(e)}")
596
+ raise HTTPException(status_code=500, detail=f"Failed to process cancel request: {str(e)}")
597
+
598
+ # TODO: Add endpoints for saving/loading rule task history if needed, similar to coding_router
599
+ # @router.post("/api/rules/save-history")
600
+ # @router.get("/api/rules/history")
601
+ # @router.get("/api/rules/task/{task_id}")
auto_coder_web/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.81"
1
+ __version__ = "0.1.83"
@@ -0,0 +1 @@
1
+ import{r,I as U,_ as G,j as e,S as ee,T as Q,R as re,a as P,h as ne,v as ae,b as q,s as f,d as K,l as se,c as le,e as J,E as X,B as y,g as n,L as _,C as oe,f as Y,i as ie,k as ce,m as de,n as he,M as fe}from"./main.js";var ue={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M296 250c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h384c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H296zm184 144H296c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8zm-48 458H208V148h560v320c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V108c0-17.7-14.3-32-32-32H168c-17.7 0-32 14.3-32 32v784c0 17.7 14.3 32 32 32h264c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm440-88H728v-36.6c46.3-13.8 80-56.6 80-107.4 0-61.9-50.1-112-112-112s-112 50.1-112 112c0 50.7 33.7 93.6 80 107.4V764H520c-8.8 0-16 7.2-16 16v152c0 8.8 7.2 16 16 16h352c8.8 0 16-7.2 16-16V780c0-8.8-7.2-16-16-16zM646 620c0-27.6 22.4-50 50-50s50 22.4 50 50-22.4 50-50 50-50-22.4-50-50zm180 266H566v-60h260v60z"}}]},name:"audit",theme:"outlined"},xe=function(d,i){return r.createElement(U,G({},d,{ref:i,icon:ue}))},me=r.forwardRef(xe),ge={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M839.6 433.8L749 150.5a9.24 9.24 0 00-8.9-6.5h-77.4c-4.1 0-7.6 2.6-8.9 6.5l-91.3 283.3c-.3.9-.5 1.9-.5 2.9 0 5.1 4.2 9.3 9.3 9.3h56.4c4.2 0 7.8-2.8 9-6.8l17.5-61.6h89l17.3 61.5c1.1 4 4.8 6.8 9 6.8h61.2c1 0 1.9-.1 2.8-.4 2.4-.8 4.3-2.4 5.5-4.6 1.1-2.2 1.3-4.7.6-7.1zM663.3 325.5l32.8-116.9h6.3l32.1 116.9h-71.2zm143.5 492.9H677.2v-.4l132.6-188.9c1.1-1.6 1.7-3.4 1.7-5.4v-36.4c0-5.1-4.2-9.3-9.3-9.3h-204c-5.1 0-9.3 4.2-9.3 9.3v43c0 5.1 4.2 9.3 9.3 9.3h122.6v.4L587.7 828.9a9.35 9.35 0 00-1.7 5.4v36.4c0 5.1 4.2 9.3 9.3 9.3h211.4c5.1 0 9.3-4.2 9.3-9.3v-43a9.2 9.2 0 00-9.2-9.3zM416 702h-76V172c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v530h-76c-6.7 0-10.5 7.8-6.3 13l112 141.9a8 8 0 0012.6 0l112-141.9c4.1-5.2.4-13-6.3-13z"}}]},name:"sort-ascending",theme:"outlined"},pe=function(d,i){return r.createElement(U,G({},d,{ref:i,icon:ge}))},ve=r.forwardRef(pe),be={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M839.6 433.8L749 150.5a9.24 9.24 0 00-8.9-6.5h-77.4c-4.1 0-7.6 2.6-8.9 6.5l-91.3 283.3c-.3.9-.5 1.9-.5 2.9 0 5.1 4.2 9.3 9.3 9.3h56.4c4.2 0 7.8-2.8 9-6.8l17.5-61.6h89l17.3 61.5c1.1 4 4.8 6.8 9 6.8h61.2c1 0 1.9-.1 2.8-.4 2.4-.8 4.3-2.4 5.5-4.6 1.1-2.2 1.3-4.7.6-7.1zM663.3 325.5l32.8-116.9h6.3l32.1 116.9h-71.2zm143.5 492.9H677.2v-.4l132.6-188.9c1.1-1.6 1.7-3.4 1.7-5.4v-36.4c0-5.1-4.2-9.3-9.3-9.3h-204c-5.1 0-9.3 4.2-9.3 9.3v43c0 5.1 4.2 9.3 9.3 9.3h122.6v.4L587.7 828.9a9.35 9.35 0 00-1.7 5.4v36.4c0 5.1 4.2 9.3 9.3 9.3h211.4c5.1 0 9.3-4.2 9.3-9.3v-43a9.2 9.2 0 00-9.2-9.3zM310.3 167.1a8 8 0 00-12.6 0L185.7 309c-4.2 5.3-.4 13 6.3 13h76v530c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V322h76c6.7 0 10.5-7.8 6.3-13l-112-141.9z"}}]},name:"sort-descending",theme:"outlined"},ye=function(d,i){return r.createElement(U,G({},d,{ref:i,icon:be}))},je=r.forwardRef(ye);se.config({paths:{vs:"/monaco-editor/min/vs"},"vs/nls":{availableLanguages:{"*":"zh-cn"}}});const{TabPane:Z}=Q,we=({commitId:o,onClose:d})=>{const[i,A]=r.useState({diff:""}),[c,R]=r.useState(null),[h,j]=r.useState(null),[M,E]=r.useState(!1),[x,L]=r.useState("split"),[m,$]=r.useState(null),[p,w]=r.useState(!1),[v,z]=r.useState("1"),N=r.useRef(null),b=r.useRef(null),C=r.useRef(null),k=r.useRef(null),B=async()=>{if(o){w(!0);try{const t=encodeURIComponent(o),a=await q.get(`/api/history/commit-diff/${t}`);a.data.success?A({diff:a.data.diff,file_changes:a.data.file_changes}):f.error(a.data.message||"获取diff失败")}catch(t){console.error("Error fetching diff:",t),f.error("获取diff失败")}finally{w(!1)}}},S=async t=>{if(o)try{E(!0),j(null),N.current=null,b.current=null,C.current=null;const a=encodeURIComponent(o),g=await q.get(`/api/history/file-diff/${a}?file_path=${encodeURIComponent(t)}`);g.data.success?j(g.data.file_diff):f.error(g.data.message||"获取文件差异失败")}catch(a){console.error("Error fetching file diff:",a),f.error("获取文件差异失败")}finally{E(!1)}},D=t=>{c===t?(R(null),j(null),N.current=null,b.current=null,C.current=null,$(null)):(R(t),S(t),$(null))},T=r.useCallback(t=>{N.current=t},[]),V=r.useCallback(t=>{b.current=t},[]),H=r.useCallback(t=>{C.current=t},[]),I=t=>{var F;const a=(F=t.split(".").pop())==null?void 0:F.toLowerCase();return{js:"javascript",jsx:"javascript",ts:"typescript",tsx:"typescript",py:"python",java:"java",c:"c",cpp:"cpp",cs:"csharp",go:"go",rs:"rust",rb:"ruby",php:"php",html:"html",css:"css",scss:"scss",json:"json",md:"markdown",yml:"yaml",yaml:"yaml",xml:"xml",sh:"shell",bash:"shell",txt:"plaintext"}[a||""]||"plaintext"},s=({viewType:t})=>{const a=m===t;return e.jsx("button",{className:"ml-2 text-gray-400 hover:text-white transition-colors",onClick:g=>{g.stopPropagation(),$(a?null:t)},title:a?"恢复正常视图":"最大化视图",children:a?e.jsx("svg",{className:"w-3.5 h-3.5",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:e.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5v-4m0 4h-4m4 0l-5-5"})}):e.jsx("svg",{className:"w-3.5 h-3.5",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:e.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5v-4m0 4h-4m4 0l-5-5"})})})},l=()=>{if(!c||!h)return null;const t=I(c),a=`diff-${o}-${c}-${x}`,g=`before-${o}-${c}`,F=`after-${o}-${c}`,O={readOnly:!0,scrollBeyondLastLine:!1,minimap:{enabled:!1},lineNumbers:"on",wordWrap:"on",automaticLayout:!0,scrollbar:{vertical:"visible",horizontal:"visible",verticalScrollbarSize:14,horizontalScrollbarSize:14}};return x==="unified"?e.jsxs("div",{ref:k,className:"bg-gray-900 rounded-lg border border-gray-700",style:{height:"500px",width:"100%",overflow:"hidden"},children:[e.jsxs("div",{className:"py-1 px-3 bg-gray-800 border-b border-gray-700 text-xs font-medium text-white flex justify-between items-center",children:[e.jsx("span",{children:"差异视图"}),e.jsx(s,{viewType:"diff"})]}),e.jsx(K,{height:"calc(100% - 26px)",width:"100%",defaultLanguage:"diff",value:h.diff_content||"",theme:"vs-dark",onMount:H,options:O,loading:e.jsx("div",{className:"flex items-center justify-center h-full text-white",children:"加载中..."})},a)]}):e.jsxs("div",{ref:k,className:"grid grid-cols-2 gap-2",style:{height:"500px",width:"100%"},children:[(m===null||m==="before")&&e.jsxs("div",{className:`bg-gray-900 rounded-lg border border-gray-700 ${m==="before"?"col-span-2":""}`,style:{overflow:"hidden"},children:[e.jsxs("div",{className:"py-1 px-3 bg-gray-800 border-b border-gray-700 text-xs font-medium text-white flex justify-between items-center",children:[e.jsxs("span",{children:["修改前 (",h.file_status==="added"?"新文件":c,")"]}),e.jsx(s,{viewType:"before"})]}),e.jsx(K,{height:"calc(100% - 26px)",width:"100%",defaultLanguage:t,value:h.before_content||"",theme:"vs-dark",onMount:T,options:O,loading:e.jsx("div",{className:"flex items-center justify-center h-full text-white",children:"加载中..."})},g)]}),(m===null||m==="after")&&e.jsxs("div",{className:`bg-gray-900 rounded-lg border border-gray-700 ${m==="after"?"col-span-2":""}`,style:{overflow:"hidden"},children:[e.jsxs("div",{className:"py-1 px-3 bg-gray-800 border-b border-gray-700 text-xs font-medium text-white flex justify-between items-center",children:[e.jsxs("span",{children:["修改后 (",h.file_status==="deleted"?"文件已删除":c,")"]}),e.jsx(s,{viewType:"after"})]}),e.jsx(K,{height:"calc(100% - 26px)",width:"100%",defaultLanguage:t,value:h.after_content||"",theme:"vs-dark",onMount:V,options:O,loading:e.jsx("div",{className:"flex items-center justify-center h-full text-white",children:"加载中..."})},F)]})]})};r.useEffect(()=>{o&&B()},[o]);const u={background:"#1F2937",borderBottom:"1px solid #374151",margin:0,padding:"0 16px"},te=(t,a)=>e.jsx(a,{...t,style:u,className:"custom-tabs-bar"});return e.jsxs("div",{className:"flex flex-col h-full bg-[#111827] overflow-hidden",children:[e.jsxs("div",{className:"flex justify-between items-center p-4 bg-[#1F2937] border-b border-[#374151]",children:[e.jsx("h2",{className:"text-lg font-semibold text-white",children:"代码变更详情"}),e.jsx(ee,{children:d&&e.jsx("button",{className:"px-3 py-1.5 bg-gray-700 hover:bg-gray-600 text-white rounded text-sm",onClick:d,children:"关闭"})})]}),e.jsx("div",{className:"flex-1 overflow-y-auto",children:e.jsxs(Q,{activeKey:v,onChange:z,type:"card",className:"diff-viewer-tabs",renderTabBar:te,style:{background:"#111827"},children:[e.jsx(Z,{tab:e.jsx("div",{className:`py-2 px-4 ${v==="1"?"text-white font-medium":"text-gray-400"}`,children:"文件列表"}),children:e.jsxs("div",{className:"p-4",children:[e.jsx("div",{className:"space-y-2 mb-4",children:p?e.jsx("div",{className:"flex items-center justify-center py-10",children:e.jsx("div",{className:"animate-spin rounded-full h-8 w-8 border-b-2 border-white"})}):i.file_changes&&i.file_changes.length>0?i.file_changes.map((t,a)=>e.jsx("div",{className:`text-sm py-2 px-3 rounded cursor-pointer ${c===t.path?"bg-gray-700 border-l-2 border-indigo-500":"hover:bg-gray-750 bg-gray-800"}`,onClick:()=>D(t.path),children:e.jsxs("div",{className:"flex items-center",children:[e.jsx("span",{className:`w-1.5 h-1.5 rounded-full mr-2 ${t.change_type==="added"?"bg-green-500":t.change_type==="modified"?"bg-yellow-500":t.change_type==="deleted"?"bg-red-500":"bg-blue-500"}`}),e.jsx("span",{className:`font-mono ${t.change_type==="deleted"?"line-through text-gray-500":"text-white"}`,children:t.path}),e.jsx(re,{className:"ml-2 text-gray-400"})]})},a)):e.jsx("div",{className:"text-center text-white py-8",children:e.jsx("p",{children:"没有文件变更信息"})})}),M&&e.jsx("div",{className:"flex items-center justify-center py-10",children:e.jsx("div",{className:"animate-spin rounded-full h-8 w-8 border-b-2 border-white"})}),c&&h&&!M&&e.jsx("div",{className:"flex justify-end mb-3",children:e.jsxs(P.Group,{value:x,onChange:t=>L(t.target.value),buttonStyle:"solid",children:[e.jsx(P.Button,{value:"split",style:{color:x==="split"?"#fff":"#1f2937"},children:"分割视图"}),e.jsx(P.Button,{value:"unified",style:{color:x==="unified"?"#fff":"#1f2937"},children:"统一视图"})]})}),c&&h&&!M&&l()]})},"1"),e.jsx(Z,{tab:e.jsx("div",{className:`py-2 px-4 ${v==="2"?"text-white font-medium":"text-gray-400"}`,children:"原始差异"}),children:e.jsx("div",{className:"p-4",children:p?e.jsx("div",{className:"flex items-center justify-center py-10",children:e.jsx("div",{className:"animate-spin rounded-full h-8 w-8 border-b-2 border-white"})}):e.jsx(ne,{language:"diff",style:ae,customStyle:{padding:"12px",borderRadius:"4px",overflow:"auto",maxHeight:"600px"},children:i.diff||""})})},"2")]})})]})};se.config({paths:{vs:"/monaco-editor/min/vs"},"vs/nls":{availableLanguages:{"*":"zh-cn"}}});const{Text:W}=ie,{TabPane:Ce}=Q,ke=()=>{const[o,d]=r.useState([]),[i,A]=r.useState(!1),[c,R]=r.useState(!1),[h,j]=r.useState(!1),[M,E]=r.useState([]),[x,L]=r.useState(null),[m,$]=r.useState(!1),[p,w]=r.useState({show:!1,commitHash:"",commitMessage:""}),[v,z]=r.useState(!1),[N,b]=r.useState(null),[C,k]=r.useState(null),B=s=>{if(!s){f.info(n("noAssociatedCodeChanges"));return}L(s)},S=async()=>{R(!0);try{const s=await q.get("/api/history/validate-and-load");s.data.success?d(s.data.queries):f.error(s.data.message||n("loadHistoryFailed"))}catch(s){console.error("Error loading queries:",s),f.error(n("loadFailed"))}finally{R(!1)}};le.useEffect(()=>{S();const s=J.subscribe(X.CODING.TASK_COMPLETE,l=>{console.log("Coding task completed, reloading queries",l),S()});return()=>{s()}},[]);const D=(s,l,u)=>{s.stopPropagation(),w({show:!0,commitHash:l,commitMessage:u})},T=async()=>{try{z(!0),b(null),k(null);const s=await fetch(`/api/commits/${p.commitHash}/revert`,{method:"POST",headers:{"Content-Type":"application/json"}});if(!s.ok){const u=await s.json();throw new Error(u.detail||n("revertCommitFailed"))}const l=await s.json();k(`${n("revertCommitSuccessPrefix")}: ${l.new_commit_hash.substring(0,7)}`),w(u=>({...u,show:!1})),S(),setTimeout(()=>{k(null)},5e3)}catch(s){b(s instanceof Error?s.message:"撤销提交失败"),console.error("Failed to revert commit:",s)}finally{z(!1)}},V=()=>{w({show:!1,commitHash:"",commitMessage:""}),b(null)},H=(s,l)=>{s.stopPropagation(),J.publish(X.CHAT.NEW_MESSAGE,{action:"review",commit_id:l,mode:"chat"}),f.success(n("reviewCommandSent"))},I=(s,l)=>{s.stopPropagation(),navigator.clipboard.writeText(l).then(()=>{f.success(n("commitCopied"))}).catch(u=>{console.error("复制失败:",u),f.error(n("copyFailed"))})};return x?e.jsx(we,{commitId:x,onClose:()=>L(null)}):e.jsxs("div",{className:"flex flex-col bg-[#111827] overflow-hidden",style:{height:"650px"},children:[e.jsx("div",{className:"flex justify-between items-center p-4 bg-[#1F2937] border-b border-[#374151] sticky top-0 z-10 shadow-md",children:e.jsxs(ee,{children:[e.jsx(y,{icon:i?e.jsx(ve,{}):e.jsx(je,{}),onClick:()=>{A(!i),d([...o].reverse())},children:i?n("ascending"):n("descending")}),e.jsx(y,{type:"primary",onClick:S,loading:c,children:n("refresh")})]})}),p.show&&e.jsx("div",{className:"fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4",children:e.jsxs("div",{className:"bg-gray-800 rounded-lg shadow-xl max-w-md w-full p-6 border border-gray-700",children:[e.jsx("h3",{className:"text-xl font-semibold text-white mb-4",children:n("confirmRevertTitle")}),e.jsx("p",{className:"text-gray-300 mb-6",children:n("confirmRevertMessage")}),e.jsxs("div",{className:"bg-gray-900 p-3 rounded mb-6 border border-gray-700",children:[e.jsxs("p",{className:"text-sm text-gray-400 mb-1",children:[n("commitInfoLabel"),":"]}),e.jsx("p",{className:"text-white",children:p.commitMessage}),e.jsxs("p",{className:"text-xs text-gray-500 mt-2",children:["Commit: ",p.commitHash.substring(0,7)]})]}),N&&e.jsx("div",{className:"bg-red-900 bg-opacity-25 text-red-400 p-3 rounded mb-4",children:N}),e.jsxs("div",{className:"flex justify-end space-x-3",children:[e.jsx("button",{className:"px-4 py-2 bg-gray-700 hover:bg-gray-600 text-gray-300 rounded transition-colors",onClick:V,disabled:v,children:n("cancel")}),e.jsx("button",{className:"px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded transition-colors flex items-center",onClick:T,disabled:v,children:v?e.jsxs(e.Fragment,{children:[e.jsxs("svg",{className:"animate-spin -ml-1 mr-2 h-4 w-4 text-white",xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",children:[e.jsx("circle",{className:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"4"}),e.jsx("path",{className:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"})]}),n("processing"),"..."]}):e.jsx(e.Fragment,{children:n("confirmRevertButton")})})]})]})}),C&&e.jsx("div",{className:"fixed top-4 right-4 bg-green-800 text-green-100 p-4 rounded-lg shadow-lg z-50 animate-fade-in-out",children:e.jsxs("div",{className:"flex items-center",children:[e.jsx("svg",{className:"w-5 h-5 mr-2",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:e.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:"2",d:"M5 13l4 4L19 7"})}),e.jsx("span",{children:C})]})}),e.jsxs("div",{className:"flex-1 overflow-y-auto p-4",children:[e.jsx(_,{dataSource:o,renderItem:s=>e.jsx(_.Item,{className:"border-b border-[#374151] last:border-b-0",children:e.jsx(oe,{className:"w-full bg-[#1F2937] border-[#374151] hover:bg-[#2D3748] transition-colors duration-200",title:e.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center"},children:[e.jsxs("div",{children:[e.jsx(Y,{style:{marginRight:"8px",color:"#9CA3AF"}}),e.jsx(W,{style:{color:"#E5E7EB"},children:`${s.file_number}_chat_action.yml`}),s.timestamp&&e.jsx(W,{style:{marginLeft:"10px",fontSize:"12px",color:"#9CA3AF"},children:s.timestamp})]}),e.jsxs("div",{className:"flex space-x-2",children:[s.urls&&s.urls.length>0&&e.jsx(y,{icon:e.jsx(Y,{}),type:"link",style:{color:"#60A5FA"},onClick:()=>{E(s.urls||[]),j(!0)},children:n("viewContext")}),s.response&&!s.is_reverted&&e.jsxs(e.Fragment,{children:[e.jsx(y,{icon:e.jsx(ce,{}),type:"link",style:{color:"#F87171"},onClick:l=>D(l,s.response,s.query),children:n("revert")}),e.jsx(y,{icon:e.jsx(me,{}),type:"link",style:{color:"#10B981"},onClick:l=>H(l,s.response),children:n("reviewChanges")}),e.jsx(y,{icon:e.jsx(de,{}),type:"link",style:{color:"#6366F1"},onClick:l=>I(l,s.response),children:n("copyCommit")})]}),e.jsx(y,{icon:e.jsx(he,{}),type:"link",style:{color:s.response?"#60A5FA":"#9CA3AF"},onClick:()=>B(s.response),disabled:!s.response,children:n("viewChanges")})]})]}),children:e.jsxs("div",{className:`${s.is_reverted?"border border-red-500 rounded-lg p-2 relative":""}`,children:[s.is_reverted&&e.jsx("div",{className:"absolute -top-2 -right-2 bg-red-500 text-white text-xs px-2 py-0.5 rounded-full",children:n("reverted")}),e.jsx("div",{style:{backgroundColor:"#111827",padding:"12px",borderRadius:"4px",color:"#E5E7EB",border:"1px solid #374151",maxWidth:"100%",fontSize:"14px",lineHeight:"1.6",whiteSpace:"normal",wordBreak:"break-word"},children:s.query})]})})})}),e.jsx(fe,{title:n("contextFileListTitle"),open:h,onCancel:()=>j(!1),width:600,footer:null,className:"dark-theme-modal",styles:{content:{backgroundColor:"#1f2937",padding:"20px"},header:{backgroundColor:"#1f2937",borderBottom:"1px solid #374151",color:"#ffffff"},body:{backgroundColor:"#1f2937",color:"#ffffff"},mask:{backgroundColor:"rgba(0, 0, 0, 0.6)"}},children:e.jsx(_,{dataSource:M,className:"dark-theme-list max-h-96 overflow-y-auto",renderItem:s=>e.jsx(_.Item,{className:"text-gray-200 border-gray-700",children:e.jsx("div",{className:"flex items-center w-full",children:e.jsx(W,{style:{color:"#E5E7EB"},children:s})})})})})]})]})};export{ke as default};
@@ -1,4 +1,4 @@
1
- import{m as et}from"./main.js";/*!-----------------------------------------------------------------------------
1
+ import{o as et}from"./main.js";/*!-----------------------------------------------------------------------------
2
2
  * Copyright (c) Microsoft Corporation. All rights reserved.
3
3
  * Version: 0.52.2(404545bded1df6ffa41ea0af4e8ddb219018c6c1)
4
4
  * Released under the MIT license