Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions packages/app-bundle/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> | undefined {
try {
const value: unknown = JSON.parse(raw)
if (value !== null && typeof value === "object" && !Array.isArray(value)) return value as Record<string, unknown>
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<string, unknown> } {
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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2713,7 +2713,23 @@ ToolRegistry.register({
defer={props.deferContent !== false}
trigger={
<div data-component="edit-trigger">
<div data-slot="message-part-title-area">
<div
data-slot="message-part-title-area"
data-clickable={path() ? "true" : undefined}
title={path()}
onClick={(event) => {
// #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())
}}
>
<div data-slot="message-part-title">
<span data-slot="message-part-title-text">
<span data-pending={pending() ? "true" : "false"}>{i18n.t("ui.messagePart.title.edit")}</span>
Expand Down
Loading