From dc886313eeb9038f85faa3542b5d1034e86c3b40 Mon Sep 17 00:00:00 2001 From: Codeplane Agent Date: Mon, 1 Jun 2026 04:24:30 +0000 Subject: [PATCH 1/2] fix: apply upstream OpenCode fixes from v1.15.11-v1.15.13 Backports critical bug fixes and improvements from OpenCode releases v1.15.11 through v1.15.13 that affect the Codeplane codebase. - fix(provider): tag SSE read timeout error as ResponseStreamError for retry logic (upstream #29769) - fix(codex): use endsWith("/responses") instead of includes to support custom OpenAI-compatible base URLs without /v1/ prefix (upstream #29636) - fix(editor): add cwd parameter to Editor.open() and guard against worktree="/" for non-git projects (upstream #29180) - fix(tui): add InlineToolRow wrapper with dynamic margin detection and click-to-expand error display for failed inline tools (upstream #28664) - feat(tui): show reasoning spinner while assistant is still thinking, with title from first line of reasoning text (upstream #29765) - feat(tui): surface subagent retry status in Task component with error coloring and attempt count (upstream #29591) - fix(tui): pass project directory as cwd to external editor in prompt and session export flows - chore(deps): bump @lydell/node-pty from 1.2.0-beta.10 to beta.12 and remove now-unnecessary @ts-expect-error (upstream #29803) - chore(sdk): regenerate SDK types and OpenAPI spec to include session metadata in GlobalSession and SyncEventSessionUpdated Verification: - bun turbo typecheck: all 8 packages pass - bun lint: 0 errors (2909 warnings, unchanged) - bun test: 13 pre-existing failures unrelated to these changes Co-Authored-By: Claude Opus 4.7 (1M context) --- bun.lock | 36 ++-- package.json | 2 +- packages/codeplane/package.json | 2 +- packages/codeplane/src/plugin/codex.ts | 2 +- packages/codeplane/src/provider/provider.ts | 2 +- packages/codeplane/src/pty/pty.node.ts | 1 - .../src/tui/component/prompt/index.tsx | 11 +- .../tui/feature-plugins/system/session-v2.tsx | 30 ++- .../src/tui/routes/session/index.tsx | 185 +++++++++++++++--- packages/codeplane/src/tui/util/editor.ts | 5 +- .../js/src/gen/core/serverSentEvents.gen.ts | 23 +-- packages/sdk/js/src/gen/sdk.gen.ts | 60 +++++- packages/sdk/js/src/gen/types.gen.ts | 36 ++++ .../src/v2/gen/core/serverSentEvents.gen.ts | 23 +-- packages/sdk/js/src/v2/gen/sdk.gen.ts | 60 +++++- packages/sdk/js/src/v2/gen/types.gen.ts | 39 +++- packages/sdk/openapi.json | 95 ++++++++- 17 files changed, 504 insertions(+), 108 deletions(-) diff --git a/bun.lock b/bun.lock index e89f4b3ddc..baf829d2bf 100644 --- a/bun.lock +++ b/bun.lock @@ -30,7 +30,7 @@ }, "packages/app": { "name": "@codeplane-ai/app", - "version": "31.1.2", + "version": "31.3.4", "dependencies": { "@codeplane-ai/sdk": "file:../sdk/js", "@codeplane-ai/shared": "file:../shared", @@ -84,7 +84,7 @@ }, "packages/codeplane": { "name": "codeplane", - "version": "31.1.2", + "version": "31.3.4", "bin": { "codeplane": "./bin/codeplane", }, @@ -125,7 +125,7 @@ "@hono/standard-validator": "0.1.5", "@hono/zod-validator": "0.4.2", "@inkjs/ui": "2.0.0", - "@lydell/node-pty": "1.2.0-beta.10", + "@lydell/node-pty": "1.2.0-beta.12", "@modelcontextprotocol/sdk": "1.29.0", "@npmcli/arborist": "9.4.0", "@npmcli/config": "10.8.1", @@ -231,7 +231,7 @@ }, "packages/desktop": { "name": "@codeplane-ai/desktop", - "version": "31.1.2", + "version": "31.3.4", "devDependencies": { "@codeplane-ai/shared": "file:../shared", "@codeplane-ai/ui": "file:../ui", @@ -254,7 +254,7 @@ }, "packages/mobile": { "name": "@codeplane-ai/mobile", - "version": "31.1.2", + "version": "31.3.4", "dependencies": { "@capacitor/android": "7.4.4", "@capacitor/app": "7.1.0", @@ -292,7 +292,7 @@ }, "packages/plugin": { "name": "@codeplane-ai/plugin", - "version": "31.1.2", + "version": "31.3.4", "dependencies": { "@codeplane-ai/sdk": "file:../sdk/js", "effect": "4.0.0-beta.48", @@ -315,7 +315,7 @@ }, "packages/script": { "name": "@codeplane-ai/script", - "version": "31.1.2", + "version": "31.3.4", "dependencies": { "@codeplane-ai/shared": "file:../shared", "semver": "^7.6.3", @@ -327,7 +327,7 @@ }, "packages/sdk/js": { "name": "@codeplane-ai/sdk", - "version": "31.1.2", + "version": "31.3.4", "dependencies": { "cross-spawn": "7.0.6", }, @@ -342,7 +342,7 @@ }, "packages/shared": { "name": "@codeplane-ai/shared", - "version": "31.1.2", + "version": "31.3.4", "dependencies": { "@effect/platform-node": "4.0.0-beta.48", "@npmcli/arborist": "9.4.0", @@ -363,7 +363,7 @@ }, "packages/ui": { "name": "@codeplane-ai/ui", - "version": "31.1.2", + "version": "31.3.4", "dependencies": { "@codeplane-ai/sdk": "file:../sdk/js", "@codeplane-ai/shared": "file:../shared", @@ -451,7 +451,7 @@ "@effect/platform-node": "4.0.0-beta.48", "@hono/zod-validator": "0.4.2", "@kobalte/core": "0.13.11", - "@lydell/node-pty": "1.2.0-beta.10", + "@lydell/node-pty": "1.2.0-beta.12", "@npmcli/arborist": "9.4.0", "@octokit/rest": "22.0.0", "@openauthjs/openauth": "0.0.0-20250322224806", @@ -1030,19 +1030,19 @@ "@lukeed/ms": ["@lukeed/ms@2.0.2", "", {}, "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA=="], - "@lydell/node-pty": ["@lydell/node-pty@1.2.0-beta.10", "", { "optionalDependencies": { "@lydell/node-pty-darwin-arm64": "1.2.0-beta.10", "@lydell/node-pty-darwin-x64": "1.2.0-beta.10", "@lydell/node-pty-linux-arm64": "1.2.0-beta.10", "@lydell/node-pty-linux-x64": "1.2.0-beta.10", "@lydell/node-pty-win32-arm64": "1.2.0-beta.10", "@lydell/node-pty-win32-x64": "1.2.0-beta.10" } }, "sha512-Fv+A3+MZVA8qhkBIZsM1E6dCdHNMyXXz22mAYiMWd03LlyK///F3OH6CKPX9mj4id7LUlxpr45yPzyBVy9aDPw=="], + "@lydell/node-pty": ["@lydell/node-pty@1.2.0-beta.12", "", { "optionalDependencies": { "@lydell/node-pty-darwin-arm64": "1.2.0-beta.12", "@lydell/node-pty-darwin-x64": "1.2.0-beta.12", "@lydell/node-pty-linux-arm64": "1.2.0-beta.12", "@lydell/node-pty-linux-x64": "1.2.0-beta.12", "@lydell/node-pty-win32-arm64": "1.2.0-beta.12", "@lydell/node-pty-win32-x64": "1.2.0-beta.12" } }, "sha512-qIK890UwPupoj07osVvgOIa++1mxeHbcGry4PKRHhNVNs81V2SCG34eJr46GybiOmBtc8Sj5PB1/GGM5PL549g=="], - "@lydell/node-pty-darwin-arm64": ["@lydell/node-pty-darwin-arm64@1.2.0-beta.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-C+eqDyRNHRYvx7RaHj6VVCx6nCpRBPuuxhTcc3JH3GuBMoxTsYeY4GkWH2XOktrgbAq1BG8e/Y8bu/wNQreCEw=="], + "@lydell/node-pty-darwin-arm64": ["@lydell/node-pty-darwin-arm64@1.2.0-beta.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tqaifcY9Cr41SblO1+FLzh8oxxtkNhuW9Dhl22lKme9BreYvKvxEZcdPIXTuqkJc5tagOEC4QHShKmJjLyLXLQ=="], - "@lydell/node-pty-darwin-x64": ["@lydell/node-pty-darwin-x64@1.2.0-beta.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-aZoIK6HtJO5BiT4ELm683U4dyHtt8b7wNgq3NJqYAQwSXrcPv576Z8vY3BIulVxfcFkht/SPLKou9TtdFXdNpg=="], + "@lydell/node-pty-darwin-x64": ["@lydell/node-pty-darwin-x64@1.2.0-beta.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-4LrS5pCJwqHKDVf1zS2gyNV0m4hKAXch+XZNhbZ6LY8uwVL8BhchzQBO40Os5anuRxRCWzHpw4Sp64Ie8q7E4Q=="], - "@lydell/node-pty-linux-arm64": ["@lydell/node-pty-linux-arm64@1.2.0-beta.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-0cKX2iMyXFNBE4fGtGK6B7IkdXcDMZajyEDoGMOgQQs/DDtoI5tSPcBcqNY9VitVrsRQA8+gFt6eKYU9Ye/lUA=="], + "@lydell/node-pty-linux-arm64": ["@lydell/node-pty-linux-arm64@1.2.0-beta.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-Sx+A71x5BDGHt9ansfrtGxwq2VFVDWvJUAdlUL0Hv0qeiJUfts+hgopx+CgT4PSwahKjdEgtu0+FAfY9rICKRw=="], - "@lydell/node-pty-linux-x64": ["@lydell/node-pty-linux-x64@1.2.0-beta.10", "", { "os": "linux", "cpu": "x64" }, "sha512-J9HnxvSzEeMH748+Ul1VrmCLWMo7iCVJy9EGijRR62+YO/Yk5GaCydUTZ+KzlH0/X5aTrgt5cfiof4vx45tRRg=="], + "@lydell/node-pty-linux-x64": ["@lydell/node-pty-linux-x64@1.2.0-beta.12", "", { "os": "linux", "cpu": "x64" }, "sha512-bJzs94njofYhGg/UDqW1nj0dtvvu+2OvxMY+RlLS1T17VgcktKoIR6PuenTwE5HJ/D6StCPADmXcT0nNsCKmIQ=="], - "@lydell/node-pty-win32-arm64": ["@lydell/node-pty-win32-arm64@1.2.0-beta.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-PlDJpJX/pnKyy6OmADKzhf+INZDDnzTBGaI0LT4laVNc6NblZNqUSkCMjLFWbeakeuQp0VG37M49WQSN9FDfeA=="], + "@lydell/node-pty-win32-arm64": ["@lydell/node-pty-win32-arm64@1.2.0-beta.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-p7POgjVEiFaBC3/y+AKuV1FzePCsJ6HmZDv2XK+jBZSfwP8+uBAw181ZiKYN1YuRa/XpmBGaWezcI8hZkbW++g=="], - "@lydell/node-pty-win32-x64": ["@lydell/node-pty-win32-x64@1.2.0-beta.10", "", { "os": "win32", "cpu": "x64" }, "sha512-ExFgWrzyldNAMi45U9PLIOu+g/RatP+f0c/dZxaooifME6yLW32BoHveH26/TtoAjZyJrc2iL0u48pgnR1fzmg=="], + "@lydell/node-pty-win32-x64": ["@lydell/node-pty-win32-x64@1.2.0-beta.12", "", { "os": "win32", "cpu": "x64" }, "sha512-IDFa00g7qUDGUYgByrUBJtC+mOjYVt/8KYyWivCg5JjGOHbBUACUQZLl0jTWmnr+tld/UyTpX90a2PY6oTVtRw=="], "@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@2.0.0", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg=="], diff --git a/package.json b/package.json index eb7ddba9a5..ae0e325b79 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020", "solid-js": "1.9.10", "vite-plugin-solid": "2.11.10", - "@lydell/node-pty": "1.2.0-beta.10" + "@lydell/node-pty": "1.2.0-beta.12" } }, "devDependencies": { diff --git a/packages/codeplane/package.json b/packages/codeplane/package.json index 23b7fb1308..dc996ce156 100644 --- a/packages/codeplane/package.json +++ b/packages/codeplane/package.json @@ -102,7 +102,7 @@ "@hono/node-ws": "1.3.0", "@hono/standard-validator": "0.1.5", "@hono/zod-validator": "0.4.2", - "@lydell/node-pty": "1.2.0-beta.10", + "@lydell/node-pty": "1.2.0-beta.12", "@modelcontextprotocol/sdk": "1.29.0", "@npmcli/arborist": "9.4.0", "@npmcli/config": "10.8.1", diff --git a/packages/codeplane/src/plugin/codex.ts b/packages/codeplane/src/plugin/codex.ts index 690ad2c847..e921b81bcb 100644 --- a/packages/codeplane/src/plugin/codex.ts +++ b/packages/codeplane/src/plugin/codex.ts @@ -527,7 +527,7 @@ export async function CodexAuthPlugin(input: PluginInput): Promise { ? requestInput : new URL(typeof requestInput === "string" ? requestInput : requestInput.url) const url = - parsed.pathname.includes("/v1/responses") || parsed.pathname.includes("/chat/completions") + parsed.pathname.endsWith("/responses") || parsed.pathname.includes("/chat/completions") ? new URL(CODEX_API_ENDPOINT) : parsed diff --git a/packages/codeplane/src/provider/provider.ts b/packages/codeplane/src/provider/provider.ts index cd18f8da24..b9740c7ee5 100644 --- a/packages/codeplane/src/provider/provider.ts +++ b/packages/codeplane/src/provider/provider.ts @@ -52,7 +52,7 @@ function wrapSSE(res: Response, ms: number, ctl: AbortController) { async pull(ctrl) { const part = await new Promise>>((resolve, reject) => { const id = setTimeout(() => { - const err = new Error("SSE read timed out") + const err = Object.assign(new Error("SSE read timed out"), { name: "ResponseStreamError" }) ctl.abort(err) void reader.cancel(err) reject(err) diff --git a/packages/codeplane/src/pty/pty.node.ts b/packages/codeplane/src/pty/pty.node.ts index b45c5bf509..76f415f4cd 100644 --- a/packages/codeplane/src/pty/pty.node.ts +++ b/packages/codeplane/src/pty/pty.node.ts @@ -1,4 +1,3 @@ -/** @ts-expect-error */ import * as pty from "@lydell/node-pty" import type { Opts, Proc } from "./pty" diff --git a/packages/codeplane/src/tui/component/prompt/index.tsx b/packages/codeplane/src/tui/component/prompt/index.tsx index d1f70ab76d..94230cf1ad 100644 --- a/packages/codeplane/src/tui/component/prompt/index.tsx +++ b/packages/codeplane/src/tui/component/prompt/index.tsx @@ -689,7 +689,16 @@ export function Prompt(props: PromptProps) { const nonTextParts = store.prompt.parts.filter((p) => p.type !== "text") const value = text - const content = await Editor.open({ value, renderer }) + const content = await Editor.open({ + value, + renderer, + cwd: + (project.instance.path().worktree === "/" + ? undefined + : project.instance.path().worktree) || + project.instance.directory() || + process.cwd(), + }) if (!content) return input.setText(content) diff --git a/packages/codeplane/src/tui/feature-plugins/system/session-v2.tsx b/packages/codeplane/src/tui/feature-plugins/system/session-v2.tsx index ea517255ad..2354edb7bd 100644 --- a/packages/codeplane/src/tui/feature-plugins/system/session-v2.tsx +++ b/packages/codeplane/src/tui/feature-plugins/system/session-v2.tsx @@ -317,7 +317,7 @@ function AssistantMessage(props: { - + @@ -369,9 +369,14 @@ function AssistantText(props: { part: SessionMessageAssistantText; syntax: Synta ) } -function AssistantReasoning(props: { part: SessionMessageAssistantReasoning; subtleSyntax: SyntaxStyle }) { +function AssistantReasoning(props: { part: SessionMessageAssistantReasoning; subtleSyntax: SyntaxStyle; done?: boolean }) { const { theme } = useTheme() const content = createMemo(() => props.part.text.replace("[REDACTED]", "").trim()) + const title = createMemo(() => { + if (!content()) return undefined + const line = content().split("\n")[0] + return line.length > 60 ? line.slice(0, 57) + "..." : line + }) return ( - + + + + Thinking{title() ? ": " + title() : ""} + + + + + + ) diff --git a/packages/codeplane/src/tui/routes/session/index.tsx b/packages/codeplane/src/tui/routes/session/index.tsx index 09dbba6815..828928d477 100644 --- a/packages/codeplane/src/tui/routes/session/index.tsx +++ b/packages/codeplane/src/tui/routes/session/index.tsx @@ -1219,9 +1219,14 @@ export function Session() { }, ) + const cwd = + (project.instance.path().worktree === "/" ? undefined : project.instance.path().worktree) || + project.instance.directory() || + process.cwd() + if (options.openWithoutSaving) { // Just open in editor without saving - await Editor.open({ value: transcript, renderer }) + await Editor.open({ value: transcript, renderer, cwd }) } else { const exportDir = process.cwd() const filename = options.filename.trim() @@ -1230,7 +1235,7 @@ export function Session() { await Filesystem.write(filepath, transcript) // Open with EDITOR if available - const result = await Editor.open({ value: transcript, renderer }) + const result = await Editor.open({ value: transcript, renderer, cwd }) if (result !== undefined) { await Filesystem.write(filepath, result) } @@ -1781,11 +1786,17 @@ const PART_MAPPING = { function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: AssistantMessage }) { const { theme, subtleSyntax } = useTheme() const ctx = use() + const done = createMemo(() => Boolean(props.message.finish)) const content = createMemo(() => { // Filter out redacted reasoning chunks from OpenRouter // OpenRouter sends encrypted reasoning data that appears as [REDACTED] return props.part.text.replace("[REDACTED]", "").trim() }) + const title = createMemo(() => { + if (!content()) return undefined + const line = content().split("\n")[0] + return line.length > 60 ? line.slice(0, 57) + "..." : line + }) return ( {/* Reasoning shares the same thin `│` rule and column grid as @@ -1804,12 +1815,21 @@ function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: Ass paddingLeft={2} paddingRight={2} > - + + + + Thinking{title() ? ": " + title() : ""} + + + + + + ) @@ -2009,6 +2029,7 @@ function InlineTool(props: { const local = useLocal() const renderer = useRenderer() const [hover, setHover] = createSignal(false) + const [errorExpanded, setErrorExpanded] = createSignal(false) const permission = createMemo(() => { const callID = sync.data.permission[ctx.sessionID]?.at(0)?.tool?.callID @@ -2028,13 +2049,6 @@ function InlineTool(props: { return agent ? local.agent.color(agent) : theme.textMuted }) - const fg = createMemo(() => { - if (permission()) return theme.warning - if (hover() && props.onClick) return theme.text - if (props.complete) return theme.textMuted - return theme.text - }) - const error = createMemo(() => (props.part.state.status === "error" ? props.part.state.error : undefined)) const denied = createMemo( @@ -2045,35 +2059,137 @@ function InlineTool(props: { error()?.includes("user dismissed"), ) + const failed = createMemo(() => Boolean(error() && !denied())) + const clickable = createMemo(() => Boolean(props.onClick || failed())) + + const fg = createMemo(() => { + if (permission()) return theme.warning + if (failed()) return theme.error + if (hover() && props.onClick) return theme.text + if (props.complete) return theme.textMuted + return theme.text + }) + return ( - props.onClick && setHover(true)} + + sync.data.message[ctx.sessionID]?.some((message) => message.role === "user" && message.id === id) ?? false + } + onMouseOver={() => clickable() && setHover(true)} onMouseOut={() => setHover(false)} onMouseUp={() => { if (renderer.getSelection()?.getSelectedText()) return + if (failed()) { + setErrorExpanded((value) => !value) + return + } props.onClick?.() }} + > + {props.children} + + ) +} + +const INLINE_TOOL_ICON_WIDTH = 2 + +function InlineToolRow(props: { + icon: string + iconColor?: RGBA + color?: RGBA + errorColor?: RGBA + failed?: boolean + denied?: boolean + error?: string + errorExpanded?: boolean + complete: any + pending: string + pendingColor?: RGBA + spinner?: boolean + children: JSX.Element + separateAfter?: (id: string | undefined) => boolean + onMouseOver?: () => void + onMouseOut?: () => void + onMouseUp?: () => void +}) { + const { theme } = useTheme() + const [margin, setMargin] = createSignal(0) + + return ( + - + } + fallback={ + + ~ {props.pending} + + } > - - {props.icon} {props.children} - + + + {props.icon} + + + {props.children} + + - - {error()} + + {props.error} ) @@ -2302,6 +2418,7 @@ function WebSearch(props: ToolProps) { function Task(props: ToolProps) { const { navigate } = useRoute() const sync = useSync() + const { theme } = useTheme() onMount(() => { if (props.metadata.sessionId && !sync.data.message[props.metadata.sessionId]?.length) @@ -2324,6 +2441,11 @@ function Task(props: ToolProps) { const isRunning = createMemo(() => props.part.state.status === "running") + const retry = createMemo(() => { + const status = sync.data.session_status[props.metadata.sessionId ?? ""] + return status?.type === "retry" ? status : undefined + }) + const duration = createMemo(() => { const first = messages().find((x) => x.role === "user")?.time.created const assistant = messages().findLast((x) => x.role === "assistant")?.time.completed @@ -2336,7 +2458,6 @@ function Task(props: ToolProps) { let content = [`${Locale.titlecase(props.input.subagent_type ?? "General")} Task — ${props.input.description}`] if (isRunning() && tools().length > 0) { - // content[0] += ` · ${tools().length} toolcalls` if (current()) { const state = current()!.state const title = state.status === "running" || state.status === "completed" ? state.title : undefined @@ -2348,9 +2469,16 @@ function Task(props: ToolProps) { content.push(`└ ${tools().length} toolcalls · ${Locale.duration(duration())}`) } + const r = retry() + if (r) { + content.push(`↳ ${r.message} [retrying attempt #${r.attempt + 1}]`) + } + return content.join("\n") }) + const color = createMemo(() => (retry() ? theme.error : undefined)) + return ( ) { complete={props.input.description} pending="Delegating..." part={props.part} + iconColor={color()} onClick={() => { if (props.metadata.sessionId) { navigate({ type: "session", sessionID: props.metadata.sessionId }) diff --git a/packages/codeplane/src/tui/util/editor.ts b/packages/codeplane/src/tui/util/editor.ts index 6b005615bd..c9450ca8d9 100644 --- a/packages/codeplane/src/tui/util/editor.ts +++ b/packages/codeplane/src/tui/util/editor.ts @@ -6,11 +6,12 @@ import { CliRenderer } from "@opentui/core" import { Filesystem } from "@/tui/_compat/filesystem" import { Process } from "@/tui/_compat/process" -export async function open(opts: { value: string; renderer: CliRenderer }): Promise { +export async function open(opts: { value: string; renderer: CliRenderer; cwd?: string }): Promise { const editor = process.env["VISUAL"] || process.env["EDITOR"] if (!editor) return - const filepath = join(tmpdir(), `${Date.now()}.md`) + const tmpdirPath = opts.cwd ?? tmpdir() + const filepath = join(tmpdirPath, `${Date.now()}.md`) await using _ = defer(async () => rm(filepath, { force: true })) await Filesystem.write(filepath, opts.value) diff --git a/packages/sdk/js/src/gen/core/serverSentEvents.gen.ts b/packages/sdk/js/src/gen/core/serverSentEvents.gen.ts index a00e1bf60a..056a812593 100644 --- a/packages/sdk/js/src/gen/core/serverSentEvents.gen.ts +++ b/packages/sdk/js/src/gen/core/serverSentEvents.gen.ts @@ -150,8 +150,9 @@ export const createSseClient = ({ while (true) { const { done, value } = await reader.read() if (done) break - const normalized = value.replace(/\r\n/g, "\n").replace(/\r/g, "\n") - buffer += normalized + buffer += value + // Normalize line endings: CRLF -> LF, then CR -> LF + buffer = buffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n") const chunks = buffer.split("\n\n") buffer = chunks.pop() ?? "" @@ -199,13 +200,14 @@ export const createSseClient = ({ } } + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }) + if (dataLines.length) { - onSseEvent?.({ - data, - event: eventName, - id: lastEventId, - retry: retryDelay, - }) yield data as any } } @@ -224,10 +226,9 @@ export const createSseClient = ({ break // stop after firing error } - // exponential backoff with jitter: double retry each attempt, cap at 30s + // exponential backoff: double retry each attempt, cap at 30s const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000) - const jittered = backoff * (0.5 + Math.random() * 0.5) - await sleep(jittered) + await sleep(backoff) } } } diff --git a/packages/sdk/js/src/gen/sdk.gen.ts b/packages/sdk/js/src/gen/sdk.gen.ts index fd9ba783fc..baac6ab556 100644 --- a/packages/sdk/js/src/gen/sdk.gen.ts +++ b/packages/sdk/js/src/gen/sdk.gen.ts @@ -8,7 +8,7 @@ import type { AppLogErrors, AppLogResponses, AppSkillsResponses, - Auth as Auth3, + Auth as Auth4, AuthRemoveErrors, AuthRemoveResponses, AuthSetErrors, @@ -64,6 +64,7 @@ import type { FindSymbolsResponses, FindTextResponses, FormatterStatusResponses, + GlobalAuthVerifyResponses, GlobalBashInteractiveKillErrors, GlobalBashInteractiveKillResponses, GlobalConfigGetResponses, @@ -563,6 +564,32 @@ export class Cron extends HeyApiClient { } } +export class Auth extends HeyApiClient { + /** + * Verify second factor + * + * Exchange a valid Basic Auth password plus a TOTP code for a short-lived second-factor session token. Returns 401 if the password is wrong, 400 if TOTP is not enabled, and 401 with { totp: true } if the code is invalid. + */ + public verify( + parameters?: { + code?: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "body", key: "code" }] }]) + return (options?.client ?? this.client).post({ + url: "/global/auth/verify", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + export class Config extends HeyApiClient { /** * Get global configuration @@ -730,6 +757,11 @@ export class Global extends HeyApiClient { }) } + private _auth?: Auth + get auth(): Auth { + return (this._auth ??= new Auth({ client: this.client })) + } + private _config?: Config get config(): Config { return (this._config ??= new Config({ client: this.client })) @@ -741,7 +773,7 @@ export class Global extends HeyApiClient { } } -export class Auth extends HeyApiClient { +export class Auth2 extends HeyApiClient { /** * Remove auth credentials * @@ -769,7 +801,7 @@ export class Auth extends HeyApiClient { public set( parameters: { providerID: string - auth?: Auth3 + auth?: Auth4 }, options?: Options, ) { @@ -2207,6 +2239,9 @@ export class Session2 extends HeyApiClient { permission?: PermissionRuleset workspaceID?: string cronRunID?: string + metadata?: { + [key: string]: unknown + } }, options?: Options, ) { @@ -2222,6 +2257,7 @@ export class Session2 extends HeyApiClient { { in: "body", key: "permission" }, { in: "body", key: "workspaceID" }, { in: "body", key: "cronRunID" }, + { in: "body", key: "metadata" }, ], }, ], @@ -2344,6 +2380,9 @@ export class Session2 extends HeyApiClient { workspace?: string title?: string permission?: PermissionRuleset + metadata?: { + [key: string]: unknown + } time?: { archived?: number | null } @@ -2360,6 +2399,7 @@ export class Session2 extends HeyApiClient { { in: "query", key: "workspace" }, { in: "body", key: "title" }, { in: "body", key: "permission" }, + { in: "body", key: "metadata" }, { in: "body", key: "time" }, ], }, @@ -3978,7 +4018,7 @@ export class Event extends HeyApiClient { } } -export class Auth2 extends HeyApiClient { +export class Auth3 extends HeyApiClient { /** * Auto-connect pending MCP OAuth * @@ -4345,9 +4385,9 @@ export class Mcp extends HeyApiClient { }) } - private _auth?: Auth2 - get auth(): Auth2 { - return (this._auth ??= new Auth2({ client: this.client })) + private _auth?: Auth3 + get auth(): Auth3 { + return (this._auth ??= new Auth3({ client: this.client })) } } @@ -4593,9 +4633,9 @@ export class CodeplaneClient extends HeyApiClient { return (this._global ??= new Global({ client: this.client })) } - private _auth?: Auth - get auth(): Auth { - return (this._auth ??= new Auth({ client: this.client })) + private _auth?: Auth2 + get auth(): Auth2 { + return (this._auth ??= new Auth2({ client: this.client })) } private _app?: App diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 71dd38235a..c0f7e2d727 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -1131,6 +1131,9 @@ export type Session = { diff?: string } cronRunID?: string + metadata?: { + [key: string]: unknown + } } export type EventSessionCreated = { @@ -1519,6 +1522,9 @@ export type SyncEventSessionUpdated = { diff?: string } | null cronRunID?: string | null + metadata?: { + [key: string]: unknown + } | null } } } @@ -2757,6 +2763,9 @@ export type GlobalSession = { diff?: string } cronRunID?: string + metadata?: { + [key: string]: unknown + } project: ProjectSummary | null } @@ -3403,6 +3412,27 @@ export type GlobalHealthResponses = { export type GlobalHealthResponse = GlobalHealthResponses[keyof GlobalHealthResponses] +export type GlobalAuthVerifyData = { + body?: { + code: string + } + path?: never + query?: never + url: "/global/auth/verify" +} + +export type GlobalAuthVerifyResponses = { + /** + * Second-factor session token + */ + 200: { + token: string + expiresAt: number + } +} + +export type GlobalAuthVerifyResponse = GlobalAuthVerifyResponses[keyof GlobalAuthVerifyResponses] + export type GlobalVersionData = { body?: never path?: never @@ -4640,6 +4670,9 @@ export type SessionCreateData = { permission?: PermissionRuleset workspaceID?: string cronRunID?: string + metadata?: { + [key: string]: unknown + } } path?: never query?: { @@ -4769,6 +4802,9 @@ export type SessionUpdateData = { body?: { title?: string permission?: PermissionRuleset + metadata?: { + [key: string]: unknown + } time?: { archived?: number | null } diff --git a/packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts b/packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts index 35761ee563..056a812593 100644 --- a/packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts +++ b/packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts @@ -148,10 +148,11 @@ export const createSseClient = ({ try { while (true) { - let { done, value } = await reader.read() + const { done, value } = await reader.read() if (done) break - value = (value || "").replace(/\r\n/g, "\n").replace(/\r/g, "\n") buffer += value + // Normalize line endings: CRLF -> LF, then CR -> LF + buffer = buffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n") const chunks = buffer.split("\n\n") buffer = chunks.pop() ?? "" @@ -199,13 +200,14 @@ export const createSseClient = ({ } } + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }) + if (dataLines.length) { - onSseEvent?.({ - data, - event: eventName, - id: lastEventId, - retry: retryDelay, - }) yield data as any } } @@ -224,10 +226,9 @@ export const createSseClient = ({ break // stop after firing error } - // exponential backoff with jitter: double retry each attempt, cap at 30s + // exponential backoff: double retry each attempt, cap at 30s const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000) - const jittered = backoff * (0.5 + Math.random() * 0.5) - await sleep(jittered) + await sleep(backoff) } } } diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index fd9ba783fc..baac6ab556 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -8,7 +8,7 @@ import type { AppLogErrors, AppLogResponses, AppSkillsResponses, - Auth as Auth3, + Auth as Auth4, AuthRemoveErrors, AuthRemoveResponses, AuthSetErrors, @@ -64,6 +64,7 @@ import type { FindSymbolsResponses, FindTextResponses, FormatterStatusResponses, + GlobalAuthVerifyResponses, GlobalBashInteractiveKillErrors, GlobalBashInteractiveKillResponses, GlobalConfigGetResponses, @@ -563,6 +564,32 @@ export class Cron extends HeyApiClient { } } +export class Auth extends HeyApiClient { + /** + * Verify second factor + * + * Exchange a valid Basic Auth password plus a TOTP code for a short-lived second-factor session token. Returns 401 if the password is wrong, 400 if TOTP is not enabled, and 401 with { totp: true } if the code is invalid. + */ + public verify( + parameters?: { + code?: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "body", key: "code" }] }]) + return (options?.client ?? this.client).post({ + url: "/global/auth/verify", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + export class Config extends HeyApiClient { /** * Get global configuration @@ -730,6 +757,11 @@ export class Global extends HeyApiClient { }) } + private _auth?: Auth + get auth(): Auth { + return (this._auth ??= new Auth({ client: this.client })) + } + private _config?: Config get config(): Config { return (this._config ??= new Config({ client: this.client })) @@ -741,7 +773,7 @@ export class Global extends HeyApiClient { } } -export class Auth extends HeyApiClient { +export class Auth2 extends HeyApiClient { /** * Remove auth credentials * @@ -769,7 +801,7 @@ export class Auth extends HeyApiClient { public set( parameters: { providerID: string - auth?: Auth3 + auth?: Auth4 }, options?: Options, ) { @@ -2207,6 +2239,9 @@ export class Session2 extends HeyApiClient { permission?: PermissionRuleset workspaceID?: string cronRunID?: string + metadata?: { + [key: string]: unknown + } }, options?: Options, ) { @@ -2222,6 +2257,7 @@ export class Session2 extends HeyApiClient { { in: "body", key: "permission" }, { in: "body", key: "workspaceID" }, { in: "body", key: "cronRunID" }, + { in: "body", key: "metadata" }, ], }, ], @@ -2344,6 +2380,9 @@ export class Session2 extends HeyApiClient { workspace?: string title?: string permission?: PermissionRuleset + metadata?: { + [key: string]: unknown + } time?: { archived?: number | null } @@ -2360,6 +2399,7 @@ export class Session2 extends HeyApiClient { { in: "query", key: "workspace" }, { in: "body", key: "title" }, { in: "body", key: "permission" }, + { in: "body", key: "metadata" }, { in: "body", key: "time" }, ], }, @@ -3978,7 +4018,7 @@ export class Event extends HeyApiClient { } } -export class Auth2 extends HeyApiClient { +export class Auth3 extends HeyApiClient { /** * Auto-connect pending MCP OAuth * @@ -4345,9 +4385,9 @@ export class Mcp extends HeyApiClient { }) } - private _auth?: Auth2 - get auth(): Auth2 { - return (this._auth ??= new Auth2({ client: this.client })) + private _auth?: Auth3 + get auth(): Auth3 { + return (this._auth ??= new Auth3({ client: this.client })) } } @@ -4593,9 +4633,9 @@ export class CodeplaneClient extends HeyApiClient { return (this._global ??= new Global({ client: this.client })) } - private _auth?: Auth - get auth(): Auth { - return (this._auth ??= new Auth({ client: this.client })) + private _auth?: Auth2 + get auth(): Auth2 { + return (this._auth ??= new Auth2({ client: this.client })) } private _app?: App diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 83fb06816e..c0f7e2d727 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1131,7 +1131,9 @@ export type Session = { diff?: string } cronRunID?: string - metadata?: Record + metadata?: { + [key: string]: unknown + } } export type EventSessionCreated = { @@ -1520,6 +1522,9 @@ export type SyncEventSessionUpdated = { diff?: string } | null cronRunID?: string | null + metadata?: { + [key: string]: unknown + } | null } } } @@ -2758,6 +2763,9 @@ export type GlobalSession = { diff?: string } cronRunID?: string + metadata?: { + [key: string]: unknown + } project: ProjectSummary | null } @@ -3404,6 +3412,27 @@ export type GlobalHealthResponses = { export type GlobalHealthResponse = GlobalHealthResponses[keyof GlobalHealthResponses] +export type GlobalAuthVerifyData = { + body?: { + code: string + } + path?: never + query?: never + url: "/global/auth/verify" +} + +export type GlobalAuthVerifyResponses = { + /** + * Second-factor session token + */ + 200: { + token: string + expiresAt: number + } +} + +export type GlobalAuthVerifyResponse = GlobalAuthVerifyResponses[keyof GlobalAuthVerifyResponses] + export type GlobalVersionData = { body?: never path?: never @@ -4641,7 +4670,9 @@ export type SessionCreateData = { permission?: PermissionRuleset workspaceID?: string cronRunID?: string - metadata?: Record + metadata?: { + [key: string]: unknown + } } path?: never query?: { @@ -4771,7 +4802,9 @@ export type SessionUpdateData = { body?: { title?: string permission?: PermissionRuleset - metadata?: Record + metadata?: { + [key: string]: unknown + } time?: { archived?: number | null } diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index ae32bbbe19..8e778e7cd7 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -3,7 +3,7 @@ "info": { "title": "codeplane", "description": "codeplane api", - "version": "31.1.0" + "version": "31.3.4" }, "paths": { "/global/cron": { @@ -678,6 +678,57 @@ ] } }, + "/global/auth/verify": { + "post": { + "operationId": "global.auth.verify", + "summary": "Verify second factor", + "description": "Exchange a valid Basic Auth password plus a TOTP code for a short-lived second-factor session token. Returns 401 if the password is wrong, 400 if TOTP is not enabled, and 401 with { totp: true } if the code is invalid.", + "responses": { + "200": { + "description": "Second-factor session token", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "token": { + "type": "string" + }, + "expiresAt": { + "type": "number" + } + }, + "required": ["token", "expiresAt"] + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "code": { + "type": "string", + "minLength": 1, + "maxLength": 16 + } + }, + "required": ["code"] + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createCodeplaneClient } from \"@codeplane-ai/sdk\"\n\nconst client = createCodeplaneClient()\nawait client.global.auth.verify({\n ...\n})" + } + ] + } + }, "/global/version": { "get": { "operationId": "global.version", @@ -3329,6 +3380,13 @@ "cronRunID": { "type": "string", "pattern": "^crun.*" + }, + "metadata": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} } } } @@ -3611,6 +3669,13 @@ "permission": { "$ref": "#/components/schemas/PermissionRuleset" }, + "metadata": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, "time": { "type": "object", "properties": { @@ -11555,6 +11620,13 @@ "cronRunID": { "type": "string", "pattern": "^crun.*" + }, + "metadata": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} } }, "required": ["id", "slug", "projectID", "directory", "title", "version", "time"] @@ -12814,6 +12886,20 @@ "type": "null" } ] + }, + "metadata": { + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ] } } } @@ -15872,6 +15958,13 @@ "type": "string", "pattern": "^crun.*" }, + "metadata": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, "project": { "anyOf": [ { From 66231e80b6daf26db63013aa09bc2d16c914eec5 Mon Sep 17 00:00:00 2001 From: Codeplane Agent Date: Mon, 1 Jun 2026 07:17:38 +0000 Subject: [PATCH 2/2] fix: resolve 6 user-impacting bugs across desktop, web, TUI, and build - desktop: remove preventDefault/callback on HTTP basic auth so Electron surfaces the native credential dialog (auth_basic connections were permanently broken) - app: guard event-reducer against undefined event.properties to prevent web UI freeze on malformed SSE events - tui: normalize error objects safely in v2 session plugin InlineTool/BlockTool to prevent TextNodeRenderable crashes when error.message is missing or non-string - tui: add optional chaining on ToolPart.state in v1 session route to prevent crashes on malformed tool parts - app: add keyCode === 229 IME guard in server URL dialog to prevent premature CJK character commit - build: replace Unix-only rm/mkdir shell commands with cross-platform fs.rmSync/fs.mkdirSync so local builds work on Windows Co-Authored-By: Claude Opus 4.7 (1M context) Co-Authored-By: codeplane-agent[bot] <287208015+codeplane-agent[bot]@users.noreply.github.com> --- .../app/src/components/dialog-select-server.tsx | 2 +- .../app/src/context/global-sync/event-reducer.ts | 3 ++- packages/codeplane/script/build.ts | 4 ++-- .../tui/feature-plugins/system/session-v2.tsx | 16 ++++++++++++++-- .../codeplane/src/tui/routes/session/index.tsx | 8 ++++---- packages/desktop/src/main/main.ts | 2 -- 6 files changed, 23 insertions(+), 12 deletions(-) diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx index 95498e744c..5d614a1751 100644 --- a/packages/app/src/components/dialog-select-server.tsx +++ b/packages/app/src/components/dialog-select-server.tsx @@ -115,7 +115,7 @@ function ServerForm(props: ServerFormProps) { props.onBack() return } - if (event.key !== "Enter" || event.isComposing) return + if (event.key !== "Enter" || event.isComposing || event.keyCode === 229) return event.preventDefault() props.onSubmit() } diff --git a/packages/app/src/context/global-sync/event-reducer.ts b/packages/app/src/context/global-sync/event-reducer.ts index 512f1e8f7b..807a40cd7c 100644 --- a/packages/app/src/context/global-sync/event-reducer.ts +++ b/packages/app/src/context/global-sync/event-reducer.ts @@ -413,7 +413,8 @@ export function applyDirectoryEvent(input: { break } case "message.part.updated": { - const part = (event.properties as { part?: Part }).part + const props = event.properties as { part?: Part } | undefined + const part = props?.part if (!part) break if (SKIP_PARTS.has(part.type)) break // Drain any buffered deltas that arrived before this part existed. diff --git a/packages/codeplane/script/build.ts b/packages/codeplane/script/build.ts index c898f568cd..f814ab4835 100644 --- a/packages/codeplane/script/build.ts +++ b/packages/codeplane/script/build.ts @@ -300,7 +300,7 @@ const targets = singleFlag const binaries: Record = {} try { - await $`rm -rf dist` + fs.rmSync(path.join(dir, "dist"), { recursive: true, force: true }) if (!skipInstall) { await $`bun install --no-save --os="*" --cpu="*" @parcel/watcher@${pkg.dependencies["@parcel/watcher"]}` } @@ -316,7 +316,7 @@ try { .filter(Boolean) .join("-") console.log(`building ${name}`) - await $`mkdir -p dist/${name}/bin` + fs.mkdirSync(path.join(dir, "dist", name, "bin"), { recursive: true }) const mainResult = await Bun.build({ // The bundler classifies the build by `target`. Without this, it defaults diff --git a/packages/codeplane/src/tui/feature-plugins/system/session-v2.tsx b/packages/codeplane/src/tui/feature-plugins/system/session-v2.tsx index 2354edb7bd..fc6f9e7c79 100644 --- a/packages/codeplane/src/tui/feature-plugins/system/session-v2.tsx +++ b/packages/codeplane/src/tui/feature-plugins/system/session-v2.tsx @@ -537,7 +537,13 @@ function InlineTool(props: { agentColor?: RGBA }) { const { theme } = useTheme() - const error = createMemo(() => (props.part.state.status === "error" ? props.part.state.error.message : undefined)) + const error = createMemo(() => { + if (props.part.state?.status !== "error") return undefined + const err = props.part.state.error + if (!err) return "unknown error" + const msg = typeof err === "object" && err !== null && "message" in err ? (err as { message: unknown }).message : err + return typeof msg === "string" ? msg : String(msg) + }) const denied = createMemo(() => { const message = error() if (!message) return false @@ -583,7 +589,13 @@ function BlockTool(props: { const { theme } = useTheme() const renderer = useRenderer() const [hover, setHover] = createSignal(false) - const error = createMemo(() => (props.part?.state.status === "error" ? props.part.state.error.message : undefined)) + const error = createMemo(() => { + if (props.part?.state?.status !== "error") return undefined + const err = props.part.state.error + if (!err) return "unknown error" + const msg = typeof err === "object" && err !== null && "message" in err ? (err as { message: unknown }).message : err + return typeof msg === "string" ? msg : String(msg) + }) return ( { if (ctx.showDetails()) return false - if (props.part.state.status !== "completed") return false + if (props.part.state?.status !== "completed") return false return true }) const toolprops = { get metadata() { - return props.part.state.status === "pending" ? {} : (props.part.state.metadata ?? {}) + return props.part.state?.status === "pending" ? {} : (props.part.state?.metadata ?? {}) }, get input() { - return props.part.state.input ?? {} + return props.part.state?.input ?? {} }, get output() { - return props.part.state.status === "completed" ? props.part.state.output : undefined + return props.part.state?.status === "completed" ? props.part.state?.output : undefined }, get permission() { const permissions = sync.data.permission[props.message.sessionID] ?? [] diff --git a/packages/desktop/src/main/main.ts b/packages/desktop/src/main/main.ts index 52b0b2892e..fd3a30706e 100644 --- a/packages/desktop/src/main/main.ts +++ b/packages/desktop/src/main/main.ts @@ -3002,8 +3002,6 @@ if (!gotLock) { // HTTP Basic Auth challenges. Do NOT call preventDefault or pass empty // credentials — doing so suppresses the OS prompt and sends a blank // Authorization header, permanently breaking auth_basic connections. - event.preventDefault() - callback() }) app