Skip to content

web: side-panel "Open file" is a silent no-op on insecure HTTP origins (v2.2.0 regression) #59791

Description

@expnn

Description

On the Web UI served over plain HTTP from a non-localhost origin (LAN IP / hostname / Tailscale), clicking Open file in the side panel's new-tab view does nothing: no tab opens, no toast, no request. The console shows:

Uncaught TypeError: crypto.randomUUID is not a function

Review and Terminal launchers in the same panel work. The same server and session work over http://127.0.0.1:<port>.

Root cause: packages/gui-plugins/src/file/renderer.tsx opens a new launcher tab with an unguarded crypto.randomUUID():

https://github.com/anomalyco/opencode/blob/v2.2.0/packages/gui-plugins/src/file/renderer.tsx#L170

if (selected || layout.narrow())
  layout.open(`file:${selected ?? OPEN}`, session, { tab: "preview", background: options?.background })
else layout.open(`file:${OPEN}:${crypto.randomUUID()}`, session, { tab: "select" })

crypto.randomUUID exists only in secure contexts, so on an insecure origin the call throws inside the click handler before layout.open, and the click is silently lost. Both the + → “Open file” launcher and Ctrl+Alt+P reach this branch; narrow layouts (<768px) take the other branch and are unaffected.

Steps to reproduce

  1. Start the Web UI (opencode serve, or the background service) and open it from a non-localhost HTTP origin, e.g. http://<LAN-IP>:<port>/ (Chrome shows “Not secure”).
  2. Open any session and open the side panel (Toggle review).
  3. Click + (Add tab), then Open file.
  4. Nothing happens; devtools shows TypeError: crypto.randomUUID is not a function.

Expected: the file browser tab opens on any origin (id generation should not require a secure context).

Regression / history

  • v2.1.4 used a fixed key (file:open) in openPicker and worked on insecure origins.
  • #57182 (879479b467, “allow multiple open file tabs”) introduced file:${OPEN}:${crypto.randomUUID()}; #55246 moved it to packages/gui-plugins/src/file/renderer.tsx. First release containing it: v2.2.0.
  • The app already has a guarded helper for exactly this: uuid() in packages/app/src/runtime/persistence/uuid.ts (falls back when crypto.randomUUID is unavailable, when isSecureContext is false, or when it throws), with unit tests. Earlier fixes #51783 and #53501 were the same class of bug.
  • Related open issues: #46168, #48144, #55777 (other unguarded crypto.randomUUID calls that silently fail on insecure origins).

Suggested fix

Use the guarded helper here — e.g. promote uuid() to @opencode/util and import it from both the app and packages/gui-plugins, or add a local fallback before layout.open:

const uuid = () =>
  typeof globalThis.crypto?.randomUUID === "function" && globalThis.isSecureContext !== false
    ? globalThis.crypto.randomUUID()
    : Math.random().toString(16).slice(2)

Worth auditing the other direct calls reachable from the web UI: packages/gui-plugins/src/btw/model.ts:107 and packages/app/src/runtime/plugin/host-apis.tsx:1026.

Verification

Control experiment on the same server/session in Chromium:

  • http://127.0.0.1:<port>: clicking Open file opens the “Open file” tab and shows “Select a file to open”.
  • Same page with Crypto.prototype.randomUUID removed (equivalent to an insecure context): the click logs TypeError: crypto.randomUUID is not a function, the tab strip still shows only “New tab”; clicking Terminal right after opens “Terminal 1” normally.
  • On the insecure origin, typeof crypto.randomUUID evaluates to "undefined" and isSecureContext is false.

Environment

  • OpenCode: v2.2.0
  • OS: Ubuntu 24.04 (Linux 7.0.0-34-generic)
  • Browser: Chrome 155.0.8059.39
  • Web UI: served by the local service, accessed via http://<LAN-IP>:<port>
  • Plugins: none relevant (built-in file, terminal GUI plugins only)

Activity

  1. opencodejr commented on Oct 28, 2026

    @opencodejr

    Thanks for the detailed report. I reproduced this on v2.2.0, and it still happens on the latest dev (86ce19d).

    1. Ran opencode serve --hostname 0.0.0.0 and opened the web UI in Chromium at the machine's LAN IP over plain HTTP (isSecureContext is false, crypto.randomUUID is undefined).
    2. Opened a session, toggled the side panel and clicked Open file in the new-tab view.
    3. The page threw TypeError: crypto.randomUUID is not a function, and the tab strip still showed only "New tab".

    The same steps on http://127.0.0.1 open the "Open file" tab with "Select a file to open". The screenshots below show both cases, and the script below reproduces it. The unguarded call in packages/gui-plugins/src/file/renderer.tsx is still there on dev.

    Related issues:

    Screenshot

    Screenshot

    Reproduction script (repro.mjs)
    // Reproduces #59791: side-panel "Open file" does nothing on an insecure (non-localhost HTTP) origin.
    // Setup:
    //   cd <git project> && opencode serve --hostname 0.0.0.0 --port 4096   (note the printed server password)
    //   curl -u opencode:$PW -XPOST -H 'content-type: application/json' \
    //     "http://127.0.0.1:4096/api/session?directory=$PWD" -d '{}'
    //   npm i playwright-core && npx playwright-core install --with-deps chromium
    // Run: node repro.mjs http://<LAN-IP>:4096/ $PW      (insecure origin -> fails)
    //      node repro.mjs http://127.0.0.1:4096/ $PW     (secure origin -> works)
    import { chromium } from "playwright-core"
    const [url, pw, shot = "/workspace/repro/result.png"] = process.argv.slice(2)
    const b = await chromium.launch()
    const p = await b.newPage({ viewport: { width: 1400, height: 900 } })
    p.on("pageerror", (e) => console.log("pageerror:", e.message))
    await p.goto(url)
    await p.waitForTimeout(3000)
    if (await p.getByText("Use password").count()) {
      await p.getByText("Use password").click()
      await p.locator("#server-connect-url").fill(url)
      await p.locator("#server-connect-password").fill(pw)
      await p.keyboard.press("Enter")
      await p.waitForTimeout(4000)
    }
    console.log("context:", await p.evaluate(() => ({ isSecureContext, randomUUID: typeof crypto.randomUUID })))
    await p.getByText("New session").first().click()
    await p.waitForTimeout(3000)
    await p.getByLabel("Toggle review").click()
    await p.waitForTimeout(1500)
    const tabs = async () => (await p.locator("[role=tab]").allInnerTexts()).map((t) => t.split("\n")[0])
    console.log("tabs before:", await tabs())
    await p.getByRole("button", { name: /Open file/ }).or(p.getByText("Open file", { exact: true })).first().click()
    await p.waitForTimeout(2000)
    console.log("tabs after Open file:", await tabs())
    console.log("'Select a file to open' visible:", await p.getByText("Select a file to open").count() > 0)
    await p.screenshot({ path: shot })
    await b.close()

    Run it: bun repro.mjs

  2. Aeromtrich commented on Oct 28, 2026

    @Aeromtrich

    I'd like to work on this issue. Is anyone already working on a fix? If not, I'd be happy to take it. 🧐

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions