From 5cf235fa6cf7b4c890c68f8ff68a96fcae992abf Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 4 Mar 2026 15:12:34 +0800 Subject: desktop: add electron version (#15663) --- packages/app/src/app.tsx | 11 ++++--- packages/app/src/components/titlebar.tsx | 2 ++ packages/app/src/pages/layout.tsx | 2 ++ packages/app/src/utils/notification-click.test.ts | 37 ++++++++++++----------- packages/app/src/utils/notification-click.ts | 16 +++++----- 5 files changed, 38 insertions(+), 30 deletions(-) (limited to 'packages/app/src') diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 4a25e8d94..52a1dac6a 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -7,8 +7,8 @@ import { MarkedProvider } from "@opencode-ai/ui/context/marked" import { Font } from "@opencode-ai/ui/font" import { ThemeProvider } from "@opencode-ai/ui/theme" import { MetaProvider } from "@solidjs/meta" -import { Navigate, Route, Router } from "@solidjs/router" -import { ErrorBoundary, type JSX, lazy, type ParentProps, Show, Suspense } from "solid-js" +import { BaseRouterProps, Navigate, Route, Router } from "@solidjs/router" +import { Component, ErrorBoundary, type JSX, lazy, type ParentProps, Show, Suspense } from "solid-js" import { CommandProvider } from "@/context/command" import { CommentsProvider } from "@/context/comments" import { FileProvider } from "@/context/file" @@ -28,6 +28,7 @@ import { TerminalProvider } from "@/context/terminal" import DirectoryLayout from "@/pages/directory-layout" import Layout from "@/pages/layout" import { ErrorPage } from "./pages/error" +import { Dynamic } from "solid-js/web" const Home = lazy(() => import("@/pages/home")) const Session = lazy(() => import("@/pages/session")) @@ -144,13 +145,15 @@ export function AppInterface(props: { children?: JSX.Element defaultServer: ServerConnection.Key servers?: Array + router?: Component }) { return ( - {routerProps.children}} > @@ -158,7 +161,7 @@ export function AppInterface(props: { - + diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index 760f40fc0..c2b5a1ef4 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -157,6 +157,7 @@ export function Titlebar() {
@@ -276,6 +277,7 @@ export function Titlebar() { "flex items-center min-w-0 justify-end": true, "pr-2": !windows(), }} + data-tauri-drag-region onMouseDown={drag} >
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index daf2aaa5c..2fd2f2fe3 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -42,6 +42,7 @@ import { Binary } from "@opencode-ai/util/binary" import { retry } from "@opencode-ai/util/retry" import { playSound, soundSrc } from "@/utils/sound" import { createAim } from "@/utils/aim" +import { setNavigate } from "@/utils/notification-click" import { Worktree as WorktreeState } from "@/utils/worktree" import { useDialog } from "@opencode-ai/ui/context/dialog" @@ -107,6 +108,7 @@ export default function Layout(props: ParentProps) { const notification = useNotification() const permission = usePermission() const navigate = useNavigate() + setNavigate(navigate) const providers = useProviders() const dialog = useDialog() const command = useCommand() diff --git a/packages/app/src/utils/notification-click.test.ts b/packages/app/src/utils/notification-click.test.ts index 76535f83a..fa81b0e02 100644 --- a/packages/app/src/utils/notification-click.test.ts +++ b/packages/app/src/utils/notification-click.test.ts @@ -1,26 +1,27 @@ -import { describe, expect, test } from "bun:test" -import { handleNotificationClick } from "./notification-click" +import { afterEach, describe, expect, test } from "bun:test" +import { handleNotificationClick, setNavigate } from "./notification-click" describe("notification click", () => { - test("focuses and navigates when href exists", () => { + afterEach(() => { + setNavigate(undefined as any) + }) + + test("navigates via registered navigate function", () => { const calls: string[] = [] - handleNotificationClick("/abc/session/123", { - focus: () => calls.push("focus"), - location: { - assign: (href) => calls.push(href), - }, - }) - expect(calls).toEqual(["focus", "/abc/session/123"]) + setNavigate((href) => calls.push(href)) + handleNotificationClick("/abc/session/123") + expect(calls).toEqual(["/abc/session/123"]) }) - test("only focuses when href is missing", () => { + test("does not navigate when href is missing", () => { const calls: string[] = [] - handleNotificationClick(undefined, { - focus: () => calls.push("focus"), - location: { - assign: (href) => calls.push(href), - }, - }) - expect(calls).toEqual(["focus"]) + setNavigate((href) => calls.push(href)) + handleNotificationClick(undefined) + expect(calls).toEqual([]) + }) + + test("falls back to location.assign without registered navigate", () => { + handleNotificationClick("/abc/session/123") + // falls back to window.location.assign — no error thrown }) }) diff --git a/packages/app/src/utils/notification-click.ts b/packages/app/src/utils/notification-click.ts index 1234cd1d6..94086c595 100644 --- a/packages/app/src/utils/notification-click.ts +++ b/packages/app/src/utils/notification-click.ts @@ -1,12 +1,12 @@ -type WindowTarget = { - focus: () => void - location: { - assign: (href: string) => void - } +let nav: ((href: string) => void) | undefined + +export const setNavigate = (fn: (href: string) => void) => { + nav = fn } -export const handleNotificationClick = (href?: string, target: WindowTarget = window) => { - target.focus() +export const handleNotificationClick = (href?: string) => { + window.focus() if (!href) return - target.location.assign(href) + if (nav) nav(href) + else window.location.assign(href) } -- cgit v1.2.3 From d7569a5625e4a2287195c27a84330af1d9d2c3df Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 4 Mar 2026 07:03:44 -0600 Subject: fix(app): terminal tab close --- packages/app/src/components/terminal.tsx | 45 ++++++++++++----------- packages/app/src/pages/session/terminal-panel.tsx | 10 ++++- 2 files changed, 31 insertions(+), 24 deletions(-) (limited to 'packages/app/src') diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index 601ace28d..c27d6a977 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -18,7 +18,7 @@ const DEFAULT_TOGGLE_TERMINAL_KEYBIND = "ctrl+`" export interface TerminalProps extends ComponentProps<"div"> { pty: LocalPTY onSubmit?: () => void - onCleanup?: (pty: LocalPTY) => void + onCleanup?: (pty: Partial & { id: string }) => void onConnect?: () => void onConnectError?: (error: unknown) => void } @@ -126,8 +126,8 @@ const persistTerminal = (input: { term: Term | undefined addon: SerializeAddon | undefined cursor: number - pty: LocalPTY - onCleanup?: (pty: LocalPTY) => void + id: string + onCleanup?: (pty: Partial & { id: string }) => void }) => { if (!input.addon || !input.onCleanup || !input.term) return const buffer = (() => { @@ -140,7 +140,7 @@ const persistTerminal = (input: { })() input.onCleanup({ - ...input.pty, + id: input.id, buffer, cursor: input.cursor, rows: input.term.rows, @@ -158,6 +158,19 @@ export const Terminal = (props: TerminalProps) => { const server = useServer() let container!: HTMLDivElement const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnect", "onConnectError"]) + const id = local.pty.id + const restore = typeof local.pty.buffer === "string" ? local.pty.buffer : "" + const restoreSize = + restore && + typeof local.pty.cols === "number" && + Number.isSafeInteger(local.pty.cols) && + local.pty.cols > 0 && + typeof local.pty.rows === "number" && + Number.isSafeInteger(local.pty.rows) && + local.pty.rows > 0 + ? { cols: local.pty.cols, rows: local.pty.rows } + : undefined + const scrollY = typeof local.pty.scrollY === "number" ? local.pty.scrollY : undefined let ws: WebSocket | undefined let term: Term | undefined let ghostty: Ghostty @@ -190,7 +203,7 @@ export const Terminal = (props: TerminalProps) => { const pushSize = (cols: number, rows: number) => { return sdk.client.pty .update({ - ptyID: local.pty.id, + ptyID: id, size: { cols, rows }, }) .catch((err) => { @@ -319,18 +332,6 @@ export const Terminal = (props: TerminalProps) => { const mod = loaded.mod const g = loaded.ghostty - const restore = typeof local.pty.buffer === "string" ? local.pty.buffer : "" - const restoreSize = - restore && - typeof local.pty.cols === "number" && - Number.isSafeInteger(local.pty.cols) && - local.pty.cols > 0 && - typeof local.pty.rows === "number" && - Number.isSafeInteger(local.pty.rows) && - local.pty.rows > 0 - ? { cols: local.pty.cols, rows: local.pty.rows } - : undefined - const t = new mod.Terminal({ cursorBlink: true, cursorStyle: "bar", @@ -427,14 +428,14 @@ export const Terminal = (props: TerminalProps) => { await write(restore) fit.fit() scheduleSize(t.cols, t.rows) - if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY) + if (scrollY !== undefined) t.scrollToLine(scrollY) startResize() } else { fit.fit() scheduleSize(t.cols, t.rows) if (restore) { await write(restore) - if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY) + if (scrollY !== undefined) t.scrollToLine(scrollY) } startResize() } @@ -446,9 +447,9 @@ export const Terminal = (props: TerminalProps) => { const once = { value: false } let closing = false - const url = new URL(sdk.url + `/pty/${local.pty.id}/connect`) + const url = new URL(sdk.url + `/pty/${id}/connect`) url.searchParams.set("directory", sdk.directory) - url.searchParams.set("cursor", String(start !== undefined ? start : local.pty.buffer ? -1 : 0)) + url.searchParams.set("cursor", String(start !== undefined ? start : restore ? -1 : 0)) url.protocol = url.protocol === "https:" ? "wss:" : "ws:" url.username = server.current?.http.username ?? "" url.password = server.current?.http.password ?? "" @@ -542,7 +543,7 @@ export const Terminal = (props: TerminalProps) => { if (ws && ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) ws.close(1000) const finalize = () => { - persistTerminal({ term, addon: serializeAddon, cursor, pty: local.pty, onCleanup: props.onCleanup }) + persistTerminal({ term, addon: serializeAddon, cursor, id, onCleanup: props.onCleanup }) cleanup() } diff --git a/packages/app/src/pages/session/terminal-panel.tsx b/packages/app/src/pages/session/terminal-panel.tsx index 49bed9490..cc4c17ee2 100644 --- a/packages/app/src/pages/session/terminal-panel.tsx +++ b/packages/app/src/pages/session/terminal-panel.tsx @@ -102,7 +102,7 @@ export function TerminalPanel() { const all = createMemo(() => terminal.all()) const ids = createMemo(() => all().map((pty) => pty.id)) - const byId = createMemo(() => new Map(all().map((pty) => [pty.id, pty]))) + const byId = createMemo(() => new Map(all().map((pty) => [pty.id, { ...pty }]))) const handleTerminalDragStart = (event: unknown) => { const id = getDraggableId(event) @@ -189,7 +189,13 @@ export function TerminalPanel() { > - {(pty) => } + + {(id) => ( + + {(pty) => } + + )} +
Date: Wed, 4 Mar 2026 07:18:03 -0600 Subject: fix(app): loading session should be scrolled to the bottom --- .../app/src/pages/session/message-timeline.tsx | 419 +++++++++++---------- 1 file changed, 210 insertions(+), 209 deletions(-) (limited to 'packages/app/src') diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index a7f503d5a..433c36e2e 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -550,227 +550,228 @@ export function MessageTimeline(props: { "--sticky-accordion-top": showHeader() ? "48px" : "0px", }} > - -
-
-
- - - - - - {titleValue()} - - } - > - { - titleRef = el - }} - value={title.draft} - disabled={title.saving} - class="text-14-medium text-text-strong grow-1 min-w-0 pl-2 rounded-[6px]" - style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }} - onInput={(event) => setTitle("draft", event.currentTarget.value)} - onKeyDown={(event) => { - event.stopPropagation() - if (event.key === "Enter") { - event.preventDefault() - void saveTitleEditor() - return - } - if (event.key === "Escape") { - event.preventDefault() - closeTitleEditor() - } - }} - onBlur={closeTitleEditor} +
+ +
+
+
+ + - -
- - {(id) => ( -
- - setTitle("menuOpen", open)} + + + {titleValue()} + + } > - - - { - if (!title.pendingRename) return + { + titleRef = el + }} + value={title.draft} + disabled={title.saving} + class="text-14-medium text-text-strong grow-1 min-w-0 pl-2 rounded-[6px]" + style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }} + onInput={(event) => setTitle("draft", event.currentTarget.value)} + onKeyDown={(event) => { + event.stopPropagation() + if (event.key === "Enter") { event.preventDefault() - setTitle("pendingRename", false) - openTitleEditor() - }} - > - { - setTitle("pendingRename", true) - setTitle("menuOpen", false) + void saveTitleEditor() + return + } + if (event.key === "Escape") { + event.preventDefault() + closeTitleEditor() + } + }} + onBlur={closeTitleEditor} + /> + + +
+ + {(id) => ( +
+ + setTitle("menuOpen", open)} + > + + + { + if (!title.pendingRename) return + event.preventDefault() + setTitle("pendingRename", false) + openTitleEditor() }} > - {language.t("common.rename")} - - void archiveSession(id())}> - {language.t("common.archive")} - - - dialog.show(() => )} - > - {language.t("common.delete")} - - - - -
- )} -
-
-
-
- -
- 0 || props.historyMore}> -
- + { + setTitle("pendingRename", true) + setTitle("menuOpen", false) + }} + > + {language.t("common.rename")} + + void archiveSession(id())}> + {language.t("common.archive")} + + + dialog.show(() => )} + > + {language.t("common.delete")} + + + + +
+ )} +
+
- - {(messageID) => { - const active = createMemo(() => activeMessageID() === messageID) - const queued = createMemo(() => { - if (active()) return false - const activeID = activeMessageID() - if (activeID) return messageID > activeID - return false - }) - const comments = createMemo(() => messageComments(sync.data.part[messageID] ?? []), [], { - equals: (a, b) => JSON.stringify(a) === JSON.stringify(b), - }) - const commentCount = createMemo(() => comments().length) - return ( -
{ - props.onRegisterMessage(el, messageID) - onCleanup(() => props.onUnregisterMessage(messageID)) - }} - classList={{ - "min-w-0 w-full max-w-full": true, - "md:max-w-200 2xl:max-w-[1000px]": props.centered, - }} + +
+ 0 || props.historyMore}> +
+ +
+
+ + {(messageID) => { + const active = createMemo(() => activeMessageID() === messageID) + const queued = createMemo(() => { + if (active()) return false + const activeID = activeMessageID() + if (activeID) return messageID > activeID + return false + }) + const comments = createMemo(() => messageComments(sync.data.part[messageID] ?? []), [], { + equals: (a, b) => JSON.stringify(a) === JSON.stringify(b), + }) + const commentCount = createMemo(() => comments().length) + return ( +
{ + props.onRegisterMessage(el, messageID) + onCleanup(() => props.onUnregisterMessage(messageID)) + }} + classList={{ + "min-w-0 w-full max-w-full": true, + "md:max-w-200 2xl:max-w-[1000px]": props.centered, + }} + > + 0}> +
+
+
+ + {(commentAccessor: () => MessageComment) => { + const comment = createMemo(() => commentAccessor()) + return ( +
+
+ + {getFilename(comment().path)} + + {(selection) => ( + + {selection().startLine === selection().endLine + ? `:${selection().startLine}` + : `:${selection().startLine}-${selection().endLine}`} + + )} + +
+
+ {comment().comment} +
-
- ) - }} - + ) + }} + +
-
- - -
- ) - }} -
+
+ +
+ ) + }} + +
-- cgit v1.2.3 From e482405cdc49654a1b8a5013958d295a89aa4831 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 4 Mar 2026 07:36:10 -0600 Subject: fix(app): remove diff lines from sessions in sidebar --- packages/app/src/pages/layout/sidebar-items.tsx | 8 -------- 1 file changed, 8 deletions(-) (limited to 'packages/app/src') diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx index eecfd17b5..0aaabc03b 100644 --- a/packages/app/src/pages/layout/sidebar-items.tsx +++ b/packages/app/src/pages/layout/sidebar-items.tsx @@ -6,7 +6,6 @@ import { useNotification } from "@/context/notification" import { usePermission } from "@/context/permission" import { base64Encode } from "@opencode-ai/util/encode" import { Avatar } from "@opencode-ai/ui/avatar" -import { DiffChanges } from "@opencode-ai/ui/diff-changes" import { HoverCard } from "@opencode-ai/ui/hover-card" import { Icon } from "@opencode-ai/ui/icon" import { IconButton } from "@opencode-ai/ui/icon-button" @@ -137,13 +136,6 @@ const SessionRow = (props: { {props.session.title} - - {(summary) => ( -
- -
- )} -
) -- cgit v1.2.3 From 64b21135f922e20ced02f9233dbff6aa88e8deaa Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 4 Mar 2026 07:39:25 -0600 Subject: fix(app): delay dock animation on session load --- packages/app/src/pages/session.tsx | 1 + .../session/composer/session-composer-region.tsx | 43 +++++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) (limited to 'packages/app/src') diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 389c0baea..cc81ae7b6 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1254,6 +1254,7 @@ export default function Page() { { inputRef = el diff --git a/packages/app/src/pages/session/composer/session-composer-region.tsx b/packages/app/src/pages/session/composer/session-composer-region.tsx index a882e25dd..93ea3d465 100644 --- a/packages/app/src/pages/session/composer/session-composer-region.tsx +++ b/packages/app/src/pages/session/composer/session-composer-region.tsx @@ -1,4 +1,5 @@ import { Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js" +import { createStore } from "solid-js/store" import { useParams } from "@solidjs/router" import { useSpring } from "@opencode-ai/ui/motion-spring" import { PromptInput } from "@/components/prompt-input" @@ -12,6 +13,7 @@ import { SessionTodoDock } from "@/pages/session/composer/session-todo-dock" export function SessionComposerRegion(props: { state: SessionComposerState + ready: boolean centered: boolean inputRef: (el: HTMLDivElement) => void newSessionWorktree: string @@ -61,7 +63,44 @@ export function SessionComposerRegion(props: { setSessionHandoff(sessionKey(), { prompt: previewPrompt() }) }) - const open = createMemo(() => props.state.dock() && !props.state.closing()) + const [gate, setGate] = createStore({ + ready: false, + }) + let timer: number | undefined + let frame: number | undefined + + const clear = () => { + if (timer !== undefined) { + window.clearTimeout(timer) + timer = undefined + } + if (frame !== undefined) { + cancelAnimationFrame(frame) + frame = undefined + } + } + + createEffect(() => { + sessionKey() + const ready = props.ready + const delay = 140 + + clear() + setGate("ready", false) + if (!ready) return + + frame = requestAnimationFrame(() => { + frame = undefined + timer = window.setTimeout(() => { + setGate("ready", true) + timer = undefined + }, delay) + }) + }) + + onCleanup(clear) + + const open = createMemo(() => gate.ready && props.state.dock() && !props.state.closing()) const config = createMemo(() => open() ? { @@ -76,7 +115,7 @@ export function SessionComposerRegion(props: { const progress = useSpring(() => (open() ? 1 : 0), config) const value = createMemo(() => Math.max(0, Math.min(1, progress()))) const [height, setHeight] = createSignal(320) - const dock = createMemo(() => props.state.dock() || value() > 0.001) + const dock = createMemo(() => (gate.ready && props.state.dock()) || value() > 0.001) const full = createMemo(() => Math.max(78, height())) const [contentRef, setContentRef] = createSignal() -- cgit v1.2.3 From a69742ccb26ad70d7442a133d86b69725af1ef06 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 4 Mar 2026 07:43:28 -0600 Subject: fix(app): remove blur from todos --- packages/app/src/pages/session/composer/session-todo-dock.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) (limited to 'packages/app/src') diff --git a/packages/app/src/pages/session/composer/session-todo-dock.tsx b/packages/app/src/pages/session/composer/session-todo-dock.tsx index ab8755aec..da2b8c8da 100644 --- a/packages/app/src/pages/session/composer/session-todo-dock.tsx +++ b/packages/app/src/pages/session/composer/session-todo-dock.tsx @@ -281,10 +281,8 @@ function TodoList(props: { todos: Todo[]; open: boolean }) { style={{ "--checkbox-align": "flex-start", "--checkbox-offset": "1px", - transition: - "opacity 220ms var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1)), filter 220ms var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1))", + transition: "opacity 220ms var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1))", opacity: todo().status === "pending" ? "0.94" : "1", - filter: todo().status === "pending" ? "blur(0.3px)" : "blur(0px)", }} > -- cgit v1.2.3