From 679270d9e0731c2b3e2c059d83907cb4086d90e2 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 12 Jan 2026 10:11:29 -0600 Subject: [PATCH] feat(app): new layout --- packages/app/index.html | 1 - .../src/components/session/session-header.tsx | 101 +--- packages/app/src/components/titlebar.tsx | 115 ++++ packages/app/src/context/layout.tsx | 8 + packages/app/src/context/sync.tsx | 2 +- packages/app/src/index.css | 4 + packages/app/src/pages/layout.tsx | 523 ++++++++++++------ packages/app/src/pages/session.tsx | 19 +- packages/desktop/index.html | 2 +- packages/desktop/src-tauri/Cargo.toml | 1 + .../src-tauri/capabilities/default.json | 1 + packages/desktop/src-tauri/src/lib.rs | 45 +- packages/desktop/src-tauri/tauri.conf.json | 14 + .../desktop/src-tauri/tauri.prod.conf.json | 21 + packages/ui/src/components/hover-card.css | 55 ++ packages/ui/src/components/hover-card.tsx | 31 ++ packages/ui/src/components/icon.tsx | 6 +- packages/ui/src/components/spinner.tsx | 7 +- packages/ui/src/styles/index.css | 1 + 19 files changed, 681 insertions(+), 276 deletions(-) create mode 100644 packages/app/src/components/titlebar.tsx create mode 100644 packages/ui/src/components/hover-card.css create mode 100644 packages/ui/src/components/hover-card.tsx diff --git a/packages/app/index.html b/packages/app/index.html index e0fbe6913..a8d663454 100644 --- a/packages/app/index.html +++ b/packages/app/index.html @@ -13,7 +13,6 @@ - diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index b2e7fafeb..5ed721740 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -34,6 +34,17 @@ export function SessionHeader() { const sync = useSync() const projectDirectory = createMemo(() => base64Decode(params.dir ?? "")) + const project = createMemo(() => { + const directory = projectDirectory() + if (!directory) return + return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory)) + }) + const name = createMemo(() => { + const current = project() + if (current) return current.name || getFilename(current.worktree) + return getFilename(projectDirectory()) + }) + const hotkey = createMemo(() => command.keybind("file.open")) const sessions = createMemo(() => (sync.data.session ?? []).filter((s) => !s.parentID)) const currentSession = createMemo(() => sync.data.session.find((s) => s.id === params.id)) @@ -58,87 +69,29 @@ export function SessionHeader() { navigate(`/${params.dir}/session/${session.id}`) } - const leftMount = createMemo(() => document.getElementById("opencode-titlebar-left")) + const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center")) const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right")) return ( <> - + {(mount) => ( -
-
- - - x.title} - value={(x) => x.id} - onSelect={(session) => { - const currentParent = parentSession() - if (session && currentParent && session.id !== currentParent.id) { - navigateToSession(session) - } - }} - class="text-14-regular text-text-base max-w-[calc(100vw-180px)] md:max-w-md" - variant="ghost" - /> -
/
- - - -
- -
- - +
)}
diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx new file mode 100644 index 000000000..5cf9f74bc --- /dev/null +++ b/packages/app/src/components/titlebar.tsx @@ -0,0 +1,115 @@ +import { createEffect, createMemo, Show } from "solid-js" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { TooltipKeybind } from "@opencode-ai/ui/tooltip" +import { useTheme } from "@opencode-ai/ui/theme" + +import { useLayout } from "@/context/layout" +import { usePlatform } from "@/context/platform" +import { useCommand } from "@/context/command" + +export function Titlebar() { + const layout = useLayout() + const platform = usePlatform() + const command = useCommand() + const theme = useTheme() + + const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos") + const reserve = createMemo( + () => platform.platform === "desktop" && (platform.os === "windows" || platform.os === "linux"), + ) + + const getWin = () => { + if (platform.platform !== "desktop") return + + const tauri = ( + window as unknown as { + __TAURI__?: { window?: { getCurrentWindow?: () => { startDragging?: () => Promise } } } + } + ).__TAURI__ + if (!tauri?.window?.getCurrentWindow) return + + return tauri.window.getCurrentWindow() + } + + createEffect(() => { + if (platform.platform !== "desktop") return + + const scheme = theme.colorScheme() + const value = scheme === "system" ? null : scheme + + const tauri = (window as unknown as { __TAURI__?: { webviewWindow?: { getCurrentWebviewWindow?: () => unknown } } }) + .__TAURI__ + const get = tauri?.webviewWindow?.getCurrentWebviewWindow + if (!get) return + + const win = get() as { setTheme?: (theme?: "light" | "dark" | null) => Promise } + if (!win.setTheme) return + + void win.setTheme(value).catch(() => undefined) + }) + + const interactive = (target: EventTarget | null) => { + if (!(target instanceof Element)) return false + + const selector = + "button, a, input, textarea, select, option, [role='button'], [role='menuitem'], [contenteditable='true'], [contenteditable='']" + + return !!target.closest(selector) + } + + const drag = (e: MouseEvent) => { + if (platform.platform !== "desktop") return + if (e.buttons !== 1) return + if (interactive(e.target)) return + + const win = getWin() + if (!win?.startDragging) return + + e.preventDefault() + void win.startDragging().catch(() => undefined) + } + + return ( +
+
+ +
+ + + +
+
+
+ +
+ +
+
+
+
+
+ ) +} diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index 385f564fa..ba332be7b 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -53,6 +53,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( sidebar: { opened: false, width: 280, + workspaces: false, }, terminal: { height: 280, @@ -304,6 +305,13 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( resize(width: number) { setStore("sidebar", "width", width) }, + workspaces: createMemo(() => store.sidebar.workspaces ?? false), + setWorkspaces(value: boolean) { + setStore("sidebar", "workspaces", value) + }, + toggleWorkspaces() { + setStore("sidebar", "workspaces", (x) => !x) + }, }, terminal: { height: createMemo(() => store.terminal.height), diff --git a/packages/app/src/context/sync.tsx b/packages/app/src/context/sync.tsx index e5f2c076e..33129e1b4 100644 --- a/packages/app/src/context/sync.tsx +++ b/packages/app/src/context/sync.tsx @@ -14,7 +14,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const sdk = useSDK() const [store, setStore] = globalSync.child(sdk.directory) const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/") - const chunk = 200 + const chunk = 400 const inflight = new Map>() const inflightDiff = new Map>() const inflightTodo = new Map>() diff --git a/packages/app/src/index.css b/packages/app/src/index.css index e40f0842b..d9d51aa8f 100644 --- a/packages/app/src/index.css +++ b/packages/app/src/index.css @@ -5,3 +5,7 @@ cursor: default; } } + +*[data-tauri-drag-region] { + app-region: drag; +} diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 535f4aef2..cc5396656 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -23,6 +23,8 @@ import { Button } from "@opencode-ai/ui/button" import { Icon } from "@opencode-ai/ui/icon" import { IconButton } from "@opencode-ai/ui/icon-button" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" +import { HoverCard } from "@opencode-ai/ui/hover-card" +import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" import { Collapsible } from "@opencode-ai/ui/collapsible" import { DiffChanges } from "@opencode-ai/ui/diff-changes" import { Spinner } from "@opencode-ai/ui/spinner" @@ -55,6 +57,8 @@ import { useCommand, type CommandOption } from "@/context/command" import { ConstrainDragXAxis } from "@/utils/solid-dnd" import { navStart } from "@/utils/perf" import { DialogSelectDirectory } from "@/components/dialog-select-directory" +import { DialogEditProject } from "@/components/dialog-edit-project" +import { Titlebar } from "@/components/titlebar" import { useServer } from "@/context/server" export default function Layout(props: ParentProps) { @@ -814,20 +818,24 @@ export default function Layout(props: ParentProps) { const opencode = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750" return ( -
- 0 && props.notify ? { "-webkit-mask-image": mask, "mask-image": mask } : undefined - } - /> +
+
+ 0 && props.notify + ? { "-webkit-mask-image": mask, "mask-image": mask } + : undefined + } + /> +
0 && props.notify}>
{ + const SessionItem = (props: { session: Session; slug: string; mobile?: boolean; dense?: boolean }): JSX.Element => { const notification = useNotification() const notifications = createMemo(() => notification.session.unseen(props.session.id)) const hasError = createMemo(() => notifications().some((n) => n.type === "error")) @@ -859,47 +867,62 @@ export default function Layout(props: ParentProps) { return status?.type === "busy" || status?.type === "retry" }) + const tint = createMemo(() => { + const messages = sessionStore.message[props.session.id] + if (!messages) return undefined + const user = messages + .slice() + .reverse() + .find((m) => m.role === "user") + if (!user?.agent) return undefined + + const agent = sessionStore.agent.find((a) => a.name === user.agent) + return agent?.color + }) + return (
prefetchSession(props.session, "high")} onFocus={() => prefetchSession(props.session, "high")} > - - {props.session.title} - -
- - - - - -
- - -
- - 0}> -
- - +
+
+ + + + + +
+ + +
+ + 0}> +
+ + +
+ + {props.session.title} + {(summary) => }
-