feat(slash-commands): list project commands in AGENTS.md block (Phase 1.5)

The chat layer client-side-expands /<name> args, but the agent still
needs to know what commands exist so it can answer "what slash
commands does this project have?" and recognise the
<!-- scarf-slash:<name> --> marker prepended to expanded prompts.

ProjectContextBlock.renderMinimalBlock(...) gains an optional
slashCommandNames parameter; when non-empty, a new "Project slash
commands" bullet lists the names as backticked /<name> entries.

Mac's ProjectAgentContextService.renderBlock(for:) reads the names
via ProjectSlashCommandService.loadCommands(at:).map(\.name) and
emits the same bullet, keeping Mac and iOS block output aligned
where the content overlaps.

iOS chat resetAndStartInProject splits the slash-command load into a
synchronous read on a detached task BEFORE writing the block —
needed because the block has to land on disk before `hermes acp`
boots, and the async load that populates the chat menu would lose
the race.

Verified: ScarfCore, Mac, iOS all build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alan Wizemann
2026-04-25 08:40:15 +02:00
parent 6808adfa98
commit 8a87ff1922
3 changed files with 47 additions and 4 deletions
+15 -3
View File
@@ -612,6 +612,19 @@ final class ChatController {
// <project.path>/.scarf/slash-commands/ into the chat menu.
// Async + non-fatal degrades cleanly on SFTP failures (logged).
vm.loadProjectScopedCommands(at: project.path)
// Synchronously load the slash command NAMES so we can list them
// in the AGENTS.md block (the agent needs to know what commands
// are available). This is a separate read from the async one
// above because the block has to land on disk BEFORE `hermes acp`
// boots async loads might lose the race. Blocking load on a
// detached task to keep the MainActor responsive.
let ctx = context
let projectPath = project.path
let slashNames: [String] = await Task.detached {
ProjectSlashCommandService(context: ctx)
.loadCommands(at: projectPath)
.map(\.name)
}.value
// Write the context block first. Non-fatal on failure chat
// still starts, just without the managed block. We capture the
// failure (rather than swallowing via `try?`) so the user gets
@@ -619,10 +632,9 @@ final class ChatController {
// for this session, with the underlying error in "Show details".
let block = ProjectContextBlock.renderMinimalBlock(
projectName: project.name,
projectPath: project.path
projectPath: project.path,
slashCommandNames: slashNames
)
let ctx = context
let projectPath = project.path
let writeResult: Result<Void, Error> = await Task.detached {
do {
try ProjectContextBlock.writeBlock(