diff --git a/packages/app-bundle/manifest.json b/packages/app-bundle/manifest.json index 9bf0ed071..0a0e9fd59 100644 --- a/packages/app-bundle/manifest.json +++ b/packages/app-bundle/manifest.json @@ -196,8 +196,8 @@ "packages/app/src/context/platform.tsx": "edd9b1773feb5c084b922a57b6914f6a473eed558b44f9de0eeb1c4d4e2b49da", "packages/app/src/context/question-inbox.test.ts": "d4e2c666194fe6463d1a1da1901f2aef3b6a17eec00394a2b020d7ce698d4c92", "packages/app/src/context/question-inbox.ts": "f42a2490670e44578f88d626bc0db0a0205fa2b5ec7cd719ba0544915f3a6179", - "packages/app/src/context/server-sdk.test.ts": "aa3815ee62cb933fd30895be924ed2074aef7fb90bcbf6cb39ad1dce687e04d8", - "packages/app/src/context/server-sdk.tsx": "5056b168d2ba0445beca4c6d07f2e43b6f46b4aefd5802b16d530ce544f26569", + "packages/app/src/context/server-sdk.test.ts": "c8c4a04846c2e5c0e3e91596f104f76994c92b116a755908fcbce2655a8a0234", + "packages/app/src/context/server-sdk.tsx": "9dc721c54074a323fb879aedf76cf29c1888088ab6b9b0c68aae683ca4f563f1", "packages/app/src/context/server-session-child-active.test.ts": "de8c467f67f88b6cce351c1805ec90728cf238eb1ef32f39ffdbbf3de4dcec22", "packages/app/src/context/server-session-orphan-part.test.ts": "1d635374c0bfff25a01825061aabac7334e5cdaedca2dab9f3d9467ff543caea", "packages/app/src/context/server-session.test.ts": "31b14c85a843eb172899f456566ad7ab7d07a22eaf8b31d29a22e255e95cb96b", @@ -503,7 +503,7 @@ "packages/session-ui/src/components/message-part-user.ts": "1f2c8cb4efff5274863a133f369a7397cac804678e953740f356ae03b71d475c", "packages/session-ui/src/components/message-part.css": "ae592deb33739738f6a7fc6a3460b7146803bdd05cbbf46f1cfd2c5cec421392", "packages/session-ui/src/components/message-part.test.ts": "7c66430837fa3d2607657e57e35720ae68dc5e84662e12826713a938301eeb7b", - "packages/session-ui/src/components/message-part.tsx": "dda5104168d8b841c5f96bb27737d49375b19f68e64f1f6a7c9e7776ae332164", + "packages/session-ui/src/components/message-part.tsx": "298ed64a50b0417c45b81d05f7ba32d9f8fdf3ac8a24ea7806a89964e1f684b5", "packages/session-ui/src/components/session-retry.tsx": "2cb1513d73275a269894f601e8c1d29bc790b14927134fe36177f2b371888721", "packages/session-ui/src/components/session-review.css": "ec8c5b359dd4b7ff80d7fad8906359678adade9ce62c9d74aedc000b646db341", "packages/session-ui/src/components/session-review.test.ts": "41515bba238b4e60ce3d80e206013e9b43e3b0d0a51be45796028dbae47c99ae", @@ -1011,7 +1011,7 @@ "packages/ui/src/v2/components/text-shimmer-v2.tsx" ], "exceptions": [], - "extracted_at": "2026-10-22T12:38:18.375Z", + "extracted_at": "2026-10-22T13:11:47.632Z", "per_package": { "packages/app": { "A": 254, diff --git a/packages/app-bundle/overlay/packages/app/src/context/server-sdk.test.ts b/packages/app-bundle/overlay/packages/app/src/context/server-sdk.test.ts index 6e88fdc6d..fcdd681d2 100644 --- a/packages/app-bundle/overlay/packages/app/src/context/server-sdk.test.ts +++ b/packages/app-bundle/overlay/packages/app/src/context/server-sdk.test.ts @@ -16,6 +16,7 @@ import { globalStreamResetRequired, postEventStreamUrlAssert, resumeStreamAfterPageShow, + parseSseFrame, } from "./server-sdk" import { armRemoteCreate, attachOwnerHeaderIfArmed, disarmRemoteCreate } from "@/components/remote-create-arm" import type { OpenCodeEvent } from "@opencode-ai/client/promise" @@ -724,3 +725,27 @@ describe("composeArmedRequestHeaders (remote-create Authorization preservation)" }) }) + +// #1843 — the webview fetch path's SDK reader yields raw SSE `data:` lines as +// strings (the platform path yields parsed objects); a string frame hit +// `"payload" in event` as a TypeError and killed the whole global event stream +// loop, every frame (the live recurring "[global-sdk] event stream failed"). +describe("parseSseFrame (#1843)", () => { + test("a JSON object line parses to the record", () => { + expect(parseSseFrame('{"directory":"/home/aaron/x","payload":{"type":"message.updated"}}')).toEqual({ + directory: "/home/aaron/x", + payload: { type: "message.updated" }, + }) + }) + + test("malformed lines and non-objects skip (undefined), never throw", () => { + expect(parseSseFrame("not json")).toBeUndefined() + expect(parseSseFrame("[1,2]")).toBeUndefined() + expect(parseSseFrame("null")).toBeUndefined() + expect(parseSseFrame("42")).toBeUndefined() + }) + + test("empty string skips", () => { + expect(parseSseFrame("")).toBeUndefined() + }) +}) diff --git a/packages/app-bundle/overlay/packages/app/src/context/server-sdk.tsx b/packages/app-bundle/overlay/packages/app/src/context/server-sdk.tsx index 84ad9be08..0dcdd0d28 100644 --- a/packages/app-bundle/overlay/packages/app/src/context/server-sdk.tsx +++ b/packages/app-bundle/overlay/packages/app/src/context/server-sdk.tsx @@ -48,6 +48,28 @@ type CurrentDelta = Extract< // The named V2 types are adapted only in their V2 shape (payload in `data`); // a V1-shaped frame of the same name falls through to the default case raw. +export function parseSseFrame(raw: string): Record | undefined { + try { + const value: unknown = JSON.parse(raw) + if (value !== null && typeof value === "object" && !Array.isArray(value)) return value as Record + return undefined + } catch { + return undefined + } +} + +/** #1843: the legacy fan-in wrapper — {directory, payload} frames (the + * frontdoor's merged-group wire shape, and what parsed string frames are). */ +function isLegacyWireFrame(value: unknown): value is { directory?: string; payload: Record } { + return !!value && typeof value === "object" && "payload" in value && typeof value.payload === "object" +} + +/** #1843: either wire vocabulary (V1 `properties` frames or V2 `data` frames + * — the WireEvent union). Anything else is an unknown shape, skipped. */ +function isWireEvent(value: unknown): value is WireEvent { + return !!value && typeof value === "object" && "type" in value +} + export function adaptServerEvent(event: WireEvent): ServerEvent { if (event.type === "permission.v2.asked" && "data" in event) { return { @@ -709,9 +731,20 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS // again, so force the resync now (the refetch cannot miss the events // between the end and this re-subscription). if (streamEndResync.frame()) setResyncCount((n) => n + 1) - const legacy = "payload" in event - const directory = legacy ? (event.directory ?? "global") : (event.location?.directory ?? "global") - const payload = legacy ? (event.payload as Event) : adaptServerEvent(event) + // #1843: the webview fetch path's SDK reader yields raw SSE `data:` + // lines as STRINGS (the platform path yields parsed objects) — a + // string frame hit `"payload" in event` as a TypeError and killed + // the whole stream loop, every frame, all day (the recurring + // "[global-sdk] event stream failed"). Parse strings before the + // discrimination; a malformed line skips (named, never kills). + const parsed = typeof event === "string" ? parseSseFrame(event) : event + if (parsed === undefined) continue + const legacy = isLegacyWireFrame(parsed) + if (!legacy && !isWireEvent(parsed)) continue + const directory = legacy + ? parsed.directory ?? "global" + : (parsed as { location?: { directory?: string } }).location?.directory ?? "global" + const payload = legacy ? (parsed.payload as unknown as Event) : adaptServerEvent(parsed) // #1522 (ADR 0033 decision A): the SSE fan-in aggregator emits // `amicode.fleet.focus` as the first frame on (re)connect. Intercept // it here and dispatch to the existing picker focus latch via @@ -764,13 +797,13 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS // replayed frame already applied is dropped, never applied twice. const verdict = cursor.frame({ id: kind === "v1" ? frameSseId : undefined, - type: legacy ? event.payload.type : payload.type, + type: payload.type, }) if (verdict.resync) forceResync(verdict.resync.namespaces) if (!verdict.apply) continue persistCursor() // a sync copy still moves the cursor (it holds a seq), but is no event - if (legacy && event.payload.type === "sync") continue + if (legacy && parsed.payload.type === "sync") continue // #1539: the origin is the composite cursor's moving namespace. // Fleet-of-one (a bare engine id) is always local. const taggedPayload: ServerEvent = verdict.origin !== undefined diff --git a/packages/app-bundle/overlay/packages/session-ui/src/components/message-part.tsx b/packages/app-bundle/overlay/packages/session-ui/src/components/message-part.tsx index bd40a630d..d02d23dbc 100644 --- a/packages/app-bundle/overlay/packages/session-ui/src/components/message-part.tsx +++ b/packages/app-bundle/overlay/packages/session-ui/src/components/message-part.tsx @@ -2713,7 +2713,23 @@ ToolRegistry.register({ defer={props.deferContent !== false} trigger={
-
+
{ + // #1843: the whole title area opens the file, not just the + // filename glyph — the live report clicked the row (the + // directory span sits between the filename and the edge) and + // got nothing. Only the button behavior changes: prevent + + // stopPropagation so the tool body's expand/collapse doesn't + // fire alongside, exactly like the filename button. + if (!path() || pending()) return + event.preventDefault() + event.stopPropagation() + openFileInEditor(path()) + }} + >
{i18n.t("ui.messagePart.title.edit")}