fix(coding-agent): merge piped stdin into initial prompt closes #2315
This commit is contained in:
@@ -451,6 +451,12 @@ pi config # Enable/disable package resources
|
|||||||
| `--mode rpc` | RPC mode for process integration (see [docs/rpc.md](docs/rpc.md)) |
|
| `--mode rpc` | RPC mode for process integration (see [docs/rpc.md](docs/rpc.md)) |
|
||||||
| `--export <in> [out]` | Export session to HTML |
|
| `--export <in> [out]` | Export session to HTML |
|
||||||
|
|
||||||
|
In print mode, pi also reads piped stdin and merges it into the initial prompt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat README.md | pi -p "Summarize this text"
|
||||||
|
```
|
||||||
|
|
||||||
### Model Options
|
### Model Options
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
@@ -525,6 +531,9 @@ pi "List all .ts files in src/"
|
|||||||
# Non-interactive
|
# Non-interactive
|
||||||
pi -p "Summarize this codebase"
|
pi -p "Summarize this codebase"
|
||||||
|
|
||||||
|
# Non-interactive with piped stdin
|
||||||
|
cat README.md | pi -p "Summarize this text"
|
||||||
|
|
||||||
# Different model
|
# Different model
|
||||||
pi --provider openai --model gpt-4o "Help me refactor"
|
pi --provider openai --model gpt-4o "Help me refactor"
|
||||||
|
|
||||||
|
|||||||
43
packages/coding-agent/src/cli/initial-message.ts
Normal file
43
packages/coding-agent/src/cli/initial-message.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import type { ImageContent } from "@mariozechner/pi-ai";
|
||||||
|
import type { Args } from "./args.js";
|
||||||
|
|
||||||
|
export interface InitialMessageInput {
|
||||||
|
parsed: Args;
|
||||||
|
fileText?: string;
|
||||||
|
fileImages?: ImageContent[];
|
||||||
|
stdinContent?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InitialMessageResult {
|
||||||
|
initialMessage?: string;
|
||||||
|
initialImages?: ImageContent[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combine stdin content, @file text, and the first CLI message into a single
|
||||||
|
* initial prompt for non-interactive mode.
|
||||||
|
*/
|
||||||
|
export function buildInitialMessage({
|
||||||
|
parsed,
|
||||||
|
fileText,
|
||||||
|
fileImages,
|
||||||
|
stdinContent,
|
||||||
|
}: InitialMessageInput): InitialMessageResult {
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (stdinContent !== undefined) {
|
||||||
|
parts.push(stdinContent);
|
||||||
|
}
|
||||||
|
if (fileText) {
|
||||||
|
parts.push(fileText);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.messages.length > 0) {
|
||||||
|
parts.push(parsed.messages[0]);
|
||||||
|
parsed.messages.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
initialMessage: parts.length > 0 ? parts.join("") : undefined,
|
||||||
|
initialImages: fileImages && fileImages.length > 0 ? fileImages : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import { createInterface } from "readline";
|
|||||||
import { type Args, parseArgs, printHelp } from "./cli/args.js";
|
import { type Args, parseArgs, printHelp } from "./cli/args.js";
|
||||||
import { selectConfig } from "./cli/config-selector.js";
|
import { selectConfig } from "./cli/config-selector.js";
|
||||||
import { processFileArguments } from "./cli/file-processor.js";
|
import { processFileArguments } from "./cli/file-processor.js";
|
||||||
|
import { buildInitialMessage } from "./cli/initial-message.js";
|
||||||
import { listModels } from "./cli/list-models.js";
|
import { listModels } from "./cli/list-models.js";
|
||||||
import { selectSession } from "./cli/session-picker.js";
|
import { selectSession } from "./cli/session-picker.js";
|
||||||
import { APP_NAME, getAgentDir, getModelsPath, VERSION } from "./config.js";
|
import { APP_NAME, getAgentDir, getModelsPath, VERSION } from "./config.js";
|
||||||
@@ -311,28 +312,22 @@ async function handlePackageCommand(args: string[]): Promise<boolean> {
|
|||||||
async function prepareInitialMessage(
|
async function prepareInitialMessage(
|
||||||
parsed: Args,
|
parsed: Args,
|
||||||
autoResizeImages: boolean,
|
autoResizeImages: boolean,
|
||||||
|
stdinContent?: string,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
initialMessage?: string;
|
initialMessage?: string;
|
||||||
initialImages?: ImageContent[];
|
initialImages?: ImageContent[];
|
||||||
}> {
|
}> {
|
||||||
if (parsed.fileArgs.length === 0) {
|
if (parsed.fileArgs.length === 0) {
|
||||||
return {};
|
return buildInitialMessage({ parsed, stdinContent });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { text, images } = await processFileArguments(parsed.fileArgs, { autoResizeImages });
|
const { text, images } = await processFileArguments(parsed.fileArgs, { autoResizeImages });
|
||||||
|
return buildInitialMessage({
|
||||||
let initialMessage: string;
|
parsed,
|
||||||
if (parsed.messages.length > 0) {
|
fileText: text,
|
||||||
initialMessage = text + parsed.messages[0];
|
fileImages: images,
|
||||||
parsed.messages.shift();
|
stdinContent,
|
||||||
} else {
|
});
|
||||||
initialMessage = text;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
initialMessage,
|
|
||||||
initialImages: images.length > 0 ? images : undefined,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Result from resolving a session argument */
|
/** Result from resolving a session argument */
|
||||||
@@ -675,13 +670,12 @@ export async function main(args: string[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read piped stdin content (if any) - skip for RPC mode which uses stdin for JSON-RPC
|
// Read piped stdin content (if any) - skip for RPC mode which uses stdin for JSON-RPC
|
||||||
|
let stdinContent: string | undefined;
|
||||||
if (parsed.mode !== "rpc") {
|
if (parsed.mode !== "rpc") {
|
||||||
const stdinContent = await readPipedStdin();
|
stdinContent = await readPipedStdin();
|
||||||
if (stdinContent !== undefined) {
|
if (stdinContent !== undefined) {
|
||||||
// Force print mode since interactive mode requires a TTY for keyboard input
|
// Force print mode since interactive mode requires a TTY for keyboard input
|
||||||
parsed.print = true;
|
parsed.print = true;
|
||||||
// Prepend stdin content to messages
|
|
||||||
parsed.messages.unshift(stdinContent);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -704,7 +698,11 @@ export async function main(args: string[]) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { initialMessage, initialImages } = await prepareInitialMessage(parsed, settingsManager.getImageAutoResize());
|
const { initialMessage, initialImages } = await prepareInitialMessage(
|
||||||
|
parsed,
|
||||||
|
settingsManager.getImageAutoResize(),
|
||||||
|
stdinContent,
|
||||||
|
);
|
||||||
const isInteractive = !parsed.print && parsed.mode === undefined;
|
const isInteractive = !parsed.print && parsed.mode === undefined;
|
||||||
const mode = parsed.mode || "text";
|
const mode = parsed.mode || "text";
|
||||||
initTheme(settingsManager.getTheme(), isInteractive);
|
initTheme(settingsManager.getTheme(), isInteractive);
|
||||||
|
|||||||
47
packages/coding-agent/test/initial-message.test.ts
Normal file
47
packages/coding-agent/test/initial-message.test.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
import type { Args } from "../src/cli/args.js";
|
||||||
|
import { buildInitialMessage } from "../src/cli/initial-message.js";
|
||||||
|
|
||||||
|
function createArgs(messages: string[] = []): Args {
|
||||||
|
return {
|
||||||
|
messages: [...messages],
|
||||||
|
fileArgs: [],
|
||||||
|
unknownFlags: new Map(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("buildInitialMessage", () => {
|
||||||
|
test("merges piped stdin with the first CLI message into one prompt", () => {
|
||||||
|
const parsed = createArgs(["Summarize the text given"]);
|
||||||
|
const result = buildInitialMessage({
|
||||||
|
parsed,
|
||||||
|
stdinContent: "README contents\n",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.initialMessage).toBe("README contents\nSummarize the text given");
|
||||||
|
expect(parsed.messages).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses stdin as the initial prompt when no CLI message is present", () => {
|
||||||
|
const parsed = createArgs();
|
||||||
|
const result = buildInitialMessage({
|
||||||
|
parsed,
|
||||||
|
stdinContent: "README contents",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.initialMessage).toBe("README contents");
|
||||||
|
expect(parsed.messages).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("combines stdin, file text, and first CLI message in one prompt", () => {
|
||||||
|
const parsed = createArgs(["Explain it", "Second message"]);
|
||||||
|
const result = buildInitialMessage({
|
||||||
|
parsed,
|
||||||
|
stdinContent: "stdin\n",
|
||||||
|
fileText: "file\n",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.initialMessage).toBe("stdin\nfile\nExplain it");
|
||||||
|
expect(parsed.messages).toEqual(["Second message"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user