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:
@@ -2,10 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import shlex
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
from app.models.config import PermissionsConfig
|
||||
from app.models.config import PermissionsConfig, ToolsConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -24,8 +26,13 @@ class PermissionsService:
|
||||
shows a modal dialog. Without a callback, unlisted tools are denied.
|
||||
"""
|
||||
|
||||
def __init__(self, config: PermissionsConfig) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
config: PermissionsConfig,
|
||||
tools_config: ToolsConfig | None = None,
|
||||
) -> None:
|
||||
self.config = config
|
||||
self._tools_config = tools_config
|
||||
self._prompt_callback: PromptCallback | None = None
|
||||
|
||||
def set_prompt_callback(self, callback: PromptCallback) -> None:
|
||||
@@ -36,9 +43,19 @@ class PermissionsService:
|
||||
"""
|
||||
self._prompt_callback = callback
|
||||
|
||||
async def check(self, tool_name: str, description: str = "") -> bool:
|
||||
async def check(
|
||||
self,
|
||||
tool_name: str,
|
||||
description: str = "",
|
||||
arguments: str = "",
|
||||
) -> bool:
|
||||
"""Check if a tool is permitted to run.
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool to check.
|
||||
description: Human-readable description for the prompt.
|
||||
arguments: Raw JSON arguments string (used for shell-aware checks).
|
||||
|
||||
Returns:
|
||||
True if permitted, False if denied.
|
||||
"""
|
||||
@@ -50,6 +67,12 @@ class PermissionsService:
|
||||
logger.debug("Tool '%s' is auto-approved", tool_name)
|
||||
return True
|
||||
|
||||
# Shell-aware: check allowed/denied command lists
|
||||
if tool_name == "run_command" and self._tools_config is not None:
|
||||
result = self._check_shell_command(arguments)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
# Prompt user via callback (TUI modal, etc.)
|
||||
if self._prompt_callback is not None:
|
||||
return await self._prompt_callback(tool_name, description)
|
||||
@@ -57,3 +80,41 @@ class PermissionsService:
|
||||
# No callback set — deny by default (safe fallback)
|
||||
logger.warning("Tool '%s' requires approval but no prompt callback set — denied", tool_name)
|
||||
return False
|
||||
|
||||
def _check_shell_command(self, arguments: str) -> bool | None:
|
||||
"""Check shell command against allowed/denied lists.
|
||||
|
||||
Returns True (allow), False (deny), or None (fall through to prompt).
|
||||
"""
|
||||
shell_config = self._tools_config.shell # type: ignore[union-attr]
|
||||
|
||||
try:
|
||||
cmd = json.loads(arguments).get("command", "")
|
||||
except (json.JSONDecodeError, AttributeError):
|
||||
return None # can't parse, fall through to prompt
|
||||
|
||||
try:
|
||||
base_cmd = shlex.split(cmd)[0]
|
||||
except (ValueError, IndexError):
|
||||
return None
|
||||
|
||||
# Denied commands: prefix match on full command string
|
||||
for denied in shell_config.denied_commands:
|
||||
if cmd.startswith(denied):
|
||||
logger.info("Shell command '%s' matches denied prefix '%s'", cmd, denied)
|
||||
return False
|
||||
|
||||
# Allowed commands: base executable match
|
||||
if shell_config.allowed_commands:
|
||||
if base_cmd in shell_config.allowed_commands:
|
||||
logger.debug(
|
||||
"Shell command '%s' auto-approved (base '%s' in allowed list)",
|
||||
cmd,
|
||||
base_cmd,
|
||||
)
|
||||
return True
|
||||
# Base command NOT in allowed list — fall through to prompt
|
||||
return None
|
||||
|
||||
# No allowed list configured — fall through to prompt
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user