Add Codex plan mode draft features
This commit is contained in:
@@ -37,6 +37,7 @@ import type {
|
||||
OpenProjectResponseMessage,
|
||||
ArchiveWorkspaceResponseMessage,
|
||||
ListCommandsResponse,
|
||||
ListProviderFeaturesResponseMessage,
|
||||
ListProviderModelsResponseMessage,
|
||||
ListProviderModesResponseMessage,
|
||||
ListAvailableProvidersResponse,
|
||||
@@ -216,13 +217,14 @@ type CreatePaseoWorktreePayload = Extract<
|
||||
>["payload"];
|
||||
type FileExplorerPayload = FileExplorerResponse["payload"];
|
||||
type FileDownloadTokenPayload = FileDownloadTokenResponse["payload"];
|
||||
type ListProviderFeaturesPayload = ListProviderFeaturesResponseMessage["payload"];
|
||||
type ListProviderModelsPayload = ListProviderModelsResponseMessage["payload"];
|
||||
type ListProviderModesPayload = ListProviderModesResponseMessage["payload"];
|
||||
type ListAvailableProvidersPayload = ListAvailableProvidersResponse["payload"];
|
||||
type ListCommandsPayload = ListCommandsResponse["payload"];
|
||||
type ListCommandsDraftConfig = Pick<
|
||||
AgentSessionConfig,
|
||||
"provider" | "cwd" | "modeId" | "model" | "thinkingOptionId"
|
||||
"provider" | "cwd" | "modeId" | "model" | "thinkingOptionId" | "featureValues"
|
||||
>;
|
||||
type ListCommandsOptions = {
|
||||
requestId?: string;
|
||||
@@ -2521,6 +2523,21 @@ export class DaemonClient {
|
||||
});
|
||||
}
|
||||
|
||||
async listProviderFeatures(
|
||||
draftConfig: ListCommandsDraftConfig,
|
||||
options?: { requestId?: string },
|
||||
): Promise<ListProviderFeaturesPayload> {
|
||||
return this.sendCorrelatedSessionRequest({
|
||||
requestId: options?.requestId,
|
||||
message: {
|
||||
type: "list_provider_features_request",
|
||||
draftConfig,
|
||||
},
|
||||
responseType: "list_provider_features_response",
|
||||
timeout: 45000,
|
||||
});
|
||||
}
|
||||
|
||||
async listAvailableProviders(options?: {
|
||||
requestId?: string;
|
||||
}): Promise<ListAvailableProvidersPayload> {
|
||||
|
||||
@@ -550,6 +550,31 @@ export class AgentManager {
|
||||
}
|
||||
}
|
||||
|
||||
async listDraftFeatures(config: AgentSessionConfig): Promise<AgentFeature[]> {
|
||||
const normalizedConfig = await this.normalizeConfig(config);
|
||||
const client = this.requireClient(normalizedConfig.provider);
|
||||
const available = await client.isAvailable();
|
||||
if (!available) {
|
||||
throw new Error(
|
||||
`Provider '${normalizedConfig.provider}' is not available. Please ensure the CLI is installed.`,
|
||||
);
|
||||
}
|
||||
|
||||
const session = await client.createSession(normalizedConfig);
|
||||
try {
|
||||
return session.features ?? [];
|
||||
} finally {
|
||||
try {
|
||||
await session.close();
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
{ err: error, provider: normalizedConfig.provider },
|
||||
"Failed to close draft feature listing session",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getAgent(id: string): ManagedAgent | null {
|
||||
const agent = this.agents.get(id);
|
||||
return agent ? { ...agent } : null;
|
||||
|
||||
@@ -69,6 +69,7 @@ export type AgentFeatureToggle = {
|
||||
id: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
tooltip?: string;
|
||||
icon?: string;
|
||||
value: boolean;
|
||||
};
|
||||
@@ -78,6 +79,7 @@ export type AgentFeatureSelect = {
|
||||
id: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
tooltip?: string;
|
||||
icon?: string;
|
||||
value: string | null;
|
||||
options: AgentSelectOption[];
|
||||
@@ -222,6 +224,10 @@ export type ToolCallDetail =
|
||||
text?: string;
|
||||
icon?: ToolCallIconName;
|
||||
}
|
||||
| {
|
||||
type: "plan";
|
||||
text: string;
|
||||
}
|
||||
| {
|
||||
type: "unknown";
|
||||
input: unknown | null;
|
||||
|
||||
@@ -59,18 +59,10 @@ const CLAUDE_MODES: AgentProviderModeDefinition[] = [
|
||||
];
|
||||
|
||||
const CODEX_MODES: AgentProviderModeDefinition[] = [
|
||||
{
|
||||
id: "read-only",
|
||||
label: "Read Only",
|
||||
description:
|
||||
"Read files and answer questions. Manual approval required for edits, commands, or network ops.",
|
||||
icon: "ShieldCheck",
|
||||
colorTier: "safe",
|
||||
},
|
||||
{
|
||||
id: "auto",
|
||||
label: "Auto",
|
||||
description: "Edit files and run commands but still request approval before escalating scope.",
|
||||
label: "Default Permissions",
|
||||
description: "Edit files and run commands with Codex's default approval flow.",
|
||||
icon: "ShieldAlert",
|
||||
colorTier: "moderate",
|
||||
},
|
||||
@@ -145,7 +137,7 @@ export const AGENT_PROVIDER_DEFINITIONS: AgentProviderDefinition[] = [
|
||||
modes: CODEX_MODES,
|
||||
voice: {
|
||||
enabled: true,
|
||||
defaultModeId: "read-only",
|
||||
defaultModeId: "auto",
|
||||
defaultModel: "gpt-5.1-codex-mini",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -35,6 +35,7 @@ describe("Codex app-server provider (e2e)", () => {
|
||||
model: CODEX_TEST_MODEL,
|
||||
thinkingOptionId: CODEX_TEST_THINKING_OPTION_ID,
|
||||
});
|
||||
expect(session.features?.some((feature) => feature.id === "plan_mode")).toBe(true);
|
||||
|
||||
const result = await session.run("Say hello in one sentence.");
|
||||
expect(result.finalText.length).toBeGreaterThan(0);
|
||||
|
||||
@@ -5,6 +5,18 @@ import { __codexAppServerInternals } from "./codex-app-server-agent.js";
|
||||
import { createTestLogger } from "../../../test-utils/test-logger.js";
|
||||
|
||||
const CODEX_PROVIDER = "codex";
|
||||
const TEST_COLLABORATION_MODES = [
|
||||
{
|
||||
name: "Code",
|
||||
mode: "code",
|
||||
developer_instructions: "Built-in code mode",
|
||||
},
|
||||
{
|
||||
name: "Plan",
|
||||
mode: "plan",
|
||||
developer_instructions: "Built-in plan mode",
|
||||
},
|
||||
];
|
||||
|
||||
function createConfig(overrides: Partial<AgentSessionConfig> = {}): AgentSessionConfig {
|
||||
return {
|
||||
@@ -28,11 +40,13 @@ function createSession(configOverrides: Partial<AgentSessionConfig> = {}) {
|
||||
) as unknown as AgentSession & { [key: string]: unknown };
|
||||
session.connected = true;
|
||||
session.currentThreadId = "test-thread";
|
||||
session.collaborationModes = TEST_COLLABORATION_MODES;
|
||||
session.refreshResolvedCollaborationMode();
|
||||
return session;
|
||||
}
|
||||
|
||||
describe("Codex app-server provider fast mode", () => {
|
||||
test("features returns fast_mode toggle when model supports it", async () => {
|
||||
describe("Codex app-server provider features", () => {
|
||||
test("features returns fast and plan toggles when supported", async () => {
|
||||
const session = createSession();
|
||||
|
||||
expect(session.features).toEqual([
|
||||
@@ -41,12 +55,23 @@ describe("Codex app-server provider fast mode", () => {
|
||||
id: "fast_mode",
|
||||
label: "Fast",
|
||||
description: "Priority inference at 2x usage",
|
||||
tooltip: "Toggle fast mode",
|
||||
icon: "zap",
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
type: "toggle",
|
||||
id: "plan_mode",
|
||||
label: "Plan",
|
||||
description: "Switch Codex into planning-only collaboration mode",
|
||||
tooltip: "Toggle plan mode",
|
||||
icon: "list-todo",
|
||||
value: false,
|
||||
},
|
||||
]);
|
||||
|
||||
await session.setFeature?.("fast_mode", true);
|
||||
await session.setFeature?.("plan_mode", true);
|
||||
|
||||
expect(session.features).toEqual([
|
||||
{
|
||||
@@ -54,16 +79,36 @@ describe("Codex app-server provider fast mode", () => {
|
||||
id: "fast_mode",
|
||||
label: "Fast",
|
||||
description: "Priority inference at 2x usage",
|
||||
tooltip: "Toggle fast mode",
|
||||
icon: "zap",
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
type: "toggle",
|
||||
id: "plan_mode",
|
||||
label: "Plan",
|
||||
description: "Switch Codex into planning-only collaboration mode",
|
||||
tooltip: "Toggle plan mode",
|
||||
icon: "list-todo",
|
||||
value: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("features returns empty array when model does not support fast mode", () => {
|
||||
test("features returns only plan toggle when model does not support fast mode", () => {
|
||||
const session = createSession({ model: "gpt-3.5-turbo" });
|
||||
|
||||
expect(session.features).toEqual([]);
|
||||
expect(session.features).toEqual([
|
||||
{
|
||||
type: "toggle",
|
||||
id: "plan_mode",
|
||||
label: "Plan",
|
||||
description: "Switch Codex into planning-only collaboration mode",
|
||||
tooltip: "Toggle plan mode",
|
||||
icon: "list-todo",
|
||||
value: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("setFeature('fast_mode', true) sets serviceTier to fast", async () => {
|
||||
@@ -103,21 +148,32 @@ describe("Codex app-server provider fast mode", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("constructor restores serviceTier from config.featureValues", () => {
|
||||
test("constructor restores feature flags from config.featureValues", () => {
|
||||
const session = createSession({
|
||||
featureValues: { fast_mode: true },
|
||||
featureValues: { fast_mode: true, plan_mode: true },
|
||||
});
|
||||
|
||||
expect((session as any).serviceTier).toBe("fast");
|
||||
expect((session as any).planModeEnabled).toBe(true);
|
||||
expect(session.features).toEqual([
|
||||
{
|
||||
type: "toggle",
|
||||
id: "fast_mode",
|
||||
label: "Fast",
|
||||
description: "Priority inference at 2x usage",
|
||||
tooltip: "Toggle fast mode",
|
||||
icon: "zap",
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
type: "toggle",
|
||||
id: "plan_mode",
|
||||
label: "Plan",
|
||||
description: "Switch Codex into planning-only collaboration mode",
|
||||
tooltip: "Toggle plan mode",
|
||||
icon: "list-todo",
|
||||
value: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -158,7 +214,17 @@ describe("Codex app-server provider fast mode", () => {
|
||||
await session.setFeature?.("fast_mode", true);
|
||||
await session.setModel("gpt-3.5-turbo");
|
||||
|
||||
expect(session.features).toEqual([]);
|
||||
expect(session.features).toEqual([
|
||||
{
|
||||
type: "toggle",
|
||||
id: "plan_mode",
|
||||
label: "Plan",
|
||||
description: "Switch Codex into planning-only collaboration mode",
|
||||
tooltip: "Toggle plan mode",
|
||||
icon: "list-todo",
|
||||
value: false,
|
||||
},
|
||||
]);
|
||||
expect((session as any).serviceTier).toBeNull();
|
||||
|
||||
await session.startTurn("hello");
|
||||
@@ -171,4 +237,29 @@ describe("Codex app-server provider fast mode", () => {
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
test("startTurn switches collaboration mode when plan mode is enabled", async () => {
|
||||
const session = createSession();
|
||||
const request = vi.fn().mockResolvedValue(undefined);
|
||||
(session as any).client = { request };
|
||||
(session as any).connected = true;
|
||||
(session as any).currentThreadId = "thread-123";
|
||||
(session as any).ensureThreadLoaded = vi.fn().mockResolvedValue(undefined);
|
||||
(session as any).ensureThread = vi.fn().mockResolvedValue(undefined);
|
||||
(session as any).buildUserInput = vi.fn().mockResolvedValue([{ type: "text", text: "hi" }]);
|
||||
(session as any).resolveSlashCommandInvocation = vi.fn().mockResolvedValue(null);
|
||||
|
||||
await session.setFeature?.("plan_mode", true);
|
||||
await session.startTurn("hello");
|
||||
|
||||
expect(request).toHaveBeenCalledWith(
|
||||
"turn/start",
|
||||
expect.objectContaining({
|
||||
collaborationMode: expect.objectContaining({
|
||||
mode: "plan",
|
||||
}),
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,6 +55,25 @@ describe("Codex app-server provider", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("maps Codex plan markdown to a synthetic plan tool call", () => {
|
||||
const item = __codexAppServerInternals.mapCodexPlanToToolCall({
|
||||
callId: "plan-turn-1",
|
||||
text: "### Login Screen\n- Build layout\n- Add validation",
|
||||
});
|
||||
|
||||
expect(item).toEqual({
|
||||
type: "tool_call",
|
||||
callId: "plan-turn-1",
|
||||
name: "plan",
|
||||
status: "completed",
|
||||
error: null,
|
||||
detail: {
|
||||
type: "plan",
|
||||
text: "### Login Screen\n- Build layout\n- Add validation",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("maps patch notifications with object-style single change payloads", () => {
|
||||
const item = __codexAppServerInternals.mapCodexPatchNotificationToToolCall({
|
||||
callId: "patch-object-single",
|
||||
|
||||
@@ -2,7 +2,6 @@ import type {
|
||||
AgentCapabilityFlags,
|
||||
AgentClient,
|
||||
AgentFeature,
|
||||
AgentFeatureToggle,
|
||||
AgentLaunchContext,
|
||||
AgentMode,
|
||||
AgentModelDefinition,
|
||||
@@ -52,6 +51,7 @@ import {
|
||||
quoteWindowsCommand,
|
||||
} from "../../../utils/executable.js";
|
||||
import { extractCodexTerminalSessionId, nonEmptyString } from "./tool-call-mapper-utils.js";
|
||||
import { buildCodexFeatures, codexModelSupportsFastMode } from "./codex-feature-definitions.js";
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 14 * 24 * 60 * 60 * 1000;
|
||||
const TURN_START_TIMEOUT_MS = 90 * 1000;
|
||||
@@ -68,16 +68,10 @@ const CODEX_APP_SERVER_CAPABILITIES: AgentCapabilityFlags = {
|
||||
};
|
||||
|
||||
const CODEX_MODES: AgentMode[] = [
|
||||
{
|
||||
id: "read-only",
|
||||
label: "Read Only",
|
||||
description:
|
||||
"Read files and answer questions. Manual approval required for edits, commands, or network ops.",
|
||||
},
|
||||
{
|
||||
id: "auto",
|
||||
label: "Auto",
|
||||
description: "Edit files and run commands but still request approval before escalating scope.",
|
||||
label: "Default Permissions",
|
||||
description: "Edit files and run commands with Codex's default approval flow.",
|
||||
},
|
||||
{
|
||||
id: "full-access",
|
||||
@@ -87,20 +81,6 @@ const CODEX_MODES: AgentMode[] = [
|
||||
];
|
||||
|
||||
const DEFAULT_CODEX_MODE_ID = "auto";
|
||||
const CODEX_FAST_MODE_SUPPORTED_MODE_PREFIXES = [
|
||||
"gpt-5",
|
||||
"gpt-4.1",
|
||||
"o3",
|
||||
"o4-mini",
|
||||
] as const;
|
||||
const CODEX_FAST_MODE_FEATURE: AgentFeatureToggle = {
|
||||
type: "toggle",
|
||||
id: "fast_mode",
|
||||
label: "Fast",
|
||||
description: "Priority inference at 2x usage",
|
||||
icon: "zap",
|
||||
value: false,
|
||||
};
|
||||
|
||||
const MODE_PRESETS: Record<
|
||||
string,
|
||||
@@ -156,16 +136,6 @@ function normalizeCodexModelLabel(displayName: string): string {
|
||||
return displayName.replace(/\bgpt\b/gi, "GPT");
|
||||
}
|
||||
|
||||
function codexModelSupportsFastMode(modelId: string | null | undefined): boolean {
|
||||
const normalizedModelId = normalizeCodexModelId(modelId);
|
||||
if (!normalizedModelId) {
|
||||
return false;
|
||||
}
|
||||
return CODEX_FAST_MODE_SUPPORTED_MODE_PREFIXES.some(
|
||||
(prefix) => normalizedModelId === prefix || normalizedModelId.startsWith(prefix),
|
||||
);
|
||||
}
|
||||
|
||||
type CodexConfiguredDefaults = {
|
||||
model?: string;
|
||||
thinkingOptionId?: string;
|
||||
@@ -714,28 +684,43 @@ function extractUserText(content: unknown): string | null {
|
||||
return parts.length > 0 ? parts.join("\n") : null;
|
||||
}
|
||||
|
||||
function parsePlanTextToTodoItems(text: string): { text: string; completed: boolean }[] {
|
||||
const lines = text
|
||||
function normalizePlanMarkdown(text: string): string {
|
||||
return text
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0);
|
||||
if (lines.length === 0) {
|
||||
return [{ text, completed: false }];
|
||||
}
|
||||
return lines.map((line) => ({
|
||||
text: line.replace(/^[-*]\s+/, ""),
|
||||
completed: false,
|
||||
}));
|
||||
.map((line) => line.replace(/\s+$/, ""))
|
||||
.join("\n")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function planStepsToTodoItems(steps: Array<{ step: string; status: string }>): {
|
||||
text: string;
|
||||
completed: boolean;
|
||||
}[] {
|
||||
return steps.map((entry) => ({
|
||||
text: entry.step,
|
||||
completed: entry.status === "completed",
|
||||
}));
|
||||
function planStepsToMarkdown(steps: Array<{ step: string; status: string }>): string {
|
||||
const lines = steps
|
||||
.map((entry) => entry.step.trim())
|
||||
.filter((step) => step.length > 0)
|
||||
.map((step) => {
|
||||
if (/^(#{1,6}\s|[-*+]\s|\d+\.\s)/.test(step)) {
|
||||
return step;
|
||||
}
|
||||
return `- ${step}`;
|
||||
});
|
||||
return normalizePlanMarkdown(lines.join("\n"));
|
||||
}
|
||||
|
||||
function mapCodexPlanToToolCall(params: { callId: string; text: string }): ToolCallTimelineItem | null {
|
||||
const text = normalizePlanMarkdown(params.text);
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: "tool_call",
|
||||
callId: params.callId,
|
||||
name: "plan",
|
||||
status: "completed",
|
||||
error: null,
|
||||
detail: {
|
||||
type: "plan",
|
||||
text,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
type CodexPatchFileChange = {
|
||||
@@ -1120,9 +1105,12 @@ function threadItemToTimeline(
|
||||
return { type: "assistant_message", text: normalizedItem.text ?? "" };
|
||||
}
|
||||
case "plan": {
|
||||
const text = normalizedItem.text ?? "";
|
||||
const items = parsePlanTextToTodoItems(text);
|
||||
return { type: "todo", items };
|
||||
return mapCodexPlanToToolCall({
|
||||
callId:
|
||||
nonEmptyString(normalizedItem.id ?? normalizedItem.itemId ?? undefined) ??
|
||||
`plan:${normalizePlanMarkdown(normalizedItem.text ?? "")}`,
|
||||
text: normalizedItem.text ?? "",
|
||||
});
|
||||
}
|
||||
case "reasoning": {
|
||||
const summary = Array.isArray(normalizedItem.summary)
|
||||
@@ -2045,6 +2033,22 @@ function buildCodexAppServerEnv(
|
||||
};
|
||||
}
|
||||
|
||||
function buildCodexAppServerInitializeParams(): {
|
||||
clientInfo: { name: string; title: string; version: string };
|
||||
capabilities: { experimentalApi: true };
|
||||
} {
|
||||
return {
|
||||
clientInfo: {
|
||||
name: "paseo",
|
||||
title: "Paseo",
|
||||
version: "0.0.0",
|
||||
},
|
||||
capabilities: {
|
||||
experimentalApi: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
class CodexAppServerAgentSession implements AgentSession {
|
||||
readonly provider = CODEX_PROVIDER;
|
||||
readonly capabilities = CODEX_APP_SERVER_CAPABILITIES;
|
||||
@@ -2060,6 +2064,7 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
private activeForegroundTurnId: string | null = null;
|
||||
private cachedRuntimeInfo: AgentRuntimeInfo | null = null;
|
||||
private serviceTier: "fast" | null = null;
|
||||
private planModeEnabled = false;
|
||||
private historyPending = false;
|
||||
private persistedHistory: AgentTimelineItem[] = [];
|
||||
private pendingPermissions = new Map<string, AgentPermissionRequest>();
|
||||
@@ -2119,6 +2124,9 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
if (this.config.featureValues?.fast_mode) {
|
||||
this.serviceTier = "fast";
|
||||
}
|
||||
if (this.config.featureValues?.plan_mode) {
|
||||
this.planModeEnabled = true;
|
||||
}
|
||||
|
||||
if (this.resumeHandle?.sessionId) {
|
||||
this.currentThreadId = this.resumeHandle.sessionId;
|
||||
@@ -2131,10 +2139,12 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
}
|
||||
|
||||
get features(): AgentFeature[] {
|
||||
if (!codexModelSupportsFastMode(this.config.model)) {
|
||||
return [];
|
||||
}
|
||||
return [{ ...CODEX_FAST_MODE_FEATURE, value: this.serviceTier === "fast" }];
|
||||
return buildCodexFeatures({
|
||||
modelId: this.config.model,
|
||||
fastModeEnabled: this.serviceTier === "fast",
|
||||
planModeEnabled: this.planModeEnabled,
|
||||
planModeAvailable: this.hasPlanCollaborationMode(),
|
||||
});
|
||||
}
|
||||
|
||||
async connect(): Promise<void> {
|
||||
@@ -2144,13 +2154,7 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
this.client.setNotificationHandler((method, params) => this.handleNotification(method, params));
|
||||
this.registerRequestHandlers();
|
||||
|
||||
await this.client.request("initialize", {
|
||||
clientInfo: {
|
||||
name: "paseo",
|
||||
title: "Paseo",
|
||||
version: "0.0.0",
|
||||
},
|
||||
});
|
||||
await this.client.request("initialize", buildCodexAppServerInitializeParams());
|
||||
this.client.notify("initialized", {});
|
||||
|
||||
await this.loadCollaborationModes();
|
||||
@@ -2182,7 +2186,7 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
this.logger.trace({ error }, "Failed to load collaboration modes");
|
||||
this.collaborationModes = [];
|
||||
}
|
||||
this.resolvedCollaborationMode = this.resolveCollaborationMode(this.currentMode);
|
||||
this.refreshResolvedCollaborationMode();
|
||||
}
|
||||
|
||||
private async loadSkills(): Promise<void> {
|
||||
@@ -2211,23 +2215,46 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
}
|
||||
}
|
||||
|
||||
private resolveCollaborationMode(
|
||||
modeId: string,
|
||||
): { mode: string; settings: Record<string, unknown>; name: string } | null {
|
||||
private findCollaborationMode(
|
||||
target: "code" | "plan",
|
||||
): {
|
||||
name: string;
|
||||
mode?: string | null;
|
||||
model?: string | null;
|
||||
reasoning_effort?: string | null;
|
||||
developer_instructions?: string | null;
|
||||
} | null {
|
||||
if (this.collaborationModes.length === 0) return null;
|
||||
const normalized = modeId.toLowerCase();
|
||||
const findByName = (predicate: (name: string) => boolean) =>
|
||||
this.collaborationModes.find((entry) => predicate(entry.name.toLowerCase()));
|
||||
let match =
|
||||
normalized === "read-only"
|
||||
? findByName((name) => name.includes("read") || name.includes("plan"))
|
||||
: normalized === "full-access"
|
||||
? findByName((name) => name.includes("full") || name.includes("exec"))
|
||||
: findByName((name) => name.includes("auto") || name.includes("code"));
|
||||
if (!match) {
|
||||
match = this.collaborationModes[0] ?? null;
|
||||
|
||||
if (target === "plan") {
|
||||
return findByName((name) => name.includes("plan") || name.includes("read")) ?? null;
|
||||
}
|
||||
|
||||
return (
|
||||
findByName((name) => name.includes("auto") || name.includes("code")) ??
|
||||
this.collaborationModes.find((entry) => {
|
||||
const name = entry.name.toLowerCase();
|
||||
return !name.includes("plan") && !name.includes("read");
|
||||
}) ??
|
||||
this.collaborationModes[0] ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
private hasPlanCollaborationMode(): boolean {
|
||||
return this.findCollaborationMode("plan") !== null;
|
||||
}
|
||||
|
||||
private resolveCollaborationMode(): {
|
||||
mode: string;
|
||||
settings: Record<string, unknown>;
|
||||
name: string;
|
||||
} | null {
|
||||
const match = this.findCollaborationMode(this.planModeEnabled ? "plan" : "code");
|
||||
if (!match) return null;
|
||||
|
||||
const settings: Record<string, unknown> = {};
|
||||
if (match.model) settings.model = match.model;
|
||||
if (match.reasoning_effort) settings.reasoning_effort = match.reasoning_effort;
|
||||
@@ -2244,6 +2271,10 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
return { mode: match.mode ?? "code", settings, name: match.name };
|
||||
}
|
||||
|
||||
private refreshResolvedCollaborationMode(): void {
|
||||
this.resolvedCollaborationMode = this.resolveCollaborationMode();
|
||||
}
|
||||
|
||||
private registerRequestHandlers(): void {
|
||||
if (!this.client) return;
|
||||
|
||||
@@ -2425,6 +2456,8 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
timeline.push(event.item);
|
||||
if (event.item.type === "assistant_message") {
|
||||
finalText = event.item.text;
|
||||
} else if (event.item.type === "tool_call" && event.item.detail.type === "plan") {
|
||||
finalText = event.item.detail.text;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -2615,7 +2648,6 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
async setMode(modeId: string): Promise<void> {
|
||||
validateCodexMode(modeId);
|
||||
this.currentMode = modeId;
|
||||
this.resolvedCollaborationMode = this.resolveCollaborationMode(modeId);
|
||||
this.cachedRuntimeInfo = null;
|
||||
}
|
||||
|
||||
@@ -2624,13 +2656,13 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
if (!codexModelSupportsFastMode(this.config.model)) {
|
||||
this.serviceTier = null;
|
||||
}
|
||||
this.resolvedCollaborationMode = this.resolveCollaborationMode(this.currentMode);
|
||||
this.refreshResolvedCollaborationMode();
|
||||
this.cachedRuntimeInfo = null;
|
||||
}
|
||||
|
||||
async setThinkingOption(thinkingOptionId: string | null): Promise<void> {
|
||||
this.config.thinkingOptionId = normalizeCodexThinkingOptionId(thinkingOptionId);
|
||||
this.resolvedCollaborationMode = this.resolveCollaborationMode(this.currentMode);
|
||||
this.refreshResolvedCollaborationMode();
|
||||
this.cachedRuntimeInfo = null;
|
||||
}
|
||||
|
||||
@@ -2640,6 +2672,12 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
this.cachedRuntimeInfo = null;
|
||||
return;
|
||||
}
|
||||
if (featureId === "plan_mode") {
|
||||
this.planModeEnabled = Boolean(value);
|
||||
this.refreshResolvedCollaborationMode();
|
||||
this.cachedRuntimeInfo = null;
|
||||
return;
|
||||
}
|
||||
throw new Error(`Unknown Codex feature: ${featureId}`);
|
||||
}
|
||||
|
||||
@@ -2966,17 +3004,22 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
}
|
||||
|
||||
if (parsed.kind === "plan_updated") {
|
||||
const items = planStepsToTodoItems(
|
||||
parsed.plan.map((entry) => ({
|
||||
step: entry.step ?? "",
|
||||
status: entry.status ?? "pending",
|
||||
})),
|
||||
);
|
||||
this.emitEvent({
|
||||
type: "timeline",
|
||||
provider: CODEX_PROVIDER,
|
||||
item: { type: "todo", items },
|
||||
const timelineItem = mapCodexPlanToToolCall({
|
||||
callId: `plan:${this.currentTurnId ?? this.currentThreadId ?? "current"}`,
|
||||
text: planStepsToMarkdown(
|
||||
parsed.plan.map((entry) => ({
|
||||
step: entry.step ?? "",
|
||||
status: entry.status ?? "pending",
|
||||
})),
|
||||
),
|
||||
});
|
||||
if (timelineItem) {
|
||||
this.emitEvent({
|
||||
type: "timeline",
|
||||
provider: CODEX_PROVIDER,
|
||||
item: timelineItem,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3520,9 +3563,7 @@ export class CodexAppServerAgentClient implements AgentClient {
|
||||
const client = new CodexAppServerClient(child, this.logger);
|
||||
|
||||
try {
|
||||
await client.request("initialize", {
|
||||
clientInfo: { name: "paseo", title: "Paseo", version: "0.0.0" },
|
||||
});
|
||||
await client.request("initialize", buildCodexAppServerInitializeParams());
|
||||
client.notify("initialized", {});
|
||||
|
||||
const limit = options?.limit ?? 20;
|
||||
@@ -3592,13 +3633,7 @@ export class CodexAppServerAgentClient implements AgentClient {
|
||||
const client = new CodexAppServerClient(child, this.logger);
|
||||
|
||||
try {
|
||||
await client.request("initialize", {
|
||||
clientInfo: {
|
||||
name: "paseo",
|
||||
title: "Paseo",
|
||||
version: "0.0.0",
|
||||
},
|
||||
});
|
||||
await client.request("initialize", buildCodexAppServerInitializeParams());
|
||||
client.notify("initialized", {});
|
||||
|
||||
const response = (await client.request("model/list", {})) as { data?: Array<any> };
|
||||
@@ -3689,4 +3724,7 @@ export const __codexAppServerInternals = {
|
||||
codexModelSupportsFastMode,
|
||||
CodexAppServerAgentSession,
|
||||
mapCodexPatchNotificationToToolCall,
|
||||
planStepsToMarkdown,
|
||||
mapCodexPlanToToolCall,
|
||||
threadItemToTimeline,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { AgentFeature, AgentFeatureToggle } from "../agent-sdk-types.js";
|
||||
|
||||
const CODEX_FAST_MODE_SUPPORTED_MODEL_PREFIXES = [
|
||||
"gpt-5",
|
||||
"gpt-4.1",
|
||||
"o3",
|
||||
"o4-mini",
|
||||
] as const;
|
||||
|
||||
export const CODEX_FAST_MODE_FEATURE: Omit<AgentFeatureToggle, "value"> = {
|
||||
type: "toggle",
|
||||
id: "fast_mode",
|
||||
label: "Fast",
|
||||
description: "Priority inference at 2x usage",
|
||||
tooltip: "Toggle fast mode",
|
||||
icon: "zap",
|
||||
};
|
||||
|
||||
export const CODEX_PLAN_MODE_FEATURE: Omit<AgentFeatureToggle, "value"> = {
|
||||
type: "toggle",
|
||||
id: "plan_mode",
|
||||
label: "Plan",
|
||||
description: "Switch Codex into planning-only collaboration mode",
|
||||
tooltip: "Toggle plan mode",
|
||||
icon: "list-todo",
|
||||
};
|
||||
|
||||
function normalizeCodexModelId(modelId: string | null | undefined): string | null {
|
||||
const normalized = typeof modelId === "string" ? modelId.trim() : "";
|
||||
return normalized.length > 0 ? normalized : null;
|
||||
}
|
||||
|
||||
export function codexModelSupportsFastMode(modelId: string | null | undefined): boolean {
|
||||
const normalizedModelId = normalizeCodexModelId(modelId);
|
||||
if (!normalizedModelId) {
|
||||
return false;
|
||||
}
|
||||
return CODEX_FAST_MODE_SUPPORTED_MODEL_PREFIXES.some(
|
||||
(prefix) => normalizedModelId === prefix || normalizedModelId.startsWith(prefix),
|
||||
);
|
||||
}
|
||||
|
||||
export function buildCodexFeatures(input: {
|
||||
modelId: string | null | undefined;
|
||||
fastModeEnabled: boolean;
|
||||
planModeEnabled: boolean;
|
||||
planModeAvailable?: boolean;
|
||||
}): AgentFeature[] {
|
||||
const features: AgentFeature[] = [];
|
||||
|
||||
if (codexModelSupportsFastMode(input.modelId)) {
|
||||
features.push({
|
||||
...CODEX_FAST_MODE_FEATURE,
|
||||
value: input.fastModeEnabled,
|
||||
});
|
||||
}
|
||||
|
||||
if (input.planModeAvailable !== false) {
|
||||
features.push({
|
||||
...CODEX_PLAN_MODE_FEATURE,
|
||||
value: input.planModeEnabled,
|
||||
});
|
||||
}
|
||||
|
||||
return features;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { CodexAppServerAgentClient } from "./codex-app-server-agent.js";
|
||||
import { createTestLogger } from "../../../test-utils/test-logger.js";
|
||||
import { isProviderAvailable } from "../../daemon-e2e/agent-configs.js";
|
||||
|
||||
function tmpCwd(): string {
|
||||
return mkdtempSync(path.join(tmpdir(), "codex-plan-mode-real-"));
|
||||
}
|
||||
|
||||
describe("Codex app-server provider (real) plan mode", () => {
|
||||
test.runIf(isProviderAvailable("codex"))(
|
||||
"maps gpt-5.4 markdown plans to a plan tool call instead of todo items",
|
||||
async () => {
|
||||
const cwd = tmpCwd();
|
||||
const client = new CodexAppServerAgentClient(createTestLogger());
|
||||
|
||||
try {
|
||||
const session = await client.createSession({
|
||||
provider: "codex",
|
||||
cwd,
|
||||
modeId: "auto",
|
||||
model: "gpt-5.4",
|
||||
thinkingOptionId: "medium",
|
||||
});
|
||||
|
||||
try {
|
||||
await session.setFeature?.("plan_mode", true);
|
||||
|
||||
const result = await session.run(
|
||||
"You are in plan mode. Produce a markdown plan with a short heading and exactly 3 bullets for implementing a login screen. Do not ask questions.",
|
||||
);
|
||||
|
||||
expect(result.timeline).not.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: "todo",
|
||||
}),
|
||||
);
|
||||
|
||||
const planCall = result.timeline.find(
|
||||
(item) => item.type === "tool_call" && item.detail.type === "plan",
|
||||
);
|
||||
|
||||
expect(planCall).toBeDefined();
|
||||
if (!planCall || planCall.type !== "tool_call" || planCall.detail.type !== "plan") {
|
||||
throw new Error("Expected a plan tool call");
|
||||
}
|
||||
|
||||
expect(planCall.detail.text).toContain("Login");
|
||||
expect(planCall.detail.text).toContain("- ");
|
||||
expect(result.finalText).toBe(planCall.detail.text);
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
} finally {
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
240_000,
|
||||
);
|
||||
});
|
||||
@@ -1700,6 +1700,10 @@ export class Session {
|
||||
await this.handleListProviderModesRequest(msg);
|
||||
break;
|
||||
|
||||
case "list_provider_features_request":
|
||||
await this.handleListProviderFeaturesRequest(msg);
|
||||
break;
|
||||
|
||||
case "list_available_providers_request":
|
||||
await this.handleListAvailableProvidersRequest(msg);
|
||||
break;
|
||||
@@ -3116,6 +3120,60 @@ export class Session {
|
||||
}
|
||||
}
|
||||
|
||||
private buildDraftAgentSessionConfig(draftConfig: {
|
||||
provider: AgentProvider;
|
||||
cwd: string;
|
||||
modeId?: string;
|
||||
model?: string;
|
||||
thinkingOptionId?: string;
|
||||
featureValues?: Record<string, unknown>;
|
||||
}): AgentSessionConfig {
|
||||
return {
|
||||
provider: draftConfig.provider,
|
||||
cwd: expandTilde(draftConfig.cwd),
|
||||
...(draftConfig.modeId ? { modeId: draftConfig.modeId } : {}),
|
||||
...(draftConfig.model ? { model: draftConfig.model } : {}),
|
||||
...(draftConfig.thinkingOptionId
|
||||
? { thinkingOptionId: draftConfig.thinkingOptionId }
|
||||
: {}),
|
||||
...(draftConfig.featureValues ? { featureValues: draftConfig.featureValues } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
private async handleListProviderFeaturesRequest(
|
||||
msg: Extract<SessionInboundMessage, { type: "list_provider_features_request" }>,
|
||||
): Promise<void> {
|
||||
const fetchedAt = new Date().toISOString();
|
||||
try {
|
||||
const sessionConfig = this.buildDraftAgentSessionConfig(msg.draftConfig);
|
||||
const features = await this.agentManager.listDraftFeatures(sessionConfig);
|
||||
this.emit({
|
||||
type: "list_provider_features_response",
|
||||
payload: {
|
||||
provider: msg.draftConfig.provider,
|
||||
features,
|
||||
error: null,
|
||||
fetchedAt,
|
||||
requestId: msg.requestId,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.sessionLogger.error(
|
||||
{ err: error, provider: msg.draftConfig.provider, draftConfig: msg.draftConfig },
|
||||
`Failed to list features for ${msg.draftConfig.provider}`,
|
||||
);
|
||||
this.emit({
|
||||
type: "list_provider_features_response",
|
||||
payload: {
|
||||
provider: msg.draftConfig.provider,
|
||||
error: (error as Error)?.message ?? String(error),
|
||||
fetchedAt,
|
||||
requestId: msg.requestId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async handleListAvailableProvidersRequest(
|
||||
msg: Extract<SessionInboundMessage, { type: "list_available_providers_request" }>,
|
||||
): Promise<void> {
|
||||
@@ -3632,16 +3690,7 @@ export class Session {
|
||||
}
|
||||
|
||||
if (!agent && draftConfig) {
|
||||
const sessionConfig: AgentSessionConfig = {
|
||||
provider: draftConfig.provider,
|
||||
cwd: expandTilde(draftConfig.cwd),
|
||||
...(draftConfig.modeId ? { modeId: draftConfig.modeId } : {}),
|
||||
...(draftConfig.model ? { model: draftConfig.model } : {}),
|
||||
...(draftConfig.thinkingOptionId
|
||||
? { thinkingOptionId: draftConfig.thinkingOptionId }
|
||||
: {}),
|
||||
};
|
||||
|
||||
const sessionConfig = this.buildDraftAgentSessionConfig(draftConfig);
|
||||
const commands = await this.agentManager.listDraftCommands(sessionConfig);
|
||||
this.emit({
|
||||
type: "list_commands_response",
|
||||
|
||||
@@ -14,6 +14,7 @@ describe("agent feature schemas", () => {
|
||||
id: "fast_mode",
|
||||
label: "Fast mode",
|
||||
description: "Uses lower latency service tier",
|
||||
tooltip: "Toggle fast mode",
|
||||
icon: "bolt",
|
||||
value: true,
|
||||
});
|
||||
@@ -31,6 +32,7 @@ describe("agent feature schemas", () => {
|
||||
id: "service_tier",
|
||||
label: "Service tier",
|
||||
description: "Choose a processing tier",
|
||||
tooltip: "Select service tier",
|
||||
icon: "gauge",
|
||||
value: "flex",
|
||||
options: [
|
||||
@@ -112,6 +114,7 @@ describe("agent feature schemas", () => {
|
||||
type: "toggle",
|
||||
id: "fast_mode",
|
||||
label: "Fast mode",
|
||||
tooltip: "Toggle fast mode",
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -27,6 +27,9 @@ describe("list_commands_request schema", () => {
|
||||
modeId: "bypassPermissions",
|
||||
model: "gpt-5",
|
||||
thinkingOptionId: "off",
|
||||
featureValues: {
|
||||
plan_mode: true,
|
||||
},
|
||||
},
|
||||
requestId: "req-456",
|
||||
});
|
||||
@@ -41,6 +44,9 @@ describe("list_commands_request schema", () => {
|
||||
modeId: "bypassPermissions",
|
||||
model: "gpt-5",
|
||||
thinkingOptionId: "off",
|
||||
featureValues: {
|
||||
plan_mode: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -82,6 +82,7 @@ export const AgentFeatureToggleSchema = z.object({
|
||||
id: z.string(),
|
||||
label: z.string(),
|
||||
description: z.string().optional(),
|
||||
tooltip: z.string().optional(),
|
||||
icon: z.string().optional(),
|
||||
value: z.boolean(),
|
||||
});
|
||||
@@ -91,6 +92,7 @@ export const AgentFeatureSelectSchema = z.object({
|
||||
id: z.string(),
|
||||
label: z.string(),
|
||||
description: z.string().optional(),
|
||||
tooltip: z.string().optional(),
|
||||
icon: z.string().optional(),
|
||||
value: z.string().nullable(),
|
||||
options: z.array(AgentSelectOptionSchema),
|
||||
@@ -315,6 +317,10 @@ const ToolCallDetailPayloadSchema: z.ZodType<ToolCallDetail> = z.discriminatedUn
|
||||
text: z.string().optional(),
|
||||
icon: z.enum(TOOL_CALL_ICON_NAMES).optional(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("plan"),
|
||||
text: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("unknown"),
|
||||
input: UnknownValueSchema,
|
||||
@@ -1154,6 +1160,13 @@ const ListCommandsDraftConfigSchema = z.object({
|
||||
modeId: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
thinkingOptionId: z.string().optional(),
|
||||
featureValues: z.record(z.unknown()).optional(),
|
||||
});
|
||||
|
||||
export const ListProviderFeaturesRequestMessageSchema = z.object({
|
||||
type: z.literal("list_provider_features_request"),
|
||||
draftConfig: ListCommandsDraftConfigSchema,
|
||||
requestId: z.string(),
|
||||
});
|
||||
|
||||
export const ListCommandsRequestSchema = z.object({
|
||||
@@ -1260,6 +1273,7 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
|
||||
CreateAgentRequestMessageSchema,
|
||||
ListProviderModelsRequestMessageSchema,
|
||||
ListProviderModesRequestMessageSchema,
|
||||
ListProviderFeaturesRequestMessageSchema,
|
||||
ListAvailableProvidersRequestMessageSchema,
|
||||
ResumeAgentRequestMessageSchema,
|
||||
RefreshAgentRequestMessageSchema,
|
||||
@@ -2179,6 +2193,17 @@ export const ListProviderModesResponseMessageSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
export const ListProviderFeaturesResponseMessageSchema = z.object({
|
||||
type: z.literal("list_provider_features_response"),
|
||||
payload: z.object({
|
||||
provider: AgentProviderSchema,
|
||||
features: z.array(AgentFeatureSchema).optional(),
|
||||
error: z.string().nullable().optional(),
|
||||
fetchedAt: z.string(),
|
||||
requestId: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
const ProviderAvailabilitySchema = z.object({
|
||||
provider: AgentProviderSchema,
|
||||
available: z.boolean(),
|
||||
@@ -2386,6 +2411,7 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
|
||||
FileDownloadTokenResponseSchema,
|
||||
ListProviderModelsResponseMessageSchema,
|
||||
ListProviderModesResponseMessageSchema,
|
||||
ListProviderFeaturesResponseMessageSchema,
|
||||
ListAvailableProvidersResponseSchema,
|
||||
ListCommandsResponseSchema,
|
||||
ListTerminalsResponseSchema,
|
||||
@@ -2462,6 +2488,9 @@ export type ListProviderModelsResponseMessage = z.infer<
|
||||
export type ListProviderModesResponseMessage = z.infer<
|
||||
typeof ListProviderModesResponseMessageSchema
|
||||
>;
|
||||
export type ListProviderFeaturesResponseMessage = z.infer<
|
||||
typeof ListProviderFeaturesResponseMessageSchema
|
||||
>;
|
||||
export type ListAvailableProvidersResponse = z.infer<typeof ListAvailableProvidersResponseSchema>;
|
||||
export type ChatCreateResponse = z.infer<typeof ChatCreateResponseSchema>;
|
||||
export type ChatListResponse = z.infer<typeof ChatListResponseSchema>;
|
||||
@@ -2504,6 +2533,9 @@ export type ListProviderModelsRequestMessage = z.infer<
|
||||
export type ListProviderModesRequestMessage = z.infer<
|
||||
typeof ListProviderModesRequestMessageSchema
|
||||
>;
|
||||
export type ListProviderFeaturesRequestMessage = z.infer<
|
||||
typeof ListProviderFeaturesRequestMessageSchema
|
||||
>;
|
||||
export type ListAvailableProvidersRequestMessage = z.infer<
|
||||
typeof ListAvailableProvidersRequestMessageSchema
|
||||
>;
|
||||
|
||||
@@ -140,4 +140,20 @@ describe("shared tool-call display mapping", () => {
|
||||
summary: "npm run test",
|
||||
});
|
||||
});
|
||||
|
||||
it("labels plan detail rows as Plan", () => {
|
||||
const display = buildToolCallDisplayModel({
|
||||
name: "plan",
|
||||
status: "completed",
|
||||
error: null,
|
||||
detail: {
|
||||
type: "plan",
|
||||
text: "### Login Screen\n- Build layout",
|
||||
},
|
||||
});
|
||||
|
||||
expect(display).toEqual({
|
||||
displayName: "Plan",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -109,6 +109,10 @@ function buildCanonicalDetailDisplay(input: ToolCallDisplayInput): DetailDisplay
|
||||
return {
|
||||
summary: input.detail.label,
|
||||
};
|
||||
case "plan":
|
||||
return {
|
||||
displayName: "Plan",
|
||||
};
|
||||
case "unknown":
|
||||
return {};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user