File size: 6,575 Bytes
5ef3875 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | """Built-in slash commands, handled locally (never sent to the model)."""
from __future__ import annotations
import re
from dataclasses import dataclass
from typing import TYPE_CHECKING, Callable
from rich.text import Text
from ycode.config import APPROVAL_MODES
from ycode.tools import git
if TYPE_CHECKING:
from ycode.app import YCodeApp
_COMMAND_RE = re.compile(r"^/([a-zA-Z][\w-]*)(?:\s+(.*))?$", re.DOTALL)
@dataclass(frozen=True)
class Command:
name: str
usage: str
help: str
handler: Callable[["YCodeApp", str], None]
def _help(app: "YCodeApp", arg: str) -> None:
app.ui.console.print(Text(" Commands", style="key"))
app.ui.print_table([(c.usage, c.help) for c in COMMANDS.values() if c.name != "quit"])
app.ui.console.print()
app.ui.info("Anything else is sent to the agent as a task. Ctrl+C interrupts the agent;")
app.ui.info("Ctrl+D or /exit quits. Esc+Enter inserts a newline.")
def _clear(app: "YCodeApp", arg: str) -> None:
app.agent.reset()
app.ui.console.clear()
app.ui.info("Conversation cleared. The agent has forgotten previous messages.")
def _status(app: "YCodeApp", arg: str) -> None:
ws = app.workspace
rows = [("Project", ws.name or str(ws)), ("Directory", str(ws))]
if app.context.is_git_repo:
rows.append(("Branch", git.current_branch(ws) or "(unknown)"))
rows.append(("Modified files", str(len(git.status_entries(ws)))))
else:
rows.append(("Git", "not a repository"))
usage = app.agent.usage
rows += [
("Model", app.provider.display_name),
("Steps used", f"{app.agent.last_steps}/{app.agent.max_steps} (last task), {app.agent.total_steps} total"),
("Tokens", f"{usage.input_tokens:,} in, {usage.output_tokens:,} out, "
f"{usage.cache_read_tokens:,} cache reads"),
("Approvals", app.permissions.mode),
("Messages", str(len(app.agent.messages))),
("Files changed", str(len(app.tool_context.changed_files)) + " this session"),
]
if app.context.instructions_path:
rows.append(("Instructions", app.context.instructions_path))
app.ui.print_table(rows)
def _diff(app: "YCodeApp", arg: str) -> None:
if not app.context.is_git_repo:
app.ui.info("Not a git repository.")
return
args = arg.split()
full = "full" in args
paths = [a for a in args if a != "full"]
diff = git.diff_text(app.workspace, paths=paths or None)
untracked = [p for p in git.untracked_files(app.workspace) if not paths or p in paths]
if not diff.strip() and not untracked:
app.ui.info("No changes.")
return
lines = diff.splitlines()
if diff.strip():
if len(lines) > 400 and not full:
app.ui.console.print(git.diff_text(app.workspace, paths=paths or None, stat=True).rstrip())
app.ui.info(f"Diff is {len(lines)} lines. Use `/diff full` or `/diff <path>` to see it.")
else:
app.ui.print_diff(diff)
if untracked:
app.ui.console.print(Text(" Untracked files:", style="key"))
for path in untracked:
app.ui.console.print(Text(f" A {path}", style="ok"))
def _files(app: "YCodeApp", arg: str) -> None:
session = sorted(app.tool_context.changed_files)
if app.context.is_git_repo:
entries = git.status_entries(app.workspace)
if entries:
app.ui.console.print(Text(" Working tree changes:", style="key"))
app.ui.print_changes(entries)
else:
app.ui.info("Working tree clean.")
if session:
app.ui.console.print(Text(" Written by YCode this session:", style="key"))
for path in session:
app.ui.console.print(f" {path}")
elif not app.context.is_git_repo:
app.ui.info("No files changed this session.")
def _model(app: "YCodeApp", arg: str) -> None:
name = arg.strip()
if app.provider.name == "local":
app.ui.info(f"Model: {app.provider.display_name}")
app.ui.info("Start YCode with `--local <model dir>` to use a different local model.")
return
if not name:
app.ui.info(f"Model: {app.provider.display_name}")
app.ui.info("Switch with /model <model-id>, e.g. /model claude-sonnet-5-5")
return
app.provider.model = name
app.ui.info(f"Model set to {name} for this session.")
def _plan(app: "YCodeApp", arg: str) -> None:
app.ui.print_plan(app.agent.plan)
def _approval(app: "YCodeApp", arg: str) -> None:
mode = arg.strip().lower()
if not mode:
app.ui.info(f"Approval mode: {app.permissions.mode} (options: {', '.join(APPROVAL_MODES)})")
return
if mode not in APPROVAL_MODES:
app.ui.info(f"Unknown mode {mode!r}. Options: {', '.join(APPROVAL_MODES)}")
return
app.permissions.mode = mode
note = " Blocked commands stay blocked." if mode == "auto" else ""
app.ui.info(f"Approval mode set to {mode}.{note}")
def _exit(app: "YCodeApp", arg: str) -> None:
app.should_exit = True
COMMANDS: dict[str, Command] = {
c.name: c
for c in [
Command("help", "/help", "Show this help", _help),
Command("clear", "/clear", "Clear the conversation and the screen", _clear),
Command("status", "/status", "Project, branch, model, steps and token usage", _status),
Command("diff", "/diff [full] [path...]", "Show the current git diff", _diff),
Command("files", "/files", "List changed files", _files),
Command("model", "/model [id]", "Show or switch the model", _model),
Command("plan", "/plan", "Show the current plan", _plan),
Command("approval", "/approval [mode]", "Show or set approval mode (strict|normal|auto)", _approval),
Command("exit", "/exit", "Quit YCode (also /quit, Ctrl+D)", _exit),
Command("quit", "/quit", "Quit YCode", _exit),
]
}
def command_names() -> list[str]:
return [f"/{name}" for name in COMMANDS]
def is_command(text: str) -> bool:
match = _COMMAND_RE.match(text.strip())
return bool(match)
def handle_command(app: "YCodeApp", text: str) -> bool:
"""Run a slash command. Returns False if it was not recognised."""
match = _COMMAND_RE.match(text.strip())
if not match:
return False
name, arg = match.group(1).lower(), match.group(2) or ""
command = COMMANDS.get(name)
if command is None:
app.ui.info(f"Unknown command /{name}. Type /help for the list of commands.")
return False
command.handler(app, arg)
return True
|