mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 02:26:37 +00:00
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:
@@ -107,7 +107,17 @@ public enum ProjectContextBlock {
|
||||
/// yet. The marker + identity headers match the Mac output byte-
|
||||
/// for-byte where the content overlaps, so a project scaffolded
|
||||
/// on iOS round-trips cleanly through the Mac.
|
||||
public static func renderMinimalBlock(projectName: String, projectPath: String) -> String {
|
||||
///
|
||||
/// `slashCommandNames` populates the v2.5 "Project slash commands"
|
||||
/// line — pass nil/empty to omit that line entirely. The names
|
||||
/// flow into the agent's context so it can answer "what commands
|
||||
/// do I have available?" and recognise the `<!-- scarf-slash:<name> -->`
|
||||
/// marker the chat layer prepends to expanded prompts.
|
||||
public static func renderMinimalBlock(
|
||||
projectName: String,
|
||||
projectPath: String,
|
||||
slashCommandNames: [String]? = nil
|
||||
) -> String {
|
||||
var lines: [String] = []
|
||||
lines.append(beginMarker)
|
||||
lines.append("## Scarf project context")
|
||||
@@ -118,6 +128,10 @@ public enum ProjectContextBlock {
|
||||
lines.append("")
|
||||
lines.append("- **Project directory:** `\(projectPath)`")
|
||||
lines.append("- **Dashboard:** `\(projectPath)/.scarf/dashboard.json`")
|
||||
if let names = slashCommandNames, !names.isEmpty {
|
||||
let formatted = names.sorted().map { "`/\($0)`" }.joined(separator: ", ")
|
||||
lines.append("- **Project slash commands:** \(formatted). The user invokes these via the chat slash menu; you'll see the expanded prompt as a normal user message preceded by `<!-- scarf-slash:<name> -->`.")
|
||||
}
|
||||
lines.append("")
|
||||
lines.append("Any content below this block is template- or user-authored; preserve and defer to it for project-specific behavior. Do NOT modify content inside these markers — Scarf rewrites this block on every project-scoped chat start.")
|
||||
lines.append(endMarker)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -129,6 +129,7 @@ struct ProjectAgentContextService: Sendable {
|
||||
let templateInfo = readTemplateInfo(for: project)
|
||||
let configFieldsLine = renderConfigFieldsLine(for: project)
|
||||
let cronLines = renderCronLines(for: project, templateId: templateInfo?.id)
|
||||
let slashCommandNames = readSlashCommandNames(for: project)
|
||||
let lockFilePresent = context.makeTransport().fileExists(
|
||||
project.path + "/.scarf/template.lock.json"
|
||||
)
|
||||
@@ -158,6 +159,11 @@ struct ProjectAgentContextService: Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
if !slashCommandNames.isEmpty {
|
||||
let formatted = slashCommandNames.sorted().map { "`/\($0)`" }.joined(separator: ", ")
|
||||
lines.append("- **Project slash commands:** \(formatted). The user invokes these via the chat slash menu; you'll see the expanded prompt as a normal user message preceded by `<!-- scarf-slash:<name> -->`.")
|
||||
}
|
||||
|
||||
if lockFilePresent {
|
||||
lines.append("- **Uninstall manifest:** `\(project.path)/.scarf/template.lock.json` (tracks files written by template install)")
|
||||
}
|
||||
@@ -169,6 +175,17 @@ struct ProjectAgentContextService: Sendable {
|
||||
return lines.joined(separator: "\n")
|
||||
}
|
||||
|
||||
/// Read the names of every project-scoped slash command at
|
||||
/// `<project>/.scarf/slash-commands/`. Empty array when the dir
|
||||
/// is absent or no `.md` files parse cleanly. Used by `renderBlock`
|
||||
/// to surface the available commands to the agent so it knows what
|
||||
/// `<!-- scarf-slash:<name> -->` markers to expect on user prompts.
|
||||
nonisolated private func readSlashCommandNames(for project: ProjectEntry) -> [String] {
|
||||
ProjectSlashCommandService(context: context)
|
||||
.loadCommands(at: project.path)
|
||||
.map(\.name)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
nonisolated private func agentsMdPath(for project: ProjectEntry) -> String {
|
||||
|
||||
Reference in New Issue
Block a user