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

60
app/tools/skills.py Normal file
View File

@@ -0,0 +1,60 @@
"""Load skill tool — allows the LLM to load skill instructions on demand."""
from __future__ import annotations
from pathlib import Path
from typing import Any, ClassVar
from pydantic import BaseModel, Field
from app.models.config import AppConfig
from app.models.tool_call import ToolResult, ToolResultStatus
from app.services.skills import SkillsManager
from app.tools.base import BaseTool
class LoadSkillParams(BaseModel):
"""Parameters for the load_skill tool."""
name: str = Field(description="Name of the skill to load")
class LoadSkillTool(BaseTool):
"""Load a skill's full instructions by name.
Use when a skill is relevant to the current task.
"""
name: ClassVar[str] = "load_skill"
description: ClassVar[str] = (
"Load a skill's full instructions by name. "
"Use when a skill is relevant to the current task."
)
params_model: ClassVar[type[BaseModel]] = LoadSkillParams
def __init__(
self,
workspace_root: Path,
config: AppConfig,
skills_manager: SkillsManager,
) -> None:
super().__init__(workspace_root, config)
self._skills = skills_manager
def execute(self, *, tool_call_id: str, **kwargs: Any) -> ToolResult:
skill_name: str = kwargs["name"]
content = self._skills.load_skill(skill_name)
if content is None:
available = [s.name for s in self._skills.list_skills()]
return ToolResult(
tool_call_id=tool_call_id,
tool_name=self.name,
status=ToolResultStatus.ERROR,
error=f"Unknown skill '{skill_name}'. Available: {available}",
)
return ToolResult(
tool_call_id=tool_call_id,
tool_name=self.name,
status=ToolResultStatus.SUCCESS,
output=content,
)