From 1dcc6551d9428638984639bde2c7c14aea91d664 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 9 Sep 2026 09:17:25 -0400 Subject: [PATCH] fix(core): preserve literal command arguments --- packages/core/src/config/plugin/command.ts | 2 +- packages/core/test/config/command.test.ts | 67 ++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/packages/core/src/config/plugin/command.ts b/packages/core/src/config/plugin/command.ts index 3be2d5bb7a6..8cef182c820 100644 --- a/packages/core/src/config/plugin/command.ts +++ b/packages/core/src/config/plugin/command.ts @@ -206,7 +206,7 @@ function evaluateTemplate( if (position === last) return args.slice(argIndex).join(" ") return args[argIndex] }) - const withArguments = expanded.replaceAll("$ARGUMENTS", input) + const withArguments = expanded.replaceAll("$ARGUMENTS", () => input) const text = placeholders.length === 0 && !template.includes("$ARGUMENTS") && input.trim() ? `${withArguments}\n\n${input}`.trim() diff --git a/packages/core/test/config/command.test.ts b/packages/core/test/config/command.test.ts index 35832b22a5f..7f7f04b40b8 100644 --- a/packages/core/test/config/command.test.ts +++ b/packages/core/test/config/command.test.ts @@ -71,6 +71,73 @@ const it = testEffect( const decode = Schema.decodeUnknownSync(Info) describe("ConfigCommandPlugin.Plugin", () => { + for (const item of [ + ...["$&", "$$", "$`", "$'"].flatMap((input) => [ + { template: "Explain $ARGUMENTS.", input, expected: `Explain ${input}.` }, + { template: "Explain $1.", input: `"${input}"`, expected: `Explain ${input}.` }, + { template: "Explain.", input, expected: `Explain.\n\n${input}` }, + ]), + ...["abc", "", "alpha beta", '"alpha beta"', "$1", "$"].map((input) => ({ + template: "Explain $ARGUMENTS.", + input, + expected: `Explain ${input}.`, + })), + { + template: "First $1. Rest $2.", + input: '"alpha beta" gamma delta', + expected: "First alpha beta. Rest gamma delta.", + }, + { template: "$ARGUMENTS / $ARGUMENTS", input: "$& $$", expected: "$& $$ / $& $$" }, + ]) { + it.live(`interpolates ${JSON.stringify(item.template)} with literal input ${JSON.stringify(item.input)}`, () => + Effect.gen(function* () { + const command = yield* Command.Service + const prompts: { text: string; delivery?: string }[] = [] + yield* ConfigCommandPlugin.Plugin.effect( + host({ + command: { + list: () => Effect.die(new Error("unused command.list")), + transform: command.transform, + reload: command.reload, + }, + session: { + prompt: (input) => + Effect.sync(() => { + prompts.push({ text: input.text, delivery: input.delivery }) + return SessionInbox.User.make({ + id: SessionMessage.ID.make("msg_test"), + sessionID: input.sessionID, + timeCreated: DateTime.makeUnsafe(0), + type: "user", + payload: { text: input.text }, + delivery: input.delivery ?? "steer", + }) + }), + }, + }), + ).pipe( + Effect.provide( + Config.testLayer([ + new Document({ + type: "document", + info: decode({ commands: { explain: { template: item.template } } }), + }), + ]), + ), + ) + yield* command.execute({ + name: "explain", + invocation: { + sessionID: Session.ID.make("ses_test"), + prompt: { text: item.input }, + delivery: "queue", + }, + }) + expect(prompts).toEqual([{ text: item.expected, delivery: "queue" }]) + }), + ) + } + it.live("loads inline and file-based commands in config order", () => Effect.acquireDisposable(Effect.promise(() => tmpdir())).pipe( Effect.flatMap((tmp) =>