diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index 54ed48a9d..5c58725c7 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -36,7 +36,6 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil const filesOnly = () => props.mode === "files" const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const tabs = createMemo(() => layout.tabs(sessionKey)) - const view = createMemo(() => layout.view(sessionKey)) const state = { cleanup: undefined as (() => void) | void, committed: false } const [grouped, setGrouped] = createSignal(false) const common = [ @@ -45,7 +44,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil "session.previous", "session.next", "terminal.toggle", - "review.toggle", + "fileTree.toggle", ] const limit = 5 @@ -163,7 +162,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil const value = file.tab(path) tabs().open(value) file.load(path) - view().reviewPanel.open() + layout.fileTree.setTab("all") props.onOpenFile?.(path) } diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index c736ef0f1..1bd7aa4eb 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -171,7 +171,6 @@ export const PromptInput: Component = (props) => { const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const tabs = createMemo(() => layout.tabs(sessionKey)) - const view = createMemo(() => layout.view(sessionKey)) const commentInReview = (path: string) => { const sessionID = params.id @@ -187,20 +186,15 @@ export const PromptInput: Component = (props) => { const focus = { file: item.path, id: item.commentID } comments.setActive(focus) - view().reviewPanel.open() - if (item.commentOrigin === "review") { - tabs().open("review") - requestAnimationFrame(() => comments.setFocus(focus)) - return - } - - if (item.commentOrigin !== "file" && commentInReview(item.path)) { - tabs().open("review") + const wantsReview = item.commentOrigin === "review" || (item.commentOrigin !== "file" && commentInReview(item.path)) + if (wantsReview) { + layout.fileTree.setTab("changes") requestAnimationFrame(() => comments.setFocus(focus)) return } + layout.fileTree.setTab("all") const tab = files.tab(item.path) tabs().open(tab) files.load(item.path) diff --git a/packages/app/src/components/session-context-usage.tsx b/packages/app/src/components/session-context-usage.tsx index 4dbb9e048..afdb18bb0 100644 --- a/packages/app/src/components/session-context-usage.tsx +++ b/packages/app/src/components/session-context-usage.tsx @@ -23,7 +23,6 @@ export function SessionContextUsage(props: SessionContextUsageProps) { const variant = createMemo(() => props.variant ?? "button") const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const tabs = createMemo(() => layout.tabs(sessionKey)) - const view = createMemo(() => layout.view(sessionKey)) const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : [])) const usd = createMemo( @@ -58,7 +57,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) { const openContext = () => { if (!params.id) return - view().reviewPanel.open() + layout.fileTree.setTab("all") tabs().open("context") tabs().setActive("context") } diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index db43b5eaf..9fddb4507 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -45,7 +45,6 @@ export function SessionHeader() { const currentSession = createMemo(() => sync.data.session.find((s) => s.id === params.id)) const shareEnabled = createMemo(() => sync.data.config.share !== "disabled") const showShare = createMemo(() => shareEnabled() && !!currentSession()) - const showReview = createMemo(() => !!currentSession()) const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const view = createMemo(() => layout.view(sessionKey)) @@ -281,62 +280,38 @@ export function SessionHeader() { - )} diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index 414c3d6f1..d30fd11cf 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -51,16 +51,37 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( const migrate = (value: unknown) => { if (!isRecord(value)) return value + const sidebar = value.sidebar - if (!isRecord(sidebar)) return value - if (typeof sidebar.workspaces !== "boolean") return value - return { - ...value, - sidebar: { + const migratedSidebar = (() => { + if (!isRecord(sidebar)) return sidebar + if (typeof sidebar.workspaces !== "boolean") return sidebar + return { ...sidebar, workspaces: {}, workspacesDefault: sidebar.workspaces, - }, + } + })() + + const fileTree = value.fileTree + const migratedFileTree = (() => { + if (!isRecord(fileTree)) return fileTree + if (fileTree.tab === "changes" || fileTree.tab === "all") return fileTree + + const width = typeof fileTree.width === "number" ? fileTree.width : 344 + return { + ...fileTree, + opened: true, + width: width === 260 ? 344 : width, + tab: "changes", + } + })() + + if (migratedSidebar === sidebar && migratedFileTree === fileTree) return value + return { + ...value, + sidebar: migratedSidebar, + fileTree: migratedFileTree, } } @@ -80,11 +101,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }, review: { diffStyle: "split" as ReviewDiffStyle, - panelOpened: true, }, fileTree: { - opened: false, - width: 260, + opened: true, + width: 344, + tab: "changes" as "changes" | "all", }, session: { width: 600, @@ -454,32 +475,40 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }, }, fileTree: { - opened: createMemo(() => store.fileTree?.opened ?? false), - width: createMemo(() => store.fileTree?.width ?? 260), + opened: createMemo(() => store.fileTree?.opened ?? true), + width: createMemo(() => store.fileTree?.width ?? 344), + tab: createMemo(() => store.fileTree?.tab ?? "changes"), + setTab(tab: "changes" | "all") { + if (!store.fileTree) { + setStore("fileTree", { opened: true, width: 344, tab }) + return + } + setStore("fileTree", "tab", tab) + }, open() { if (!store.fileTree) { - setStore("fileTree", { opened: true, width: 260 }) + setStore("fileTree", { opened: true, width: 344, tab: "changes" }) return } setStore("fileTree", "opened", true) }, close() { if (!store.fileTree) { - setStore("fileTree", { opened: false, width: 260 }) + setStore("fileTree", { opened: false, width: 344, tab: "changes" }) return } setStore("fileTree", "opened", false) }, toggle() { if (!store.fileTree) { - setStore("fileTree", { opened: true, width: 260 }) + setStore("fileTree", { opened: true, width: 344, tab: "changes" }) return } setStore("fileTree", "opened", (x) => !x) }, resize(width: number) { if (!store.fileTree) { - setStore("fileTree", { opened: true, width }) + setStore("fileTree", { opened: true, width, tab: "changes" }) return } setStore("fileTree", "width", width) @@ -526,7 +555,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( const s = createMemo(() => store.sessionView[key()] ?? { scroll: {} }) const terminalOpened = createMemo(() => store.terminal?.opened ?? false) - const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? true) function setTerminalOpened(next: boolean) { const current = store.terminal @@ -540,18 +568,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( setStore("terminal", "opened", next) } - function setReviewPanelOpened(next: boolean) { - const current = store.review - if (!current) { - setStore("review", { diffStyle: "split" as ReviewDiffStyle, panelOpened: next }) - return - } - - const value = current.panelOpened ?? true - if (value === next) return - setStore("review", "panelOpened", next) - } - return { scroll(tab: string) { return scroll.scroll(key(), tab) @@ -571,18 +587,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( setTerminalOpened(!terminalOpened()) }, }, - reviewPanel: { - opened: reviewPanelOpened, - open() { - setReviewPanelOpened(true) - }, - close() { - setReviewPanelOpened(false) - }, - toggle() { - setReviewPanelOpened(!reviewPanelOpened()) - }, - }, review: { open: createMemo(() => s().reviewOpen), setOpen(open: string[]) { @@ -620,10 +624,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( const tabs = createMemo(() => store.sessionTabs[key()] ?? { all: [] }) return { tabs, - active: createMemo(() => tabs().active), - all: createMemo(() => tabs().all), + active: createMemo(() => (tabs().active === "review" ? undefined : tabs().active)), + all: createMemo(() => tabs().all.filter((tab) => tab !== "review")), setActive(tab: string | undefined) { const session = key() + if (tab === "review") return if (!store.sessionTabs[session]) { setStore("sessionTabs", session, { all: [], active: tab }) } else { @@ -632,25 +637,18 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }, setAll(all: string[]) { const session = key() + const next = all.filter((tab) => tab !== "review") if (!store.sessionTabs[session]) { - setStore("sessionTabs", session, { all, active: undefined }) + setStore("sessionTabs", session, { all: next, active: undefined }) } else { - setStore("sessionTabs", session, "all", all) + setStore("sessionTabs", session, "all", next) } }, async open(tab: string) { + if (tab === "review") return const session = key() const current = store.sessionTabs[session] ?? { all: [] } - if (tab === "review") { - if (!store.sessionTabs[session]) { - setStore("sessionTabs", session, { all: [], active: tab }) - return - } - setStore("sessionTabs", session, "active", tab) - return - } - if (tab === "context") { const all = [tab, ...current.all.filter((x) => x !== tab)] if (!store.sessionTabs[session]) { diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index a75b1169a..146450293 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -23,7 +23,6 @@ import { IconButton } from "@opencode-ai/ui/icon-button" import { Button } from "@opencode-ai/ui/button" import { Icon } from "@opencode-ai/ui/icon" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" -import { DiffChanges } from "@opencode-ai/ui/diff-changes" import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { Tabs } from "@opencode-ai/ui/tabs" import { useCodeComponent } from "@opencode-ai/ui/context/code" @@ -433,7 +432,7 @@ export default function Page() { expanded: {} as Record, messageId: undefined as string | undefined, turnStart: 0, - mobileTab: "session" as "session" | "review", + mobileTab: "session" as "session" | "changes", newSessionWorktree: "main", promptHeight: 0, }) @@ -693,12 +692,12 @@ export default function Page() { onSelect: () => view().terminal.toggle(), }, { - id: "review.toggle", - title: language.t("command.review.toggle"), + id: "fileTree.toggle", + title: language.t("command.fileTree.toggle"), description: "", category: language.t("command.category.view"), keybind: "mod+shift+r", - onSelect: () => view().reviewPanel.toggle(), + onSelect: () => layout.fileTree.toggle(), }, { id: "terminal.new", @@ -822,7 +821,7 @@ export default function Page() { const sessionID = params.id if (!sessionID) return if (status()?.type !== "idle") { - await sdk.client.session.abort({ sessionID }).catch(() => {}) + await sdk.client.session.abort({ sessionID }).catch(() => { }) } const revert = info()?.revert?.messageID // Find the last user message that's not already reverted @@ -905,69 +904,69 @@ export default function Page() { }, ...(sync.data.config.share !== "disabled" ? [ - { - id: "session.share", - title: language.t("command.session.share"), - description: language.t("command.session.share.description"), - category: language.t("command.category.session"), - slash: "share", - disabled: !params.id || !!info()?.share?.url, - onSelect: async () => { - if (!params.id) return - await sdk.client.session - .share({ sessionID: params.id }) - .then((res) => { - navigator.clipboard.writeText(res.data!.share!.url).catch(() => - showToast({ - title: language.t("toast.session.share.copyFailed.title"), - variant: "error", - }), - ) - }) - .then(() => + { + id: "session.share", + title: language.t("command.session.share"), + description: language.t("command.session.share.description"), + category: language.t("command.category.session"), + slash: "share", + disabled: !params.id || !!info()?.share?.url, + onSelect: async () => { + if (!params.id) return + await sdk.client.session + .share({ sessionID: params.id }) + .then((res) => { + navigator.clipboard.writeText(res.data!.share!.url).catch(() => showToast({ - title: language.t("toast.session.share.success.title"), - description: language.t("toast.session.share.success.description"), - variant: "success", - }), - ) - .catch(() => - showToast({ - title: language.t("toast.session.share.failed.title"), - description: language.t("toast.session.share.failed.description"), + title: language.t("toast.session.share.copyFailed.title"), variant: "error", }), ) - }, + }) + .then(() => + showToast({ + title: language.t("toast.session.share.success.title"), + description: language.t("toast.session.share.success.description"), + variant: "success", + }), + ) + .catch(() => + showToast({ + title: language.t("toast.session.share.failed.title"), + description: language.t("toast.session.share.failed.description"), + variant: "error", + }), + ) }, - { - id: "session.unshare", - title: language.t("command.session.unshare"), - description: language.t("command.session.unshare.description"), - category: language.t("command.category.session"), - slash: "unshare", - disabled: !params.id || !info()?.share?.url, - onSelect: async () => { - if (!params.id) return - await sdk.client.session - .unshare({ sessionID: params.id }) - .then(() => - showToast({ - title: language.t("toast.session.unshare.success.title"), - description: language.t("toast.session.unshare.success.description"), - variant: "success", - }), - ) - .catch(() => - showToast({ - title: language.t("toast.session.unshare.failed.title"), - description: language.t("toast.session.unshare.failed.description"), - variant: "error", - }), - ) - }, + }, + { + id: "session.unshare", + title: language.t("command.session.unshare"), + description: language.t("command.session.unshare.description"), + category: language.t("command.category.session"), + slash: "unshare", + disabled: !params.id || !info()?.share?.url, + onSelect: async () => { + if (!params.id) return + await sdk.client.session + .unshare({ sessionID: params.id }) + .then(() => + showToast({ + title: language.t("toast.session.unshare.success.title"), + description: language.t("toast.session.unshare.success.description"), + variant: "success", + }), + ) + .catch(() => + showToast({ + title: language.t("toast.session.unshare.failed.title"), + description: language.t("toast.session.unshare.failed.description"), + variant: "error", + }), + ) }, - ] + }, + ] : []), ]) @@ -1067,40 +1066,31 @@ export default function Page() { .filter((tab) => tab !== "context"), ) - const mobileReview = createMemo(() => !isDesktop() && view().reviewPanel.opened() && store.mobileTab === "review") + const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes") - const showTabs = createMemo(() => view().reviewPanel.opened()) + const fileTreeTab = () => layout.fileTree.tab() + const setFileTreeTab = (value: "changes" | "all") => layout.fileTree.setTab(value) const [tree, setTree] = createStore({ - fileTreeTab: "changes" as "changes" | "all", reviewScroll: undefined as HTMLDivElement | undefined, pendingDiff: undefined as string | undefined, }) - const fileTreeTab = () => tree.fileTreeTab - const setFileTreeTab = (value: "changes" | "all") => setTree("fileTreeTab", value) const reviewScroll = () => tree.reviewScroll const setReviewScroll = (value: HTMLDivElement | undefined) => setTree("reviewScroll", value) const pendingDiff = () => tree.pendingDiff const setPendingDiff = (value: string | undefined) => setTree("pendingDiff", value) const showAllFiles = () => { - if (!layout.fileTree.opened()) return if (fileTreeTab() !== "changes") return setFileTreeTab("all") } - createEffect(() => { - if (!layout.fileTree.opened()) return - setFileTreeTab("changes") - }) - createEffect( on( () => tabs().active(), (active) => { if (!active) return - if (!layout.fileTree.opened()) return if (fileTreeTab() !== "changes") return if (!file.pathFromTab(active)) return showAllFiles() @@ -1197,49 +1187,30 @@ export default function Page() { const activeTab = createMemo(() => { const active = tabs().active() - if (layout.fileTree.opened() && fileTreeTab() === "all") { - if (active && active !== "review" && active !== "context") return normalizeTab(active) - - const first = openedTabs()[0] - if (first) return first - return "review" - } - if (active) return normalizeTab(active) - if (hasReview()) return "review" + if (active === "context") return "context" + if (active && file.pathFromTab(active)) return normalizeTab(active) const first = openedTabs()[0] if (first) return first if (contextOpen()) return "context" - return "review" + return "empty" }) createEffect(() => { if (!layout.ready()) return if (tabs().active()) return - if (!hasReview() && openedTabs().length === 0 && !contextOpen()) return - tabs().setActive(activeTab()) - }) + if (openedTabs().length === 0 && !contextOpen()) return - createEffect(() => { - if (!layout.fileTree.opened()) return - if (fileTreeTab() !== "all") return - - const first = openedTabs()[0] - if (!first) return - - const active = tabs().active() - if (active && active !== "review" && active !== "context") return - tabs().setActive(first) + const next = activeTab() + if (next === "empty") return + tabs().setActive(next) }) createEffect(() => { const id = params.id if (!id) return - const wants = isDesktop() - ? view().reviewPanel.opened() && - (layout.fileTree.opened() ? fileTreeTab() === "changes" : activeTab() === "review") - : view().reviewPanel.opened() && store.mobileTab === "review" + const wants = isDesktop() ? fileTreeTab() === "changes" : store.mobileTab === "changes" if (!wants) return if (sync.data.session_diff[id] !== undefined) return @@ -1654,8 +1625,8 @@ export default function Page() {
- {/* Mobile tab bar - only shown on mobile when user opened review */} - + {/* Mobile tab bar */} + setStore("mobileTab", "review")} + onClick={() => setStore("mobileTab", "changes")} > {language.t("session.review.filesChanged", { count: reviewCount() })} - {language.t("session.tab.review")} + {language.t("session.review.change.other")} @@ -1690,7 +1661,7 @@ export default function Page() { "flex-1 md:flex-none pt-6 md:pt-3": true, }} style={{ - width: isDesktop() && showTabs() ? `${layout.session.width()}px` : "100%", + width: isDesktop() ? `${layout.session.width()}px` : "100%", "--prompt-height": store.promptHeight ? `${store.promptHeight}px` : undefined, }} > @@ -1699,7 +1670,7 @@ export default function Page() { @@ -1789,7 +1760,6 @@ export default function Page() { "sticky top-0 z-30 bg-background-stronger": true, "w-full": true, "px-4 md:px-6": true, - "md:max-w-200 md:mx-auto": !showTabs(), }} >
@@ -1814,13 +1784,7 @@ export default function Page() {
0}>
@@ -1868,10 +1832,7 @@ export default function Page() {
(promptDock = el)} class="absolute inset-x-0 bottom-0 pt-12 pb-4 flex flex-col justify-center items-center z-50 px-4 md:px-0 bg-gradient-to-t from-background-stronger via-background-stronger to-transparent pointer-events-none" > -
+
{(perm) => (
@@ -2000,7 +1956,7 @@ export default function Page() {
- +
- {/* Desktop tabs panel (Review + Context + Files) - hidden on mobile */} - + {/* Desktop side panel - hidden on mobile */} +