refactor(server,app): lift return types and parse at boundaries (T2 typeaware production sweep) (#758)

* refactor(server,app): lift return types and parse at boundaries (T2 typeaware production sweep)

* fix(server): align acp-agent test assertions with pino call shape
This commit is contained in:
Mohamed Boudra
2026-05-05 17:16:38 +08:00
parent 53e1be4da6
commit 685a3d0ee2
8 changed files with 203 additions and 150 deletions

View File

@@ -32,12 +32,13 @@ export async function loadRealDaemonState(): Promise<RealDaemonState> {
if (!paseoHome) throw new Error("E2E_PASEO_HOME not set — globalSetup must run first");
const resp = await fetch(`http://127.0.0.1:${port}/api/status`);
const data = (await resp.json()) as DaemonApiStatus;
const data: DaemonApiStatus = await resp.json();
let pid: number | null = null;
try {
const raw = readFileSync(`${paseoHome}/paseo.pid`, "utf8");
pid = (JSON.parse(raw) as PidFileContent).pid ?? null;
const pidContent: PidFileContent = JSON.parse(raw);
pid = pidContent.pid ?? null;
} catch (err) {
// PID file may not be present yet on a very fresh daemon start
console.warn("[desktop-updates] paseo.pid not found:", err);
@@ -70,6 +71,12 @@ export interface ConfirmDialogCall {
title: string | undefined;
}
declare global {
interface Window {
__capturedDialogCall: ConfirmDialogCall | undefined;
}
}
/**
* Injects window.paseoDesktop before app load so all Electron-gated code
* activates. The update-check IPC is mocked at the boundary so the real
@@ -151,12 +158,14 @@ export async function injectDesktopBridge(page: Page, config: DesktopBridgeConfi
}
if (command === "patch_desktop_settings") {
const patchDaemon =
args?.daemon && typeof args.daemon === "object"
? (args.daemon as Record<string, unknown>)
: {};
if (typeof patchDaemon.manageBuiltInDaemon === "boolean") {
manageDaemon = patchDaemon.manageBuiltInDaemon;
const daemon = args?.daemon;
if (
daemon !== null &&
typeof daemon === "object" &&
"manageBuiltInDaemon" in daemon &&
typeof daemon.manageBuiltInDaemon === "boolean"
) {
manageDaemon = daemon.manageBuiltInDaemon;
}
return {
releaseChannel: "stable",
@@ -183,9 +192,9 @@ export async function injectDesktopBridge(page: Page, config: DesktopBridgeConfi
},
dialog: {
ask: async (message: string, options?: Record<string, unknown>) => {
(window as unknown as Record<string, unknown>).__capturedDialogCall = {
window.__capturedDialogCall = {
message,
title: options?.title,
title: typeof options?.title === "string" ? options.title : undefined,
};
return cfg.confirmShouldAccept ?? false;
},
@@ -228,13 +237,8 @@ export async function interceptDaemonManagementConfirmDialog(
page: Page,
): Promise<ConfirmDialogCall> {
await page.getByRole("switch", { name: "Manage built-in daemon" }).click();
await page.waitForFunction(
() => !!(window as unknown as Record<string, unknown>).__capturedDialogCall,
{ timeout: 5_000 },
);
return page.evaluate(
() => (window as unknown as Record<string, unknown>).__capturedDialogCall as ConfirmDialogCall,
);
await page.waitForFunction(() => !!window.__capturedDialogCall, { timeout: 5_000 });
return page.evaluate(() => window.__capturedDialogCall!);
}
export async function toggleDaemonManagement(

View File

@@ -20,7 +20,7 @@ const IS_WEB = platformIsWeb;
import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox";
const EMPTY_COMBOBOX_OPTIONS: ReadonlyArray<ComboboxOption> = [];
const EMPTY_COMBOBOX_OPTIONS: ComboboxOption[] = [];
function noop() {}
@@ -446,12 +446,11 @@ function ProviderSearchInput({
const InputComponent = isMobile ? BottomSheetTextInput : TextInput;
useEffect(() => {
if (autoFocus && platformIsWeb && inputRef.current) {
const timer = setTimeout(() => {
inputRef.current?.focus();
}, 50);
return () => clearTimeout(timer);
}
if (!autoFocus || !platformIsWeb || !inputRef.current) return () => {};
const timer = setTimeout(() => {
inputRef.current?.focus();
}, 50);
return () => clearTimeout(timer);
}, [autoFocus]);
const inputStyle = useMemo(
@@ -699,12 +698,12 @@ export function CombinedModelSelector({
useEffect(() => {
if (platformIsWeb) {
return;
return () => {};
}
if (!isOpen) {
setIsContentReady(false);
return;
return () => {};
}
const frame = requestAnimationFrame(() => {
@@ -791,7 +790,7 @@ export function CombinedModelSelector({
)}
</Pressable>
<Combobox
options={EMPTY_COMBOBOX_OPTIONS as ComboboxOption[]}
options={EMPTY_COMBOBOX_OPTIONS}
value=""
onSelect={noop}
open={isOpen}

View File

@@ -865,11 +865,7 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
}, [client, isConnected, serverId]);
useEffect(() => {
if (!voiceRuntime) {
return;
}
return voiceRuntime.registerSession({
const unregister = voiceRuntime?.registerSession({
serverId,
setVoiceMode: async (enabled, agentId) => {
if (!client) {
@@ -899,6 +895,7 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
setIsPlayingAudio(serverId, isPlaying);
},
});
return () => unregister?.();
}, [client, serverId, setIsPlayingAudio, voiceRuntime]);
useEffect(() => {
@@ -916,7 +913,7 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
useEffect(() => {
if (!client || !isConnected) {
return;
return () => {};
}
let cancelled = false;
@@ -1242,10 +1239,10 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
if (message.payload.kind === "remove") {
clearWorkspaceArchivePending({
serverId,
workspaceId: String(message.payload.id),
workspaceId: message.payload.id,
});
removeWorkspaceSetup({ serverId, workspaceId: String(message.payload.id) });
removeWorkspace(serverId, String(message.payload.id));
removeWorkspaceSetup({ serverId, workspaceId: message.payload.id });
removeWorkspace(serverId, message.payload.id);
return;
}
const workspace = normalizeWorkspaceDescriptor(message.payload.workspace);
@@ -1403,15 +1400,10 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
}
if (data.type === "tool_call" && data.metadata) {
const {
toolCallId,
toolName,
arguments: args,
} = data.metadata as {
toolCallId: string;
toolName: string;
arguments: unknown;
};
const toolCallId =
typeof data.metadata.toolCallId === "string" ? data.metadata.toolCallId : "";
const toolName = typeof data.metadata.toolName === "string" ? data.metadata.toolName : "";
const args = data.metadata.arguments;
setMessages(serverId, (prev) => [
...prev,
@@ -1428,10 +1420,9 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
}
if (data.type === "tool_result" && data.metadata) {
const { toolCallId, result } = data.metadata as {
toolCallId: string;
result: unknown;
};
const toolCallId =
typeof data.metadata.toolCallId === "string" ? data.metadata.toolCallId : "";
const result = data.metadata.result;
const applyToolResult = applyToolResultToMessages(toolCallId, result);
setMessages(serverId, applyToolResult);
@@ -1439,10 +1430,9 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
}
if (data.type === "error" && data.metadata && "toolCallId" in data.metadata) {
const { toolCallId, error } = data.metadata as {
toolCallId: string;
error: unknown;
};
const toolCallId =
typeof data.metadata.toolCallId === "string" ? data.metadata.toolCallId : "";
const error = data.metadata.error;
const applyToolError = applyToolErrorToMessages(toolCallId, error);
setMessages(serverId, applyToolError);
@@ -1800,7 +1790,7 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
} catch (error) {
console.error("[Session] Failed to prepare images for agent creation:", error);
}
return client.createAgent({
await client.createAgent({
config,
...(trimmedPrompt ? { initialPrompt: trimmedPrompt } : {}),
...(imagesData && imagesData.length > 0 ? { images: imagesData } : {}),

View File

@@ -601,8 +601,8 @@ export interface WaitForFinishResult {
interface Waiter<T> {
predicate: (msg: SessionOutboundMessage) => T | null;
resolve: (value: T) => void;
reject: (error: Error) => void;
resolve(value: T): void;
reject(error: Error): void;
timeoutHandle: ReturnType<typeof setTimeout> | null;
}
@@ -1160,7 +1160,7 @@ export class DaemonClient {
}
const type = arg1;
const handler = arg2 as (message: SessionOutboundMessage) => void;
const handler = arg2!;
if (!this.messageHandlers.has(type)) {
this.messageHandlers.set(type, new Set());
@@ -4043,7 +4043,13 @@ export class DaemonClient {
const parsed = WSOutboundMessageSchema.safeParse(parsedJson);
if (!parsed.success) {
const msgType = (parsedJson as { type?: string })?.type ?? "unknown";
const msgType =
parsedJson != null &&
typeof parsedJson === "object" &&
"type" in parsedJson &&
typeof parsedJson.type === "string"
? parsedJson.type
: "unknown";
this.logger.warn({ msgType, error: parsed.error.message }, "Message validation failed");
return;
}
@@ -4405,7 +4411,7 @@ export class DaemonClient {
timeout > 0
? setTimeout(() => {
if (waiter) {
this.waiters.delete(waiter as Waiter<unknown>);
this.waiters.delete(waiter);
}
wrappedReject(timeoutError);
}, timeout)
@@ -4417,7 +4423,7 @@ export class DaemonClient {
reject: wrappedReject,
timeoutHandle,
};
this.waiters.add(waiter as Waiter<unknown>);
this.waiters.add(waiter);
});
const cancel = (error: Error) => {
@@ -4426,7 +4432,7 @@ export class DaemonClient {
}
if (waiter) {
this.waiters.delete(waiter as Waiter<unknown>);
this.waiters.delete(waiter);
if (waiter.timeoutHandle) {
clearTimeout(waiter.timeoutHandle);
}

View File

@@ -220,7 +220,7 @@ test("ACP setModel only uses config-option fallback when the matching select cho
await expect(session.setModel("new-provider-model")).resolves.toBeUndefined();
expect(childLogger.warn).toHaveBeenCalledWith(
"new-provider-model",
{ value: "new-provider-model" },
expect.stringContaining("is not a valid claude-acp model config option"),
);
expect(setSessionConfigOption).not.toHaveBeenCalled();
@@ -514,11 +514,11 @@ describe("ACPAgentSession Zed parity", () => {
expect(invalid.setSessionMode).not.toHaveBeenCalled();
expect(invalid.unstableSetSessionModel).not.toHaveBeenCalled();
expect(childLogger.warn).toHaveBeenCalledWith(
expect.stringContaining("acceptEdits"),
{ value: expect.stringContaining("acceptEdits") },
expect.stringContaining("not valid"),
);
expect(childLogger.warn).toHaveBeenCalledWith(
expect.stringContaining("opus"),
{ value: expect.stringContaining("opus") },
expect.stringContaining("not a valid"),
);
});

View File

@@ -3,6 +3,10 @@ import { randomUUID } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { Readable, Writable } from "node:stream";
import type {
ReadableStream as NodeReadableStream,
WritableStream as NodeWritableStream,
} from "node:stream/web";
import {
ClientSideConnection,
PROTOCOL_VERSION,
@@ -90,6 +94,18 @@ import { appendOrReplaceGrowingAssistantMessage, runProviderTurn } from "./provi
import { findExecutable } from "../../../utils/executable.js";
import { spawnProcess } from "../../../utils/spawn.js";
function assertChildWithPipes(
child: ChildProcess,
): asserts child is ChildProcessWithoutNullStreams {
if (!child.stdin || !child.stdout || !child.stderr) {
throw new Error("Child process did not expose stdio pipes");
}
}
function isRecord(value: unknown): value is Record<string, unknown> {
return value != null && typeof value === "object" && !Array.isArray(value);
}
const DEFAULT_ACP_CAPABILITIES: AgentCapabilityFlags = {
supportsStreaming: true,
supportsSessionPersistence: true,
@@ -120,8 +136,8 @@ function summarizeMalformedACPStdoutError(error: unknown): { type: string; messa
}
export function createLoggedNdJsonStream(
output: WritableStream<Uint8Array>,
input: ReadableStream<Uint8Array>,
output: NodeWritableStream,
input: NodeReadableStream,
options: { logger: Logger; provider: string },
): ACPStream {
const textEncoder = new TextEncoder();
@@ -152,7 +168,7 @@ export function createLoggedNdJsonStream(
}
try {
const message = JSON.parse(trimmedLine) as AnyMessage;
const message: AnyMessage = JSON.parse(trimmedLine);
controller.enqueue(message);
} catch (error) {
options.logger.warn(
@@ -629,7 +645,8 @@ export class ACPAgentClient implements AgentClient {
overlays: [launchEnv],
}),
stdio: ["pipe", "pipe", "pipe"],
}) as ChildProcessWithoutNullStreams;
});
assertChildWithPipes(child);
const stderrChunks: string[] = [];
child.stderr.on("data", (chunk: Buffer | string) => {
@@ -643,13 +660,9 @@ export class ACPAgentClient implements AgentClient {
});
});
if (!child.stdin || !child.stdout) {
throw new Error(`${this.provider} ACP process did not expose stdio pipes`);
}
const stream = createLoggedNdJsonStream(
Writable.toWeb(child.stdin) as WritableStream<Uint8Array>,
Readable.toWeb(child.stdout) as ReadableStream<Uint8Array>,
Writable.toWeb(child.stdin),
Readable.toWeb(child.stdout),
{ logger: this.logger, provider: this.provider },
);
const connection = new ClientSideConnection(() => this.buildProbeClient(), stream);
@@ -1546,7 +1559,8 @@ export class ACPAgentSession implements AgentSession, ACPClient {
overlays: [this.launchEnv],
}),
stdio: ["pipe", "pipe", "pipe"],
}) as ChildProcessWithoutNullStreams;
});
assertChildWithPipes(child);
const stderrChunks: string[] = [];
child.stderr.on("data", (chunk: Buffer | string) => {
@@ -1568,13 +1582,9 @@ export class ACPAgentSession implements AgentSession, ACPClient {
}
});
if (!child.stdin || !child.stdout) {
throw new Error(`${this.provider} ACP process did not expose stdio pipes`);
}
const stream = createLoggedNdJsonStream(
Writable.toWeb(child.stdin) as WritableStream<Uint8Array>,
Readable.toWeb(child.stdout) as ReadableStream<Uint8Array>,
Writable.toWeb(child.stdin),
Readable.toWeb(child.stdout),
{ logger: this.logger, provider: this.provider },
);
const connection = new ClientSideConnection(() => this, stream);
@@ -1630,7 +1640,7 @@ export class ACPAgentSession implements AgentSession, ACPClient {
}
private warnInvalidSelection(value: string, message: string): void {
(this.logger.warn as unknown as (value: string, message: string) => void)(value, message);
this.logger.warn({ value }, message);
}
private translateSessionUpdate(update: SessionUpdate): AgentStreamEvent[] {
@@ -2410,9 +2420,7 @@ function appendTerminalOutput(entry: TerminalEntry, chunk: string): void {
}
function readRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
return isRecord(value) ? value : null;
}
function readString(record: Record<string, unknown> | null, keys: string[]): string | undefined {
@@ -2473,7 +2481,7 @@ function stringifyUnknown(value: unknown): string | undefined {
try {
return JSON.stringify(value);
} catch {
return String(value);
return typeof value === "bigint" ? String(value) : "[unserializable]";
}
}

View File

@@ -30,7 +30,7 @@ import type {
import type { Logger } from "pino";
import { homedir } from "node:os";
import type { ChildProcessWithoutNullStreams } from "node:child_process";
import type { ChildProcess, ChildProcessWithoutNullStreams } from "node:child_process";
import { randomUUID } from "node:crypto";
import { Dirent } from "node:fs";
import fs from "node:fs/promises";
@@ -65,6 +65,18 @@ import {
import { runProviderTurn } from "./provider-runner.js";
import type { WorkspaceGitService } from "../../workspace-git-service.js";
function assertChildWithPipes(
child: ChildProcess,
): asserts child is ChildProcessWithoutNullStreams {
if (!child.stdin || !child.stdout || !child.stderr) {
throw new Error("Child process did not expose stdio pipes");
}
}
function isRecord(value: unknown): value is Record<string, unknown> {
return value != null && typeof value === "object" && !Array.isArray(value);
}
const DEFAULT_TIMEOUT_MS = 14 * 24 * 60 * 60 * 1000;
const TURN_START_TIMEOUT_MS = 90 * 1000;
const INTERRUPT_TIMEOUT_MS = 2_000;
@@ -564,29 +576,24 @@ interface JsonRpcNotification {
params?: unknown;
}
type JsonRpcMessage = JsonRpcRequest | JsonRpcResponse | JsonRpcNotification;
function isJsonRpcResponse(msg: JsonRpcMessage): msg is JsonRpcResponse {
const m = msg as unknown as Record<string, unknown>;
if (typeof m.id !== "number") return false;
return m.result !== undefined || !!m.error;
function isJsonRpcResponse(msg: unknown): msg is JsonRpcResponse {
if (!isRecord(msg)) return false;
if (typeof msg.id !== "number") return false;
return msg.result !== undefined || !!msg.error;
}
function isJsonRpcRequest(msg: JsonRpcMessage): msg is JsonRpcRequest {
const m = msg as unknown as Record<string, unknown>;
return typeof m.id === "number" && typeof m.method === "string";
function isJsonRpcRequest(msg: unknown): msg is JsonRpcRequest {
if (!isRecord(msg)) return false;
return typeof msg.id === "number" && typeof msg.method === "string";
}
function isJsonRpcNotification(msg: JsonRpcMessage): msg is JsonRpcNotification {
const m = msg as unknown as Record<string, unknown>;
return typeof m.method === "string" && typeof m.id !== "number";
function isJsonRpcNotification(msg: unknown): msg is JsonRpcNotification {
if (!isRecord(msg)) return false;
return typeof msg.method === "string" && typeof msg.id !== "number";
}
function toObjectRecord(value: unknown): Record<string, unknown> | undefined {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return undefined;
}
return value as Record<string, unknown>;
return isRecord(value) ? value : undefined;
}
interface PendingRequest {
@@ -615,9 +622,28 @@ interface CodexModel {
supportedReasoningEfforts?: CodexReasoningEffortEntry[];
}
interface CodexModelListResponse {
data?: CodexModel[];
}
const CodexModelListResponseSchema = z.object({
data: z
.array(
z.object({
id: z.string(),
displayName: z.string().optional(),
description: z.string().optional(),
isDefault: z.boolean().optional(),
model: z.string().optional(),
defaultReasoningEffort: z.string().optional(),
supportedReasoningEfforts: z
.array(
z.object({
reasoningEffort: z.string().optional(),
description: z.string().optional(),
}),
)
.optional(),
}),
)
.optional(),
});
class CodexAppServerClient {
private readonly rl: readline.Interface;
@@ -740,31 +766,30 @@ class CodexAppServerClient {
private async handleLine(line: string): Promise<void> {
if (!line.trim()) return;
const raw = JSON.parse(line);
const msg = toObjectRecord(raw) as unknown as JsonRpcMessage;
if (!msg) {
const raw: unknown = JSON.parse(line);
if (!isRecord(raw)) {
this.logger.warn({ line }, "Parsed JSON is not an object");
return;
}
if (isJsonRpcResponse(msg)) {
const id = msg.id;
if (msg.result !== undefined || msg.error) {
if (isJsonRpcResponse(raw)) {
const id = raw.id;
if (raw.result !== undefined || raw.error) {
const pending = this.pending.get(id);
if (!pending) return;
clearTimeout(pending.timer);
this.pending.delete(id);
if (msg.error) {
pending.reject(new Error(msg.error.message ?? "Unknown error"));
if (raw.error) {
pending.reject(new Error(raw.error.message ?? "Unknown error"));
} else {
pending.resolve(msg.result);
pending.resolve(raw.result);
}
return;
}
// Server-initiated request
if (isJsonRpcRequest(msg)) {
const request = msg;
if (isJsonRpcRequest(raw)) {
const request = raw;
const handler = this.requestHandlers.get(request.method);
try {
const result = handler ? await handler(request.params) : {};
@@ -779,8 +804,8 @@ class CodexAppServerClient {
}
}
if (isJsonRpcNotification(msg)) {
this.notificationHandler?.(msg.method, msg.params);
if (isJsonRpcNotification(raw)) {
this.notificationHandler?.(raw.method, raw.params);
}
}
}
@@ -4322,14 +4347,16 @@ class CodexAppServerAgentSession implements AgentSession {
}
private handleCommandApprovalRequest(params: unknown): Promise<unknown> {
const parsed = params as {
itemId: string;
threadId: string;
turnId: string;
command?: string | null;
cwd?: string | null;
reason?: string | null;
};
const parsed = z
.object({
itemId: z.string(),
threadId: z.string(),
turnId: z.string(),
command: z.string().nullable().optional(),
cwd: z.string().nullable().optional(),
reason: z.string().nullable().optional(),
})
.parse(params);
const commandPreview = mapCodexExecNotificationToToolCall({
callId: parsed.itemId,
command: parsed.command,
@@ -4371,12 +4398,14 @@ class CodexAppServerAgentSession implements AgentSession {
}
private handleFileChangeApprovalRequest(params: unknown): Promise<unknown> {
const parsed = params as {
itemId: string;
threadId: string;
turnId: string;
reason?: string | null;
};
const parsed = z
.object({
itemId: z.string(),
threadId: z.string(),
turnId: z.string(),
reason: z.string().nullable().optional(),
})
.parse(params);
const requestId = `permission-${parsed.itemId}`;
const request: AgentPermissionRequest = {
id: requestId,
@@ -4406,12 +4435,14 @@ class CodexAppServerAgentSession implements AgentSession {
}
private handleToolApprovalRequest(params: unknown): Promise<unknown> {
const parsed = params as {
itemId: string;
threadId: string;
turnId: string;
questions: unknown[];
};
const parsed = z
.object({
itemId: z.string(),
threadId: z.string(),
turnId: z.string(),
questions: z.array(z.unknown()),
})
.parse(params);
const requestId = `permission-${parsed.itemId}`;
const questions = normalizeCodexQuestionPrompts(parsed.questions);
const request: AgentPermissionRequest = {
@@ -4475,14 +4506,16 @@ export class CodexAppServerAgentClient implements AgentClient {
},
"Spawning Codex app server",
);
return spawnProcess(launchPrefix.command, [...launchPrefix.args, "app-server"], {
const child = spawnProcess(launchPrefix.command, [...launchPrefix.args, "app-server"], {
detached: process.platform !== "win32",
stdio: ["pipe", "pipe", "pipe"],
...createProviderEnvSpec({
runtimeSettings: this.runtimeSettings,
overlays: [launchEnv],
}),
}) as ChildProcessWithoutNullStreams;
});
assertChildWithPipes(child);
return child;
}
async createSession(
@@ -4508,7 +4541,7 @@ export class CodexAppServerAgentClient implements AgentClient {
overrides?: Partial<AgentSessionConfig>,
launchContext?: AgentLaunchContext,
): Promise<AgentSession> {
const storedConfig = (handle.metadata ?? {}) as unknown as AgentSessionConfig;
const storedConfig = (handle.metadata ?? {}) as Partial<AgentSessionConfig>;
const merged: AgentSessionConfig = {
...storedConfig,
...overrides,
@@ -4599,8 +4632,9 @@ export class CodexAppServerAgentClient implements AgentClient {
await client.request("initialize", buildCodexAppServerInitializeParams());
client.notify("initialized", {});
const response = (await client.request("model/list", {})) as CodexModelListResponse;
const models = Array.isArray(response?.data) ? response.data : [];
const rawResponse = await client.request("model/list", {});
const parsedResponse = CodexModelListResponseSchema.safeParse(rawResponse);
const models = parsedResponse.success ? (parsedResponse.data.data ?? []) : [];
const configuredDefaults = await readCodexConfiguredDefaults(client, this.logger);
const configuredDefaultModelId = configuredDefaults.model;
const configuredDefaultThinkingOptionId = configuredDefaults.thinkingOptionId;

View File

@@ -44,6 +44,18 @@ interface PullRequestStatusLookupTarget {
headRepositoryOwner?: string;
}
function getErrorStderr(error: Error): string {
return "stderr" in error && typeof error.stderr === "string" ? error.stderr : "";
}
function getErrorStdout(error: Error): string {
return "stdout" in error && typeof error.stdout === "string" ? error.stdout : "";
}
function throwBranchNotFound(branch: string | undefined): never {
throw new Error(`Branch not found: ${branch ?? "unknown"}`);
}
function createPullRequestStatusCache(ttlMs: number) {
return new TTLCache<string, PullRequestStatusResult>({
ttl: ttlMs,
@@ -380,8 +392,8 @@ export async function checkoutResolvedBranch(
cwd,
});
return { source: "remote" };
case "not-found":
throw new Error(`Branch not found: ${input.requestedBranch ?? "unknown"}`);
default:
return throwBranchNotFound(input.requestedBranch);
}
}
@@ -1967,7 +1979,7 @@ async function detectAndThrowMergeToBaseConflict(
const { operationCwd, error, baseRef, currentBranch } = input;
const errorDetails =
error instanceof Error
? `${error.message}\n${(error as { stderr?: string }).stderr ?? ""}\n${(error as { stdout?: string }).stdout ?? ""}`
? `${error.message}\n${getErrorStderr(error)}\n${getErrorStdout(error)}`
: String(error);
try {
const [unmergedOutput, lsFilesOutput, statusOutput] = await Promise.all([
@@ -1990,7 +2002,7 @@ async function detectAndThrowMergeToBaseConflict(
.split("\n")
.map((line) => line.trim())
.filter(Boolean)
.map((line) => line.split("\t").pop() as string),
.map((line) => line.split("\t").at(-1) ?? ""),
...statusConflicts,
].filter(Boolean);
const conflictDetected =
@@ -2150,7 +2162,7 @@ async function detectAndThrowMergeFromBaseConflict(
const { cwd, error, baseRef, currentBranch } = input;
const errorDetails =
error instanceof Error
? `${error.message}\n${(error as { stderr?: string }).stderr ?? ""}\n${(error as { stdout?: string }).stdout ?? ""}`
? `${error.message}\n${getErrorStderr(error)}\n${getErrorStdout(error)}`
: String(error);
try {
const [unmergedOutput, lsFilesOutput, statusOutput] = await Promise.all([
@@ -2173,7 +2185,7 @@ async function detectAndThrowMergeFromBaseConflict(
.split("\n")
.map((line) => line.trim())
.filter(Boolean)
.map((line) => line.split("\t").pop() as string),
.map((line) => line.split("\t").at(-1) ?? ""),
...statusConflicts,
].filter(Boolean);
const conflictDetected =