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
16 changes: 10 additions & 6 deletions extensions/cli/src/commands/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { gracefulExit } from "../util/exit.js";
import { formatAnthropicError, formatError } from "../util/formatError.js";
import { logger } from "../util/logger.js";
import { question } from "../util/prompt.js";
import { prependPrompt } from "../util/promptProcessor.js";
import {
calculateContextUsagePercentage,
countChatHistoryTokens,
Expand Down Expand Up @@ -481,9 +482,11 @@ async function runHeadlessMode(
const agentFileState = await serviceContainer.get<AgentFileServiceState>(
SERVICE_NAMES.AGENT_FILE,
);
const initialPrompt =
`${agentFileState?.agentFile?.prompt ?? ""}\n\n${prompt ?? ""}`.trim() ||
undefined;

const initialPrompt = prependPrompt(
agentFileState?.agentFile?.prompt,
prompt,
);
const initialUserInput = await processAndCombinePrompts(
options.prompt,
initialPrompt,
Expand Down Expand Up @@ -556,9 +559,10 @@ export async function chat(prompt?: string, options: ChatOptions = {}) {
SERVICE_NAMES.AGENT_FILE,
);

const initialPrompt =
`${agentFileState?.agentFile?.prompt ?? ""}\n\n${prompt ?? ""}`.trim() ||
undefined;
const initialPrompt = prependPrompt(
agentFileState?.agentFile?.prompt,
prompt,
);

// Start TUI with skipOnboarding since we already handled it
const tuiOptions: any = {
Expand Down
22 changes: 15 additions & 7 deletions extensions/cli/src/commands/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import express, { Request, Response } from "express";

import { ToolPermissionServiceState } from "src/services/ToolPermissionService.js";
import { posthogService } from "src/telemetry/posthogService.js";
import { prependPrompt } from "src/util/promptProcessor.js";

import { getAccessToken, getAssistantSlug } from "../auth/workos.js";
import { runEnvironmentInstallSafe } from "../environment/environmentHandler.js";
Expand All @@ -16,6 +17,7 @@ import {
services,
} from "../services/index.js";
import {
AgentFileServiceState,
AuthServiceState,
ConfigServiceState,
ModelServiceState,
Expand Down Expand Up @@ -73,11 +75,13 @@ export async function serve(prompt?: string, options: ServeOptions = {}) {
});

// Get initialized services from the service container
const [configState, modelState, permissionsState] = await Promise.all([
getService<ConfigServiceState>(SERVICE_NAMES.CONFIG),
getService<ModelServiceState>(SERVICE_NAMES.MODEL),
getService<ToolPermissionServiceState>(SERVICE_NAMES.TOOL_PERMISSIONS),
]);
const [configState, modelState, permissionsState, agentFileState] =
await Promise.all([
getService<ConfigServiceState>(SERVICE_NAMES.CONFIG),
getService<ModelServiceState>(SERVICE_NAMES.MODEL),
getService<ToolPermissionServiceState>(SERVICE_NAMES.TOOL_PERMISSIONS),
getService<AgentFileServiceState>(SERVICE_NAMES.AGENT_FILE),
]);

if (!configState.config || !modelState.llmApi || !modelState.model) {
throw new Error("Failed to initialize required services");
Expand Down Expand Up @@ -389,9 +393,13 @@ export async function serve(prompt?: string, options: ServeOptions = {}) {
runEnvironmentInstallSafe();

// If initial prompt provided, queue it for processing
if (actualPrompt) {
const initialPrompt = prependPrompt(
agentFileState?.agentFile?.prompt,
actualPrompt,
);
if (initialPrompt) {
console.log(chalk.dim("\nProcessing initial prompt..."));
await messageQueue.enqueueMessage(actualPrompt);
await messageQueue.enqueueMessage(initialPrompt);
processMessages(state, llmApi);
}
});
Expand Down
178 changes: 177 additions & 1 deletion extensions/cli/src/util/promptProcessor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";

import { processRule } from "../hubLoader.js";

import { processAndCombinePrompts } from "./promptProcessor.js";
import { prependPrompt, processAndCombinePrompts } from "./promptProcessor.js";

// Mock the args module
vi.mock("../hubLoader.js", () => ({
Expand Down Expand Up @@ -125,3 +125,179 @@ describe("promptProcessor", () => {
});
});
});

describe("prependPrompt", () => {
describe("normal cases", () => {
it("should combine two non-empty strings with double newlines", () => {
const result = prependPrompt("Hello", "World");
expect(result).toBe("Hello\n\nWorld");
});

it("should combine multi-line strings correctly", () => {
const prepend = "Line 1\nLine 2";
const original = "Line 3\nLine 4";
const result = prependPrompt(prepend, original);
expect(result).toBe("Line 1\nLine 2\n\nLine 3\nLine 4");
});

it("should handle strings with special characters", () => {
const prepend = "Special: @#$%^&*()";
const original = "Unicode: 🚀 ∑ ∆";
const result = prependPrompt(prepend, original);
expect(result).toBe("Special: @#$%^&*()\n\nUnicode: 🚀 ∑ ∆");
});
});

describe("undefined parameter cases", () => {
it("should handle undefined prepend with defined original", () => {
const result = prependPrompt(undefined, "original");
expect(result).toBe("original");
});

it("should handle defined prepend with undefined original", () => {
const result = prependPrompt("prepend", undefined);
expect(result).toBe("prepend");
});

it("should return undefined when both parameters are undefined", () => {
const result = prependPrompt(undefined, undefined);
expect(result).toBeUndefined();
});
});

describe("empty string cases", () => {
it("should handle empty prepend with non-empty original", () => {
const result = prependPrompt("", "original");
expect(result).toBe("original");
});

it("should handle non-empty prepend with empty original", () => {
const result = prependPrompt("prepend", "");
expect(result).toBe("prepend");
});

it("should return undefined when both parameters are empty strings", () => {
const result = prependPrompt("", "");
expect(result).toBeUndefined();
});

it("should handle mixed undefined and empty string", () => {
const result1 = prependPrompt(undefined, "");
const result2 = prependPrompt("", undefined);
expect(result1).toBeUndefined();
expect(result2).toBeUndefined();
});
});

describe("whitespace cases", () => {
it("should trim leading and trailing whitespace", () => {
const result = prependPrompt(" hello ", " world ");
expect(result).toBe("hello\n\nworld");
});

it("should return undefined when result is only whitespace", () => {
const result = prependPrompt(" ", " ");
expect(result).toBeUndefined();
});

it("should handle newlines in parameters", () => {
const prepend = "\nhello\n";
const original = "\nworld\n";
const result = prependPrompt(prepend, original);
expect(result).toBe("hello\n\nworld");
});

it("should handle mixed whitespace characters", () => {
const prepend = "\t hello \t";
const original = "\r\n world \r\n";
const result = prependPrompt(prepend, original);
expect(result).toBe("hello\n\nworld");
});

it("should preserve internal whitespace and newlines", () => {
const prepend = "hello\n world";
const original = "foo\t\tbar";
const result = prependPrompt(prepend, original);
expect(result).toBe("hello\n world\n\nfoo\t\tbar");
});
});

describe("edge cases", () => {
it("should handle very long strings", () => {
const longString = "a".repeat(10000);
const result = prependPrompt(longString, "short");
expect(result).toBe(`${longString}\n\nshort`);
expect(result?.length).toBe(10000 + 2 + 5); // long + \n\n + short
});

it("should handle strings with only newlines", () => {
const result = prependPrompt("\n\n\n", "\n\n");
expect(result).toBeUndefined();
});

it("should handle one parameter with content and other with only whitespace", () => {
const result1 = prependPrompt("content", " \n ");
const result2 = prependPrompt(" \t ", "content");
expect(result1).toBe("content");
expect(result2).toBe("content");
});
});

describe("boundary conditions", () => {
it("should return undefined for whitespace-only result after trim", () => {
const cases = [
["", ""],
[" ", ""],
["", " "],
[" ", " "],
["\n", "\n"],
["\t", "\r"],
[undefined, ""],
["", undefined],
[undefined, undefined],
[" \n ", " \n "],
] as const;

cases.forEach(([prepend, original]) => {
const result = prependPrompt(prepend, original);
expect(result).toBeUndefined();
});
});

it("should return trimmed string for non-empty result", () => {
const cases = [
["a", "", "a"],
["", "b", "b"],
["a", "b", "a\n\nb"],
[" a ", "", "a"],
["", " b ", "b"],
[" a ", " b ", "a\n\nb"],
[undefined, "content", "content"],
["content", undefined, "content"],
] as const;

cases.forEach(([prepend, original, expected]) => {
const result = prependPrompt(prepend, original);
expect(result).toBe(expected);
});
});
});

describe("return type consistency", () => {
it("should always return string or undefined", () => {
const testCases = [
["hello", "world"],
[undefined, "world"],
["hello", undefined],
[undefined, undefined],
["", ""],
[" ", " "],
] as const;

testCases.forEach(([prepend, original]) => {
const result = prependPrompt(prepend, original);
expect(typeof result === "string" || result === undefined).toBe(true);
});
});
});
});
11 changes: 11 additions & 0 deletions extensions/cli/src/util/promptProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,14 @@ export async function processAndCombinePrompts(
? `${combinedPrompts}\n\n${initialPrompt}`
: combinedPrompts;
}

// Merges two prompts with new lines between them, handling undefined
export function prependPrompt(
prepend: string | undefined,
original: string | undefined,
) {
return (
`${(prepend ?? "").trim()}\n\n${(original ?? "").trim()}`.trim() ||
undefined
);
}
Loading