@@ -9,6 +9,7 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed prompt template argument parsing to split unquoted multiline input on newlines ([#4553](https://github.com/earendil-works/pi/issues/4553)).
|
||||
- Fixed `--resume` session listing to cap in-flight session metadata loads and avoid OOM on large session histories ([#4583](https://github.com/earendil-works/pi/issues/4583)).
|
||||
- Fixed interactive error messages to render with trailing spacing so reload errors do not run into resource listings ([#4510](https://github.com/earendil-works/pi/issues/4510)).
|
||||
- Fixed nested code fences in the Termux setup documentation so the example AGENTS.md renders correctly ([#4503](https://github.com/earendil-works/pi/issues/4503)).
|
||||
|
||||
@@ -37,7 +37,7 @@ export function parseCommandArgs(argsString: string): string[] {
|
||||
}
|
||||
} else if (char === '"' || char === "'") {
|
||||
inQuote = char;
|
||||
} else if (char === " " || char === "\t") {
|
||||
} else if (/\s/.test(char)) {
|
||||
if (current) {
|
||||
args.push(current);
|
||||
current = "";
|
||||
@@ -282,9 +282,11 @@ export function loadPromptTemplates(options: LoadPromptTemplatesOptions): Prompt
|
||||
export function expandPromptTemplate(text: string, templates: PromptTemplate[]): string {
|
||||
if (!text.startsWith("/")) return text;
|
||||
|
||||
const spaceIndex = text.indexOf(" ");
|
||||
const templateName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
|
||||
const argsString = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1);
|
||||
const match = text.match(/^\/([^\s]+)(?:\s+([\s\S]*))?$/);
|
||||
if (!match) return text;
|
||||
|
||||
const templateName = match[1];
|
||||
const argsString = match[2] ?? "";
|
||||
|
||||
const template = templates.find((t) => t.name === templateName);
|
||||
if (template) {
|
||||
|
||||
@@ -13,7 +13,12 @@ import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
import { afterAll, describe, expect, test } from "vitest";
|
||||
import { getAgentDir } from "../src/config.js";
|
||||
import { loadPromptTemplates, parseCommandArgs, substituteArgs } from "../src/core/prompt-templates.js";
|
||||
import {
|
||||
expandPromptTemplate,
|
||||
loadPromptTemplates,
|
||||
parseCommandArgs,
|
||||
substituteArgs,
|
||||
} from "../src/core/prompt-templates.js";
|
||||
|
||||
// ============================================================================
|
||||
// substituteArgs
|
||||
@@ -335,10 +340,25 @@ describe("parseCommandArgs", () => {
|
||||
expect(parseCommandArgs("日本語 🎉 café")).toEqual(["日本語", "🎉", "café"]);
|
||||
});
|
||||
|
||||
test("should handle newlines in arguments", () => {
|
||||
test("should handle newlines in quoted arguments", () => {
|
||||
expect(parseCommandArgs('"line1\nline2" second')).toEqual(["line1\nline2", "second"]);
|
||||
});
|
||||
|
||||
test("should treat unquoted newlines as separators", () => {
|
||||
expect(parseCommandArgs("label-2\n\nHere is some description #2.")).toEqual([
|
||||
"label-2",
|
||||
"Here",
|
||||
"is",
|
||||
"some",
|
||||
"description",
|
||||
"#2.",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should collapse mixed unquoted whitespace", () => {
|
||||
expect(parseCommandArgs("a\n\n\tb c")).toEqual(["a", "b", "c"]);
|
||||
});
|
||||
|
||||
test("should handle escaped quotes inside quoted strings", () => {
|
||||
// Note: This implementation doesn't handle escaped quotes - backslash is literal
|
||||
expect(parseCommandArgs('"quoted \\"text\\""')).toEqual(["quoted \\text\\"]);
|
||||
@@ -357,6 +377,40 @@ describe("parseCommandArgs", () => {
|
||||
// Integration
|
||||
// ============================================================================
|
||||
|
||||
describe("expandPromptTemplate", () => {
|
||||
test("should split template arguments on unquoted newlines", () => {
|
||||
const result = expandPromptTemplate("/arg-test label-2\n\nHere is some description #2.", [
|
||||
{
|
||||
name: "arg-test",
|
||||
description: "test",
|
||||
content: `- arg1: $1\n- rest: \${@:2}`,
|
||||
sourceInfo: { path: "/tmp/arg-test.md", source: "local", scope: "temporary", origin: "top-level" },
|
||||
filePath: "/tmp/arg-test.md",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(result).toBe("- arg1: label-2\n- rest: Here is some description #2.");
|
||||
});
|
||||
|
||||
test("should support template command separated from args by newline", () => {
|
||||
const result = expandPromptTemplate("/arg-test\nlabel-2", [
|
||||
{
|
||||
name: "arg-test",
|
||||
description: "test",
|
||||
content: "arg1: $1",
|
||||
sourceInfo: { path: "/tmp/arg-test.md", source: "local", scope: "temporary", origin: "top-level" },
|
||||
filePath: "/tmp/arg-test.md",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(result).toBe("arg1: label-2");
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Integration
|
||||
// ============================================================================
|
||||
|
||||
describe("parseCommandArgs + substituteArgs integration", () => {
|
||||
test("should parse and substitute together correctly", () => {
|
||||
const input = 'Button "onClick handler" "disabled support"';
|
||||
|
||||
Reference in New Issue
Block a user