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
7 changes: 7 additions & 0 deletions packages/app/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@
- App (from `packages/app`): `bun dev -- --port 4444`
- Open `http://localhost:4444` to verify UI changes (it targets the backend at `http://localhost:4096`).

## Insecure Origins (HTTP LAN)

The Web UI is served over plain HTTP on non-localhost LAN and Tailscale origins (`isSecureContext === false`). This rule applies across `app`, `ui`, `session-ui` and `gui-plugins` window code.

- Never call `crypto.randomUUID()` directly in renderer or browser code; it is `[SecureContext]`-only and throws `TypeError: crypto.randomUUID is not a function` on plain HTTP origins. Always use `uuid()` from `@opencode/util/uuid`.
- Guard any other `[SecureContext]`-only Web API (`crypto.subtle`, `navigator.clipboard`, `navigator.serviceWorker`) with a non-secure fallback.

## SolidJS

- Always prefer `createStore` over multiple `createSignal` calls
Expand Down
21 changes: 21 additions & 0 deletions packages/app/e2e/regression/review.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,27 @@ test("a rejected Git init releases its session's button without showing an error
await expect(init).toBeEnabled()
})

test("opens file picker launchers when randomUUID is unavailable on insecure HTTP origins", async ({ page }) => {
await page.addInitScript(() => {
Object.defineProperty(globalThis, "isSecureContext", { configurable: true, value: false })
Object.defineProperty(globalThis.crypto, "randomUUID", { configurable: true, value: undefined })
})

await openSession(page, {
name: "ReviewOpenFileInsecure",
seed: { panes: { ses_reviewopenfileinsecure: { review: true } } },
})

const panel = page.locator("#review-panel")
const tab = panel.getByRole("tab", { name: "Open file", exact: true })
await openFileLauncher(panel)
await expect(tab).toHaveAttribute("aria-selected", "true")
await expect(panel.getByRole("combobox", { name: "Filter files" })).toBeFocused()

await openFileLauncher(panel)
await expect(tab).toHaveCount(2)
})

test("open file tab browses, searches, and tracks missing files", async ({ page }) => {
const searches: { query: string; dirs?: string; limit?: number; directory: string }[] = []
const directory = "C:/OpenCode/ReviewOpenFile"
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/composer/attachments/attachments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import { createBlobReference } from "@/runtime/persistence/drafts"
import { uuid } from "@/runtime/persistence/uuid"
import { uuid } from "@opencode/util/uuid"
import type { ComposerPrompt } from "../types"
import type { ImageAttachmentPart, PathAttachmentPart } from "../state"
import type { AttachmentDestination } from "./destination"
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/composer/attachments/destination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Accessor } from "solid-js"
import { useServer } from "@/runtime/server/current"
import { useServerSDK } from "@/runtime/server/client"
import { authTokenFromCredentials } from "@/runtime/server/api"
import { uuid } from "@/runtime/persistence/uuid"
import { uuid } from "@opencode/util/uuid"
import { useWorkspaceLocation } from "@/workspaces/location"
import type { ComposerControls } from "../adapter"

Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/composer/comment-note.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { SessionMessageUser } from "@opencode/client/promise"
import { Option, Schema } from "effect"
import { Persistence } from "@/runtime/persistence/schema"
import { uuid } from "@/runtime/persistence/uuid"
import { uuid } from "@opencode/util/uuid"
import { FileSelection } from "@/workspaces/files/types"
import { durableNote, LegacyBrowserNote, NoteComment, QuoteComment, type ContextItem } from "./schema"

Expand Down
11 changes: 7 additions & 4 deletions packages/app/src/composer/comments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Persist, persisted } from "@/runtime/persistence/storage"
import { useServerSDK } from "@/runtime/server/client"
import type { ServerScope } from "@/runtime/server/scope"
import { createScopedCache } from "@/runtime/server/scoped-cache"
import { uuid } from "@/runtime/persistence/uuid"
import { uuid } from "@opencode/util/uuid"
import type { SelectedLineRange } from "@/workspaces/files/model"
import { useWorkspaceLocation } from "@/workspaces/location"
import { CommentStore, type LineComment } from "./schema"
Expand Down Expand Up @@ -80,9 +80,12 @@ function group(comments: LineComment[]) {
}

function createCommentSessionState(store: Store<CommentStore>, setStore: SetStoreFunction<CommentStore>) {
const [state, setState] = createStore({
focus: null as CommentFocus | null,
active: null as CommentFocus | null,
const [state, setState] = createStore<{
focus: CommentFocus | null
active: CommentFocus | null
}>({
focus: null,
active: null,
})

// Reuse the previous array when contents are unchanged so consumers keep a stable
Expand Down
3 changes: 2 additions & 1 deletion packages/app/src/runtime/plugin/host-apis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { useDialog } from "@opencode/ui/context/dialog"
import { useTheme } from "@opencode/ui/theme/context"
import { toaster } from "@opencode/ui/toast"
import { base64Encode } from "@opencode/util/encode"
import { uuid } from "@opencode/util/uuid"
import {
type App,
type Layout,
Expand Down Expand Up @@ -1023,7 +1024,7 @@ export function createPluginAttachment(apis: HostApis) {
id: error ? settings.sounds.errors() : settings.sounds.agent(),
}))

const key = `plugin:${plugin}\0${options.id ?? crypto.randomUUID()}`
const key = `plugin:${plugin}\0${options.id ?? uuid()}`
const result = { notification: false, sound: false }
const session = options.session

Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/runtime/server/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Data } from "@opencode/client/solid"
import type { OpenCodeEvent, SessionInfo } from "@opencode/client/promise"
import { onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { uuid } from "@/runtime/persistence/uuid"
import { uuid } from "@opencode/util/uuid"

type SessionMutation = { readonly id: string; readonly type: "remove"; readonly sessionID: string }

Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/session/composer/region.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { showToast } from "@/shell/notifications/toast"
import { errorMessage } from "@/shell/layout/helpers"
import { useLocal } from "@/providers/models/selection"
import { usePlatform } from "@/runtime/platform/platform"
import { uuid } from "@/runtime/persistence/uuid"
import { uuid } from "@opencode/util/uuid"
import { contextItemKey } from "@/composer/schema"
import type { QuoteSource } from "@/composer/quote-source"
import type { ComposerQuoteAnchor } from "@opencode/plugin/gui"
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/shell/tabs/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { usePluginServers } from "@/runtime/plugin/servers"
import { createEffect, getOwner, onCleanup, startTransition } from "solid-js"
import { useLocation, useNavigate } from "@solidjs/router"
import { usePlatform } from "@/runtime/platform/platform"
import { uuid } from "@/runtime/persistence/uuid"
import { uuid } from "@opencode/util/uuid"
import { SessionTabsRemovedDetail } from "@/shell/titlebar/session-events"
import { sessionHref } from "@/shell/routes/session"
import { createTabMemory } from "./memory"
Expand Down
5 changes: 3 additions & 2 deletions packages/app/src/voice/transport.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { OpenCodeClient } from "@opencode/client/promise"
import { uuid } from "@opencode/util/uuid"
import { decodeEvent, type VoiceEvent } from "./protocol"
import { delegatorInstructions, liveInstructions, tools } from "./spec"

Expand Down Expand Up @@ -162,7 +163,7 @@ export class VoiceTransport {

send(event: VoiceClientEvent) {
if (this.ended || this.channel.readyState !== "open") throw new VoiceFailure("dropped")
this.channel.send(JSON.stringify({ event_id: crypto.randomUUID(), ...event }))
this.channel.send(JSON.stringify({ event_id: uuid(), ...event }))
}

mute(muted: boolean) {
Expand Down Expand Up @@ -192,7 +193,7 @@ export class VoiceTransport {
await Promise.resolve()
.then(async () => {
if (this.channel.readyState !== "open") return
this.channel.send(JSON.stringify({ type: "session.close", event_id: crypto.randomUUID() }))
this.channel.send(JSON.stringify({ type: "session.close", event_id: uuid() }))
const timeout = setTimeout(() => this.closed.resolve(), 1000)
await this.closed.promise
clearTimeout(timeout)
Expand Down
1 change: 1 addition & 0 deletions packages/gui-plugins/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Built-in features of the desktop and web app, each behind the GUI plugin SDK, `@
- The APIs the host always provides are context properties (`ctx.layout`, `ctx.sessions`, `ctx.storage`, `ctx.desktop`, …; in main `ctx.storage`, `ctx.windows`, `ctx.serverEndpoints`, …). `usePlugin<typeof definition>()` returns the same context in components. Registries and contracts stay tokens: `ctx.add(Registry, item)`, `ctx.provide(Contract, impl)`; main provides Ipcs with `ctx.provide(Ipc, impl)`.
- Fonts follow the Fonts section of `packages/app/AGENTS.md`. Terminal-rendered output uses `ctx.appearance.font("mono")` (the terminal font); other UI uses the code and UI font CSS variables. Never bundle font files or write literal families, except for documents that carry their own fonts (Office, font previews).
- Never import `@opencode/app`, `@opencode/desktop`, or `@/` paths. Import CSS with `?inline` and contribute it through `ctx.add(Style, css)`. No module-level state: keep state inside `setup`. `bun run lint` enforces these rules.
- Never call `crypto.randomUUID()` directly in window code; use `uuid()` from `@opencode/util/uuid` so plain HTTP LAN origins keep working (see the Insecure Origins section of `packages/app/AGENTS.md`).
- `bun run lint:changed` must pass before you finish: every file you add or edit has no oxlint problem at all, including the warn-level anti-slop rules (`unknown` parameters and returns, unchecked type assertions, widened types, unsafe dictionaries, missing spacing). Touching a file means leaving the whole file clean, older warnings included. Fix the code; suppress only with a `SAFETY:` comment that states a real checked invariant.

## SDK docs
Expand Down
5 changes: 3 additions & 2 deletions packages/gui-plugins/src/browser/comment.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Browser } from "@opencode/plugin-browser/rpc"
import { uuid } from "@opencode/util/uuid"
import { Schema } from "effect"
import type { ComposerNote } from "@opencode/plugin/gui"
import type { PaneElement } from "./ipc"
Expand Down Expand Up @@ -30,7 +31,7 @@ export function commentNote(input: ElementComment): ComposerNote {
const note: ComposerNote = {
type: "note",
origin: input.origin,
commentID: crypto.randomUUID(),
commentID: uuid(),
label: input.element.label,
title: input.element.selector || input.element.label,
subtitle: input.subtitle,
Expand Down Expand Up @@ -58,7 +59,7 @@ export function pageNote(input: {
return {
type: "note",
origin: input.origin,
commentID: crypto.randomUUID(),
commentID: uuid(),
label: input.label,
icon: "outline-globe",
subject: `the page in browser tab ${input.tabID} at ${input.url}${title}`,
Expand Down
3 changes: 2 additions & 1 deletion packages/gui-plugins/src/browser/connection.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Browser } from "@opencode/plugin-browser/rpc"
import type { IpcClient } from "@opencode/plugin/gui"
import { uuid } from "@opencode/util/uuid"
import type { BrowserPane, PaneEvent } from "./ipc"

type Client = IpcClient<(typeof BrowserPane)["spec"]>
Expand Down Expand Up @@ -310,7 +311,7 @@ function open(
listener: (event: PaneEvent) => void,
failed: (cause: unknown) => void,
): Registration {
const binding = crypto.randomUUID()
const binding = uuid()
const status = { closed: false }

const stop = listen(binding, (event) => {
Expand Down
3 changes: 2 additions & 1 deletion packages/gui-plugins/src/btw/model.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { batch, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { showToast } from "@opencode/ui/toast"
import { uuid } from "@opencode/util/uuid"
import type { MountedSession, SetupContext } from "@opencode/plugin/gui"
import type Btw from "./index"

Expand Down Expand Up @@ -104,7 +105,7 @@ export function createBtw(ctx: SetupContext<typeof Btw>) {

if (!session?.id) return

const id = crypto.randomUUID()
const id = uuid()
const created = Date.now()
const current = selection(session)

Expand Down
3 changes: 2 additions & 1 deletion packages/gui-plugins/src/file/renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
type Plugin,
} from "@opencode/plugin/gui"
import { artifactKind } from "@opencode/util/artifact"
import { uuid } from "@opencode/util/uuid"
import { resolveArtifactPath } from "./artifact"
import { FileContext, type FileShared } from "./context"
import type { OpenApp } from "./apps"
Expand Down Expand Up @@ -167,7 +168,7 @@ const setup: Plugin.Setup<typeof File> = (ctx) => {

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" })
else layout.open(`file:${OPEN}:${uuid()}`, session, { tab: "select" })

if (!options?.background && layout.narrow() && !layout.side.opened(session)) layout.side.toggle(session)
})
Expand Down
3 changes: 2 additions & 1 deletion packages/gui-plugins/src/ssh/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@op
import { Divider } from "@opencode/ui/divider"
import { TextInput } from "@opencode/ui/text-input"
import { useDialog } from "@opencode/ui/context/dialog"
import { uuid } from "@opencode/util/uuid"
import { createMemo, createSignal, on, onCleanup, onMount, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { createKeyed, usePlugin } from "@opencode/plugin/gui"
Expand All @@ -26,7 +27,7 @@ export function DialogSsh(props: {
const ssh = props.ssh
// Entry-point behavior is fixed for the lifetime of this dialog.
const openProject = props.onOpenProject
const id = props.config?.id ?? crypto.randomUUID()
const id = props.config?.id ?? uuid()
let cancelButton: HTMLButtonElement | undefined

const [state, setState] = createStore({
Expand Down
3 changes: 2 additions & 1 deletion packages/gui-plugins/src/terminal/panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { IconButton } from "@opencode/ui/icon-button"
import { Icon } from "@opencode/ui/icon"
import { Tooltip } from "@opencode/ui/tooltip"
import { Keybind } from "@opencode/ui/keybind"
import { uuid } from "@opencode/util/uuid"
import { createKeyed, usePlugin, usePanel, type MountedSession, type SessionScreen } from "@opencode/plugin/gui"
import type { TerminalModel, TerminalWorkspace } from "./model"
import { terminalTab, type LocalPTY } from "./state"
Expand Down Expand Up @@ -62,7 +63,7 @@ export default function TerminalPanel(props: {
available: () => opened() && props.screen.composer.quotable(),
add(text, anchor) {
const composer = props.screen.composer
const commentID = crypto.randomUUID()
const commentID = uuid()

composer.attach({
type: "quote",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, test } from "bun:test"
import { uuid } from "./uuid"
import { uuid } from "./uuid.js"

const cryptoDescriptor = Object.getOwnPropertyDescriptor(globalThis, "crypto")

Expand Down
File renamed without changes.
Loading