feat(headless): Implement Phase 4 - Session & Context Management via API
This commit is contained in:
115
gui_2.py
115
gui_2.py
@@ -6,6 +6,7 @@ import math
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from tkinter import filedialog, Tk
|
||||
import aggregate
|
||||
@@ -90,10 +91,8 @@ def _parse_history_entries(history: list[str], roles: list[str] | None = None) -
|
||||
|
||||
|
||||
class ConfirmDialog:
|
||||
_next_id = 0
|
||||
def __init__(self, script: str, base_dir: str):
|
||||
ConfirmDialog._next_id += 1
|
||||
self._uid = ConfirmDialog._next_id
|
||||
self._uid = str(uuid.uuid4())
|
||||
self._script = str(script) if script is not None else ""
|
||||
self._base_dir = str(base_dir) if base_dir is not None else ""
|
||||
self._condition = threading.Condition()
|
||||
@@ -191,6 +190,7 @@ class App:
|
||||
self._pending_dialog: ConfirmDialog | None = None
|
||||
self._pending_dialog_open = False
|
||||
self._pending_dialog_lock = threading.Lock()
|
||||
self._pending_actions: dict[str, ConfirmDialog] = {}
|
||||
|
||||
self._tool_log: list[tuple[str, str]] = []
|
||||
self._comms_log: list[dict] = []
|
||||
@@ -315,6 +315,9 @@ class App:
|
||||
temperature: float | None = None
|
||||
max_tokens: int | None = None
|
||||
|
||||
class ConfirmRequest(BaseModel):
|
||||
approved: bool
|
||||
|
||||
API_KEY_NAME = "X-API-KEY"
|
||||
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
|
||||
|
||||
@@ -343,6 +346,75 @@ class App:
|
||||
"session_usage": self.session_usage
|
||||
}
|
||||
|
||||
@api.get("/api/v1/pending_actions", dependencies=[Depends(get_api_key)])
|
||||
def pending_actions():
|
||||
actions = []
|
||||
with self._pending_dialog_lock:
|
||||
# Include multi-actions from headless mode
|
||||
for uid, dialog in self._pending_actions.items():
|
||||
actions.append({
|
||||
"action_id": uid,
|
||||
"script": dialog._script,
|
||||
"base_dir": dialog._base_dir
|
||||
})
|
||||
# Include single active dialog from GUI mode
|
||||
if self._pending_dialog:
|
||||
actions.append({
|
||||
"action_id": self._pending_dialog._uid,
|
||||
"script": self._pending_dialog._script,
|
||||
"base_dir": self._pending_dialog._base_dir
|
||||
})
|
||||
return actions
|
||||
|
||||
@api.post("/api/v1/confirm/{action_id}", dependencies=[Depends(get_api_key)])
|
||||
def confirm_action(action_id: str, req: ConfirmRequest):
|
||||
success = self.resolve_pending_action(action_id, req.approved)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail=f"Action ID {action_id} not found")
|
||||
return {"status": "success", "action_id": action_id, "approved": req.approved}
|
||||
|
||||
@api.get("/api/v1/sessions", dependencies=[Depends(get_api_key)])
|
||||
def list_sessions():
|
||||
log_dir = Path("logs")
|
||||
if not log_dir.exists():
|
||||
return []
|
||||
return sorted([f.name for f in log_dir.glob("*.log")], reverse=True)
|
||||
|
||||
@api.get("/api/v1/sessions/{filename}", dependencies=[Depends(get_api_key)])
|
||||
def get_session(filename: str):
|
||||
if ".." in filename or "/" in filename or "\\" in filename:
|
||||
raise HTTPException(status_code=400, detail="Invalid filename")
|
||||
log_path = Path("logs") / filename
|
||||
if not log_path.exists() or not log_path.is_file():
|
||||
raise HTTPException(status_code=404, detail="Session log not found")
|
||||
try:
|
||||
content = log_path.read_text(encoding="utf-8")
|
||||
return {"filename": filename, "content": content}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@api.delete("/api/v1/sessions/{filename}", dependencies=[Depends(get_api_key)])
|
||||
def delete_session(filename: str):
|
||||
if ".." in filename or "/" in filename or "\\" in filename:
|
||||
raise HTTPException(status_code=400, detail="Invalid filename")
|
||||
log_path = Path("logs") / filename
|
||||
if not log_path.exists() or not log_path.is_file():
|
||||
raise HTTPException(status_code=404, detail="Session log not found")
|
||||
try:
|
||||
log_path.unlink()
|
||||
return {"status": "success", "message": f"Deleted {filename}"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@api.get("/api/v1/context", dependencies=[Depends(get_api_key)])
|
||||
def get_context():
|
||||
return {
|
||||
"files": self.files,
|
||||
"screenshots": self.screenshots,
|
||||
"files_base_dir": self.ui_files_base_dir,
|
||||
"screenshots_base_dir": self.ui_shots_base_dir
|
||||
}
|
||||
|
||||
@api.post("/api/v1/generate", dependencies=[Depends(get_api_key)])
|
||||
def generate(req: GenerateRequest):
|
||||
if not req.prompt.strip():
|
||||
@@ -856,8 +928,16 @@ class App:
|
||||
def _confirm_and_run(self, script: str, base_dir: str) -> str | None:
|
||||
print(f"[DEBUG] _confirm_and_run triggered for script length: {len(script)}")
|
||||
dialog = ConfirmDialog(script, base_dir)
|
||||
with self._pending_dialog_lock:
|
||||
self._pending_dialog = dialog
|
||||
|
||||
is_headless = "--headless" in sys.argv
|
||||
|
||||
if is_headless:
|
||||
with self._pending_dialog_lock:
|
||||
self._pending_actions[dialog._uid] = dialog
|
||||
print(f"[PENDING_ACTION] Created action {dialog._uid}")
|
||||
else:
|
||||
with self._pending_dialog_lock:
|
||||
self._pending_dialog = dialog
|
||||
|
||||
# Notify API hook subscribers
|
||||
if self.test_hooks_enabled and hasattr(self, '_api_event_queue'):
|
||||
@@ -865,12 +945,19 @@ class App:
|
||||
with self._api_event_queue_lock:
|
||||
self._api_event_queue.append({
|
||||
"type": "script_confirmation_required",
|
||||
"action_id": dialog._uid,
|
||||
"script": str(script),
|
||||
"base_dir": str(base_dir),
|
||||
"ts": time.time()
|
||||
})
|
||||
|
||||
approved, final_script = dialog.wait()
|
||||
|
||||
if is_headless:
|
||||
with self._pending_dialog_lock:
|
||||
if dialog._uid in self._pending_actions:
|
||||
del self._pending_actions[dialog._uid]
|
||||
|
||||
print(f"[DEBUG] _confirm_and_run result: approved={approved}")
|
||||
if not approved:
|
||||
self._append_tool_log(final_script, "REJECTED by user")
|
||||
@@ -883,6 +970,24 @@ class App:
|
||||
self.ai_status = "powershell done, awaiting AI..."
|
||||
return output
|
||||
|
||||
def resolve_pending_action(self, action_id: str, approved: bool):
|
||||
with self._pending_dialog_lock:
|
||||
if action_id in self._pending_actions:
|
||||
dialog = self._pending_actions[action_id]
|
||||
with dialog._condition:
|
||||
dialog._approved = approved
|
||||
dialog._done = True
|
||||
dialog._condition.notify_all()
|
||||
return True
|
||||
elif self._pending_dialog and self._pending_dialog._uid == action_id:
|
||||
dialog = self._pending_dialog
|
||||
with dialog._condition:
|
||||
dialog._approved = approved
|
||||
dialog._done = True
|
||||
dialog._condition.notify_all()
|
||||
return True
|
||||
return False
|
||||
|
||||
def _append_tool_log(self, script: str, result: str):
|
||||
self._tool_log.append((script, result, time.time()))
|
||||
self.ui_last_script_text = script
|
||||
|
||||
Reference in New Issue
Block a user