mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Merge branch 'main' of github.com:getpaseo/paseo into dev
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
||||
import { createDaemonTestContext, type DaemonTestContext } from "../../test-utils/index.js";
|
||||
import { getFullAccessConfig } from "../../daemon-e2e/agent-configs.js";
|
||||
|
||||
describe("opencode agent commands E2E", () => {
|
||||
let ctx: DaemonTestContext;
|
||||
|
||||
beforeEach(async () => {
|
||||
ctx = await createDaemonTestContext();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await ctx.cleanup();
|
||||
}, 60000);
|
||||
|
||||
test("lists available slash commands for an opencode agent", async () => {
|
||||
const agent = await ctx.client.createAgent({
|
||||
...getFullAccessConfig("opencode"),
|
||||
cwd: "/tmp",
|
||||
title: "OpenCode Commands Test Agent",
|
||||
});
|
||||
|
||||
expect(agent.id).toBeTruthy();
|
||||
expect(agent.provider).toBe("opencode");
|
||||
expect(agent.status).toBe("idle");
|
||||
|
||||
const result = await ctx.client.listCommands(agent.id);
|
||||
|
||||
expect(result.error).toBeNull();
|
||||
expect(result.commands.length).toBeGreaterThan(0);
|
||||
|
||||
for (const cmd of result.commands) {
|
||||
expect(cmd.name).toBeTruthy();
|
||||
expect(typeof cmd.description).toBe("string");
|
||||
expect(typeof cmd.argumentHint).toBe("string");
|
||||
expect(cmd.name.startsWith("/")).toBe(false);
|
||||
}
|
||||
}, 60_000);
|
||||
|
||||
test("returns error for non-existent agent", async () => {
|
||||
const result = await ctx.client.listCommands("non-existent-agent-id");
|
||||
|
||||
expect(result.error).toBeTruthy();
|
||||
expect(result.error).toContain("Agent not found");
|
||||
expect(result.commands).toEqual([]);
|
||||
}, 60_000);
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import pino from "pino";
|
||||
|
||||
import { isCommandAvailable } from "../provider-launch-config.js";
|
||||
import type { AgentSlashCommand } from "../agent-sdk-types.js";
|
||||
import { OpenCodeAgentClient } from "./opencode-agent.js";
|
||||
|
||||
describe("opencode agent commands contract (real)", () => {
|
||||
test("lists slash commands with the expected contract", async () => {
|
||||
expect(isCommandAvailable("opencode")).toBe(true);
|
||||
|
||||
const client = new OpenCodeAgentClient(pino({ level: "silent" }));
|
||||
const session = await client.createSession({
|
||||
provider: "opencode",
|
||||
cwd: process.cwd(),
|
||||
modeId: "plan",
|
||||
});
|
||||
|
||||
try {
|
||||
expect(typeof session.listCommands).toBe("function");
|
||||
const commands = await session.listCommands!();
|
||||
|
||||
expect(Array.isArray(commands)).toBe(true);
|
||||
expect(commands.length).toBeGreaterThan(0);
|
||||
|
||||
for (const command of commands) {
|
||||
const typed = command as AgentSlashCommand;
|
||||
expect(typeof typed.name).toBe("string");
|
||||
expect(typed.name.length).toBeGreaterThan(0);
|
||||
expect(typed.name.startsWith("/")).toBe(false);
|
||||
expect(typeof typed.description).toBe("string");
|
||||
expect(typeof typed.argumentHint).toBe("string");
|
||||
}
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
}, 60_000);
|
||||
});
|
||||
@@ -21,6 +21,7 @@ import type {
|
||||
AgentRuntimeInfo,
|
||||
AgentSession,
|
||||
AgentSessionConfig,
|
||||
AgentSlashCommand,
|
||||
AgentStreamEvent,
|
||||
AgentTimelineItem,
|
||||
AgentUsage,
|
||||
@@ -445,7 +446,7 @@ export class OpenCodeAgentClient implements AgentClient {
|
||||
throw new Error("OpenCode session creation returned no data");
|
||||
}
|
||||
|
||||
return new OpenCodeAgentSession(openCodeConfig, client, session.id);
|
||||
return new OpenCodeAgentSession(openCodeConfig, client, session.id, this.logger);
|
||||
}
|
||||
|
||||
async resumeSession(
|
||||
@@ -470,7 +471,7 @@ export class OpenCodeAgentClient implements AgentClient {
|
||||
directory: openCodeConfig.cwd,
|
||||
});
|
||||
|
||||
return new OpenCodeAgentSession(openCodeConfig, client, handle.sessionId);
|
||||
return new OpenCodeAgentSession(openCodeConfig, client, handle.sessionId, this.logger);
|
||||
}
|
||||
|
||||
async listModels(options?: ListModelsOptions): Promise<AgentModelDefinition[]> {
|
||||
@@ -975,6 +976,7 @@ class OpenCodeAgentSession implements AgentSession {
|
||||
private readonly config: OpenCodeAgentConfig;
|
||||
private readonly client: OpencodeClient;
|
||||
private readonly sessionId: string;
|
||||
private readonly logger: Logger;
|
||||
private currentMode: string = "default";
|
||||
private pendingPermissions = new Map<string, AgentPermissionRequest>();
|
||||
private abortController: AbortController | null = null;
|
||||
@@ -994,10 +996,16 @@ class OpenCodeAgentSession implements AgentSession {
|
||||
private nextTurnOrdinal = 0;
|
||||
private activeForegroundTurnId: string | null = null;
|
||||
|
||||
constructor(config: OpenCodeAgentConfig, client: OpencodeClient, sessionId: string) {
|
||||
constructor(
|
||||
config: OpenCodeAgentConfig,
|
||||
client: OpencodeClient,
|
||||
sessionId: string,
|
||||
logger: Logger,
|
||||
) {
|
||||
this.config = config;
|
||||
this.client = client;
|
||||
this.sessionId = sessionId;
|
||||
this.logger = logger;
|
||||
this.currentMode = normalizeOpenCodeModeId(config.modeId);
|
||||
}
|
||||
|
||||
@@ -1129,23 +1137,37 @@ class OpenCodeAgentSession implements AgentSession {
|
||||
thinkingOptionId && thinkingOptionId !== "default" ? thinkingOptionId : undefined;
|
||||
const effectiveMode = normalizeOpenCodeModeId(this.currentMode);
|
||||
|
||||
const promptResponse = await this.client.session.promptAsync({
|
||||
sessionID: this.sessionId,
|
||||
directory: this.config.cwd,
|
||||
parts,
|
||||
...(options?.outputSchema
|
||||
? {
|
||||
format: {
|
||||
type: "json_schema" as const,
|
||||
schema: options.outputSchema as Record<string, unknown>,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(this.config.systemPrompt ? { system: this.config.systemPrompt } : {}),
|
||||
...(model ? { model } : {}),
|
||||
...(effectiveMode ? { agent: effectiveMode } : {}),
|
||||
...(effectiveVariant ? { variant: effectiveVariant } : {}),
|
||||
});
|
||||
let promptResponse;
|
||||
const slashCommand = await this.resolveSlashCommandInvocation(prompt);
|
||||
if (slashCommand) {
|
||||
promptResponse = await this.client.session.command({
|
||||
sessionID: this.sessionId,
|
||||
directory: this.config.cwd,
|
||||
command: slashCommand.commandName,
|
||||
arguments: slashCommand.args,
|
||||
...(this.config.model ? { model: this.config.model } : {}),
|
||||
...(effectiveMode ? { agent: effectiveMode } : {}),
|
||||
...(effectiveVariant ? { variant: effectiveVariant } : {}),
|
||||
});
|
||||
} else {
|
||||
promptResponse = await this.client.session.promptAsync({
|
||||
sessionID: this.sessionId,
|
||||
directory: this.config.cwd,
|
||||
parts,
|
||||
...(options?.outputSchema
|
||||
? {
|
||||
format: {
|
||||
type: "json_schema" as const,
|
||||
schema: options.outputSchema as Record<string, unknown>,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(this.config.systemPrompt ? { system: this.config.systemPrompt } : {}),
|
||||
...(model ? { model } : {}),
|
||||
...(effectiveMode ? { agent: effectiveMode } : {}),
|
||||
...(effectiveVariant ? { variant: effectiveVariant } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
if (promptResponse.error) {
|
||||
const errorMsg = JSON.stringify(promptResponse.error);
|
||||
@@ -1335,6 +1357,20 @@ class OpenCodeAgentSession implements AgentSession {
|
||||
return this.currentMode;
|
||||
}
|
||||
|
||||
async listCommands(): Promise<AgentSlashCommand[]> {
|
||||
const result = await this.client.command.list({
|
||||
directory: this.config.cwd,
|
||||
});
|
||||
if (result.error || !result.data) {
|
||||
return [];
|
||||
}
|
||||
return result.data.map((cmd) => ({
|
||||
name: cmd.name,
|
||||
description: cmd.description ?? "",
|
||||
argumentHint: cmd.hints?.length ? cmd.hints.join(" ") : "",
|
||||
}));
|
||||
}
|
||||
|
||||
async setMode(modeId: string): Promise<void> {
|
||||
this.currentMode = normalizeOpenCodeModeId(modeId);
|
||||
}
|
||||
@@ -1418,6 +1454,45 @@ class OpenCodeAgentSession implements AgentSession {
|
||||
.map((p) => ({ type: "text", text: p.text }));
|
||||
}
|
||||
|
||||
private parseSlashCommandInput(text: string): { commandName: string; args?: string } | null {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed.startsWith("/") || trimmed.length <= 1) {
|
||||
return null;
|
||||
}
|
||||
const withoutPrefix = trimmed.slice(1);
|
||||
const firstWhitespaceIdx = withoutPrefix.search(/\s/);
|
||||
const commandName =
|
||||
firstWhitespaceIdx === -1 ? withoutPrefix : withoutPrefix.slice(0, firstWhitespaceIdx);
|
||||
if (!commandName || commandName.includes("/")) {
|
||||
return null;
|
||||
}
|
||||
const rawArgs =
|
||||
firstWhitespaceIdx === -1 ? "" : withoutPrefix.slice(firstWhitespaceIdx + 1).trim();
|
||||
return rawArgs.length > 0 ? { commandName, args: rawArgs } : { commandName };
|
||||
}
|
||||
|
||||
private async resolveSlashCommandInvocation(
|
||||
prompt: AgentPromptInput,
|
||||
): Promise<{ commandName: string; args?: string } | null> {
|
||||
if (typeof prompt !== "string") {
|
||||
return null;
|
||||
}
|
||||
const parsed = this.parseSlashCommandInput(prompt);
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const commands = await this.listCommands();
|
||||
return commands.some((command) => command.name === parsed.commandName) ? parsed : null;
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
{ err: error, commandName: parsed.commandName },
|
||||
"Failed to resolve slash command; falling back to plain prompt input",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private parseModel(model?: string): { providerID: string; modelID: string } | undefined {
|
||||
if (!model) {
|
||||
return undefined;
|
||||
|
||||
Reference in New Issue
Block a user