diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 04c11c2174..473789f01a 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -12,6 +12,7 @@ - Made Telegram reference-client capability diagnostics safe for TUI embedding. - A Telegram notification daemon whose reconciliation pass fails no longer exits. The pass persists through the shared topic authority, and a momentarily unavailable authority (lock contention or a rejected compare-and-set) rejected out of both the scan timer and the run loop into the process-level fatal handler, killing the owner. Every session topic was then left behind as an unarchived shell that answers nothing — including for sessions that were still live and lost their notifications. The pass now reports the failure and the next scan interval retries it; the queue-flush timer is guarded the same way. - MiniMax M3 preset and profile ids canonicalized to `MiniMax-M3` (issue #3896): the `minimax` / `minimax-cn` onboarding presets and the `minimax-eco` / `minimax-medium` / `minimax-pro` builtin model profiles no longer reference the removed lowercase `minimax-m3` / `minimax-v3` first-class catalog ids. +- Slash commands now expand in non-interactive runs. `gjc -p "/init"` previously reached the model as the literal text `/init`, so no command body was injected, no file was written, and the model still answered as if the command had run. Print mode now loads the same bundled and file-based command list interactive mode uses before it prompts. - `gjc plugin install ` now names the marketplaces that offer `` when the npm resolution it falls back to fails, so a plugin name copied out of `gjc plugin discover` no longer dead-ends on a bare `install_failed`. - A remote multi-select ask now shows what is already selected. The ask tool re-issues one remote request per toggle, but the request carried no selection state, so Telegram kept posting an identical prompt with no sign that option 1 had been picked — the checkbox rendering existed only for durable workflow gates. `AskAnswerRequest` now carries `multi` and the selected option labels, the notification bus publishes them as `selectedOptionIndices` with the `(N selected)` question prefix while keeping the ask tool's own Next/Done control, and pre-numbered options (deep interview) are renumbered once instead of rendering as `1. ☑ 1. …`. diff --git a/packages/coding-agent/src/modes/print-mode.ts b/packages/coding-agent/src/modes/print-mode.ts index 890c7d37b1..33f85a7c80 100644 --- a/packages/coding-agent/src/modes/print-mode.ts +++ b/packages/coding-agent/src/modes/print-mode.ts @@ -7,6 +7,7 @@ */ import { type AssistantMessage, type ImageContent, isContextOverflow } from "@gajae-code/ai"; import { isKnownSinkPeerClosedError, logger, sanitizeText } from "@gajae-code/utils"; +import { loadSlashCommands } from "../extensibility/slash-commands"; import type { AgentSession } from "../session/agent-session"; import { isSilentAbort } from "../session/messages"; import { initializeExtensions } from "./runtime-init"; @@ -207,6 +208,15 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti if (header) stdout.write(`${JSON.stringify(header)}\n`); } + // Bundled and file-based slash commands (`/init`, project `commands/*.md`, …) expand + // inside AgentSession#prompt from the list handed over by setSlashCommands. Interactive + // mode loads it while building its autocomplete; print mode has no autocomplete, so + // without this the list stays empty and a leading-slash prompt reaches the model as + // literal prose — no expansion, no error, and an answer that looks like the command ran. + await logger.time("print:slash-commands", async () => { + session.setSlashCommands(await loadSlashCommands({ cwd: session.sessionManager.getCwd() })); + }); + // Set up extensions for print mode (no UI, no command context). await initializeExtensions(session, { reportSendError: (action, err) => { diff --git a/packages/coding-agent/test/silent-abort-print-mode.test.ts b/packages/coding-agent/test/silent-abort-print-mode.test.ts index 82aec2a844..f1a4fc3931 100644 --- a/packages/coding-agent/test/silent-abort-print-mode.test.ts +++ b/packages/coding-agent/test/silent-abort-print-mode.test.ts @@ -70,7 +70,9 @@ function createMockSession( autoCompactionEnabled: opts?.autoCompactionEnabled ?? false, sessionManager: { getHeader: () => undefined, + getCwd: () => import.meta.dir, }, + setSlashCommands: () => {}, extensionRunner: undefined, subscribe: () => () => {}, prompt: async () => {}, @@ -113,11 +115,16 @@ function createPrintModeTrackingSession( for (const event of events) emit(event); lifecycle.push("prompt:end"); }); + // Print mode hands the loaded slash commands to the session before it prompts, so the + // fake models both seams. Ordering is pinned in its own test rather than in `lifecycle`, + // which the disposal/EPIPE cases assert byte-for-byte. + const setSlashCommands = vi.fn(); const session = { configWarnings: [], state: { messages }, - sessionManager: { getHeader: () => header }, + sessionManager: { getHeader: () => header, getCwd: () => import.meta.dir }, extensionRunner: undefined, + setSlashCommands, subscribe: (listener: (event: unknown) => void) => { lifecycle.push("subscribe"); onEvent = listener; @@ -135,6 +142,7 @@ function createPrintModeTrackingSession( session, dispose, prompt, + setSlashCommands, lifecycle, unsubscribeCount: () => unsubscribeCount, }; @@ -527,3 +535,28 @@ describe("Print mode", () => { } }); }); + +describe("Print mode slash-command expansion", () => { + it("hands bundled slash commands to the session before the first prompt", async () => { + const { runPrintMode } = await import("../src/modes/print-mode"); + const tracking = createPrintModeTrackingSession(); + installImmediateStdoutMock(); + + const order: string[] = []; + tracking.setSlashCommands.mockImplementation((commands: unknown) => { + order.push("setSlashCommands"); + const names = (commands as Array<{ name: string }>).map(cmd => cmd.name); + // `/init` is embedded in the binary, so it is present regardless of cwd. Without the + // handover the session keeps an empty list and `/init` reaches the model as prose. + expect(names).toContain("init"); + }); + tracking.prompt.mockImplementation(async () => { + order.push("prompt"); + }); + + await runPrintMode(tracking.session, { mode: "text", initialMessage: "/init" }); + + expect(tracking.setSlashCommands).toHaveBeenCalledTimes(1); + expect(order).toEqual(["setSlashCommands", "prompt"]); + }); +});