first commit
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any, Callable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .agent_runtime import LocalCodingAgent
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ParsedSlashCommand:
|
||||
command_name: str
|
||||
args: str
|
||||
is_mcp: bool
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SlashCommandResult:
|
||||
handled: bool
|
||||
should_query: bool
|
||||
prompt: str | None = None
|
||||
output: str = ''
|
||||
transcript: tuple[dict[str, Any], ...] = ()
|
||||
|
||||
|
||||
SlashCommandHandler = Callable[['LocalCodingAgent', str, str], SlashCommandResult]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SlashCommandSpec:
|
||||
names: tuple[str, ...]
|
||||
description: str
|
||||
handler: SlashCommandHandler
|
||||
|
||||
|
||||
def parse_slash_command(input_text: str) -> ParsedSlashCommand | None:
|
||||
trimmed = input_text.strip()
|
||||
if not trimmed.startswith('/'):
|
||||
return None
|
||||
|
||||
without_slash = trimmed[1:]
|
||||
words = without_slash.split(' ')
|
||||
if not words or not words[0]:
|
||||
return None
|
||||
|
||||
command_name = words[0]
|
||||
is_mcp = False
|
||||
args_start_index = 1
|
||||
if len(words) > 1 and words[1] == '(MCP)':
|
||||
command_name = f'{command_name} (MCP)'
|
||||
is_mcp = True
|
||||
args_start_index = 2
|
||||
|
||||
return ParsedSlashCommand(
|
||||
command_name=command_name,
|
||||
args=' '.join(words[args_start_index:]),
|
||||
is_mcp=is_mcp,
|
||||
)
|
||||
|
||||
|
||||
def looks_like_command(command_name: str) -> bool:
|
||||
return re.search(r'[^a-zA-Z0-9:\-_]', command_name) is None
|
||||
|
||||
|
||||
def preprocess_slash_command(
|
||||
agent: 'LocalCodingAgent',
|
||||
input_text: str,
|
||||
) -> SlashCommandResult:
|
||||
if not input_text.strip().startswith('/'):
|
||||
return SlashCommandResult(handled=False, should_query=True, prompt=input_text)
|
||||
|
||||
parsed = parse_slash_command(input_text)
|
||||
if parsed is None:
|
||||
return _local_result(
|
||||
input_text,
|
||||
'Commands are in the form `/command [args]`.',
|
||||
)
|
||||
|
||||
if parsed.is_mcp:
|
||||
return _local_result(
|
||||
input_text,
|
||||
'MCP slash commands are not implemented in the Python runtime yet.',
|
||||
)
|
||||
|
||||
spec = find_slash_command(parsed.command_name)
|
||||
if spec is None:
|
||||
if looks_like_command(parsed.command_name):
|
||||
return _local_result(input_text, f'Unknown skill: {parsed.command_name}')
|
||||
return SlashCommandResult(handled=False, should_query=True, prompt=input_text)
|
||||
|
||||
return spec.handler(agent, parsed.args.strip(), input_text)
|
||||
|
||||
|
||||
def get_slash_command_specs() -> tuple[SlashCommandSpec, ...]:
|
||||
return (
|
||||
SlashCommandSpec(
|
||||
names=('help', 'commands'),
|
||||
description='Show the built-in Python slash commands.',
|
||||
handler=_handle_help,
|
||||
),
|
||||
SlashCommandSpec(
|
||||
names=('context', 'usage'),
|
||||
description='Show estimated session context usage similar to the npm /context command.',
|
||||
handler=_handle_context,
|
||||
),
|
||||
SlashCommandSpec(
|
||||
names=('context-raw', 'env'),
|
||||
description='Show the raw environment, user context, and system context snapshot.',
|
||||
handler=_handle_context_raw,
|
||||
),
|
||||
SlashCommandSpec(
|
||||
names=('prompt', 'system-prompt'),
|
||||
description='Render the effective Python system prompt.',
|
||||
handler=_handle_prompt,
|
||||
),
|
||||
SlashCommandSpec(
|
||||
names=('permissions',),
|
||||
description='Show the active tool permission mode.',
|
||||
handler=_handle_permissions,
|
||||
),
|
||||
SlashCommandSpec(
|
||||
names=('model',),
|
||||
description='Show or update the active model for the current agent instance.',
|
||||
handler=_handle_model,
|
||||
),
|
||||
SlashCommandSpec(
|
||||
names=('tools',),
|
||||
description='List the registered tools and whether the current permissions allow them.',
|
||||
handler=_handle_tools,
|
||||
),
|
||||
SlashCommandSpec(
|
||||
names=('memory',),
|
||||
description='Show the currently loaded CLAUDE.md memory bundle and discovered files.',
|
||||
handler=_handle_memory,
|
||||
),
|
||||
SlashCommandSpec(
|
||||
names=('status', 'session'),
|
||||
description='Show a short runtime/session status summary.',
|
||||
handler=_handle_status,
|
||||
),
|
||||
SlashCommandSpec(
|
||||
names=('clear',),
|
||||
description='Clear ephemeral Python runtime state for this process.',
|
||||
handler=_handle_clear,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def find_slash_command(command_name: str) -> SlashCommandSpec | None:
|
||||
lowered = command_name.lower()
|
||||
for spec in get_slash_command_specs():
|
||||
if lowered in spec.names:
|
||||
return spec
|
||||
return None
|
||||
|
||||
|
||||
def _handle_help(agent: 'LocalCodingAgent', _args: str, input_text: str) -> SlashCommandResult:
|
||||
lines = ['# Slash Commands', '']
|
||||
for spec in get_slash_command_specs():
|
||||
primary = f'/{spec.names[0]}'
|
||||
aliases = ', '.join(f'/{name}' for name in spec.names[1:])
|
||||
label = f'{primary} ({aliases})' if aliases else primary
|
||||
lines.append(f'- `{label}`: {spec.description}')
|
||||
lines.extend(
|
||||
[
|
||||
'',
|
||||
'These commands are handled locally before the model loop, similar to the npm runtime.',
|
||||
]
|
||||
)
|
||||
return _local_result(input_text, '\n'.join(lines))
|
||||
|
||||
|
||||
def _handle_context(agent: 'LocalCodingAgent', args: str, input_text: str) -> SlashCommandResult:
|
||||
prompt = args or None
|
||||
return _local_result(input_text, agent.render_context_report(prompt))
|
||||
|
||||
|
||||
def _handle_context_raw(agent: 'LocalCodingAgent', _args: str, input_text: str) -> SlashCommandResult:
|
||||
return _local_result(input_text, agent.render_context_snapshot_report())
|
||||
|
||||
|
||||
def _handle_prompt(agent: 'LocalCodingAgent', _args: str, input_text: str) -> SlashCommandResult:
|
||||
return _local_result(input_text, agent.render_system_prompt())
|
||||
|
||||
|
||||
def _handle_permissions(agent: 'LocalCodingAgent', _args: str, input_text: str) -> SlashCommandResult:
|
||||
return _local_result(input_text, agent.render_permissions_report())
|
||||
|
||||
|
||||
def _handle_model(agent: 'LocalCodingAgent', args: str, input_text: str) -> SlashCommandResult:
|
||||
if not args:
|
||||
return _local_result(input_text, f'Current model: {agent.model_config.model}')
|
||||
agent.set_model(args)
|
||||
return _local_result(input_text, f'Set model to {agent.model_config.model}')
|
||||
|
||||
|
||||
def _handle_tools(agent: 'LocalCodingAgent', _args: str, input_text: str) -> SlashCommandResult:
|
||||
return _local_result(input_text, agent.render_tools_report())
|
||||
|
||||
|
||||
def _handle_memory(agent: 'LocalCodingAgent', _args: str, input_text: str) -> SlashCommandResult:
|
||||
return _local_result(input_text, agent.render_memory_report())
|
||||
|
||||
|
||||
def _handle_status(agent: 'LocalCodingAgent', _args: str, input_text: str) -> SlashCommandResult:
|
||||
return _local_result(input_text, agent.render_status_report())
|
||||
|
||||
|
||||
def _handle_clear(agent: 'LocalCodingAgent', _args: str, input_text: str) -> SlashCommandResult:
|
||||
agent.clear_runtime_state()
|
||||
return _local_result(
|
||||
input_text,
|
||||
'Cleared ephemeral Python agent state for this process.',
|
||||
)
|
||||
|
||||
|
||||
def _local_result(input_text: str, output: str) -> SlashCommandResult:
|
||||
transcript = (
|
||||
{'role': 'user', 'content': input_text},
|
||||
{'role': 'assistant', 'content': output},
|
||||
)
|
||||
return SlashCommandResult(
|
||||
handled=True,
|
||||
should_query=False,
|
||||
output=output,
|
||||
transcript=transcript,
|
||||
)
|
||||
Reference in New Issue
Block a user