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:
2026-03-11 15:46:44 -05:00
parent 7600195ecf
commit 3f9012e6c2
13 changed files with 683 additions and 37 deletions

View File

@@ -1,7 +1,10 @@
"""AgentLoop — ReAct-style tool-call loop for autonomous task execution."""
from __future__ import annotations
import json
from typing import Any
import time
from typing import TYPE_CHECKING, Any
from app.agent.context import SessionContext
from app.models.config import AppConfig
@@ -14,6 +17,10 @@ from app.tools.registry import ToolRegistry
from app.utils.display import DisplayAdapter
from app.utils.logging import get_logger
if TYPE_CHECKING:
from app.services.debug_log import DebugLogger
from app.services.skills import SkillsManager
logger = get_logger(__name__)
_MAX_REASONING_RETRIES = 2
@@ -36,6 +43,8 @@ class AgentLoop:
registry: ToolRegistry,
permissions: PermissionsService,
display: DisplayAdapter | None = None,
debug_logger: DebugLogger | None = None,
skills_manager: SkillsManager | None = None,
) -> None:
self._config = config
self._ctx = ctx
@@ -44,6 +53,8 @@ class AgentLoop:
self._registry = registry
self._permissions = permissions
self._display = display
self._debug = debug_logger
self._skills = skills_manager
self._tools_schema = registry.get_openai_tools_schema()
self._system_prompt = self._build_system_prompt()
self._cancelled = False
@@ -55,7 +66,7 @@ class AgentLoop:
def _build_system_prompt(self) -> str:
"""Build the system prompt including tool schemas and agent instructions."""
tool_names = [t["function"]["name"] for t in self._tools_schema]
return (
prompt = (
"You are SneakyCode, a local AI coding agent. "
"You help users with software engineering tasks by reading files, "
"searching code, and answering questions about their project.\n\n"
@@ -68,6 +79,9 @@ class AgentLoop:
"with a brief summary. If you can answer directly without tools, just respond "
"with text (no tool call needed)."
)
if self._skills:
prompt += self._skills.get_system_prompt_snippet()
return prompt
def _get_messages_with_system_prompt(self) -> list[Message]:
"""Prepend the system prompt to conversation history."""
@@ -195,9 +209,16 @@ class AgentLoop:
The assistant Message, or None if an error occurred.
"""
messages = self._get_messages_with_system_prompt()
if self._debug:
self._debug.log_request(messages, self._config.llm.model)
t0 = time.monotonic()
try:
chunk_iter = self._client.stream_chat_with_retry(messages, tools=self._tools_schema)
return await self._handler.process_stream(chunk_iter)
result = await self._handler.process_stream(chunk_iter)
if result and self._debug:
elapsed = (time.monotonic() - t0) * 1000
self._debug.log_response(result, self._handler.usage, elapsed)
return result
except KeyboardInterrupt:
if self._display:
self._display.write_warning("Response interrupted.")
@@ -268,7 +289,7 @@ class AgentLoop:
# Check permissions (truncate args for display in prompt)
desc = tc.function.arguments[:120] + "..." if len(tc.function.arguments) > 120 else tc.function.arguments
if not await self._permissions.check(name, description=desc):
if not await self._permissions.check(name, description=desc, arguments=tc.function.arguments):
result = ToolResult(
tool_call_id=tc_id,
tool_name=name,
@@ -281,9 +302,14 @@ class AgentLoop:
continue
# Execute tool (BaseTool.run never raises)
tool_t0 = time.monotonic()
result = tool.run(tc_id, parsed_args)
results.append(result)
if self._debug:
tool_elapsed = (time.monotonic() - tool_t0) * 1000
self._debug.log_tool_execution(name, result.status.value, tool_elapsed)
if self._config.display.show_tool_calls and self._display:
is_error = result.status == ToolResultStatus.ERROR
output = result.error if is_error else result.output