feat: implement tweaks plan - modals, smart shell, spinner, /models, debug log, skills
Phase 1: Permission modal dialog, session resume modal, HistoryInput with up/down arrow cycling, remove "You:" echo from chat log, LLM client cleanup on unmount. Phase 2: Smart shell auto-approve using allowed/denied command lists from ToolsConfig, animated thinking spinner with live token count in status bar. Phase 3: /models slash command (list + switch), CLI directory positional argument, JSONL debug logger with rotation. Phase 4: Skills system with SkillsManager, load_skill LLM tool, /skills listing, skill invocation via slash commands, system prompt integration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
77
app/services/debug_log.py
Normal file
77
app/services/debug_log.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""Debug logger — writes detailed LLM interaction logs to JSONL files."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from app.models.message import Message
|
||||
|
||||
|
||||
class DebugLogger:
|
||||
"""Writes detailed LLM interaction logs to JSONL files for debugging."""
|
||||
|
||||
def __init__(self, log_dir: Path, max_files: int = 10) -> None:
|
||||
self._log_dir = log_dir
|
||||
self._log_dir.mkdir(parents=True, exist_ok=True)
|
||||
self._max_files = max_files
|
||||
self._file = self._log_dir / f"debug_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.jsonl"
|
||||
self._rotate()
|
||||
|
||||
def log_request(self, messages: list[Message], model: str) -> None:
|
||||
"""Log outbound LLM request (message roles/lengths, model)."""
|
||||
self._write({
|
||||
"event": "llm_request",
|
||||
"model": model,
|
||||
"message_count": len(messages),
|
||||
"messages": [
|
||||
{"role": m.role, "content_len": len(m.content or "")}
|
||||
for m in messages
|
||||
],
|
||||
})
|
||||
|
||||
def log_response(
|
||||
self,
|
||||
message: Message,
|
||||
usage: Any | None,
|
||||
elapsed_ms: float,
|
||||
) -> None:
|
||||
"""Log LLM response with timing and token counts."""
|
||||
self._write({
|
||||
"event": "llm_response",
|
||||
"elapsed_ms": round(elapsed_ms, 1),
|
||||
"content_len": len(message.content or ""),
|
||||
"tool_call_count": len(message.tool_calls or []),
|
||||
"tool_calls": [tc.function.name for tc in (message.tool_calls or [])],
|
||||
"usage": usage.__dict__ if usage else None,
|
||||
})
|
||||
|
||||
def log_tool_execution(
|
||||
self,
|
||||
tool_name: str,
|
||||
result_status: str,
|
||||
elapsed_ms: float,
|
||||
) -> None:
|
||||
"""Log tool execution with timing."""
|
||||
self._write({
|
||||
"event": "tool_execution",
|
||||
"tool": tool_name,
|
||||
"status": result_status,
|
||||
"elapsed_ms": round(elapsed_ms, 1),
|
||||
})
|
||||
|
||||
def _write(self, record: dict[str, Any]) -> None:
|
||||
record["timestamp"] = datetime.now(UTC).isoformat()
|
||||
with open(self._file, "a") as f:
|
||||
f.write(json.dumps(record) + "\n")
|
||||
|
||||
def _rotate(self) -> None:
|
||||
"""Remove old debug log files beyond max_files."""
|
||||
files = sorted(
|
||||
self._log_dir.glob("debug_*.jsonl"),
|
||||
key=lambda p: p.stat().st_mtime,
|
||||
)
|
||||
while len(files) > self._max_files:
|
||||
files.pop(0).unlink()
|
||||
Reference in New Issue
Block a user