Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name>` now names the marketplaces that offer `<name>` 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. …`.

Expand Down
10 changes: 10 additions & 0 deletions packages/coding-agent/src/modes/print-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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) => {
Expand Down
35 changes: 34 additions & 1 deletion packages/coding-agent/test/silent-abort-print-mode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ function createMockSession(
autoCompactionEnabled: opts?.autoCompactionEnabled ?? false,
sessionManager: {
getHeader: () => undefined,
getCwd: () => import.meta.dir,
},
setSlashCommands: () => {},
extensionRunner: undefined,
subscribe: () => () => {},
prompt: async () => {},
Expand Down Expand Up @@ -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;
Expand All @@ -135,6 +142,7 @@ function createPrintModeTrackingSession(
session,
dispose,
prompt,
setSlashCommands,
lifecycle,
unsubscribeCount: () => unsubscribeCount,
};
Expand Down Expand Up @@ -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"]);
});
});
Loading