diff --git a/packages/gui-plugins/src/file/browser.tsx b/packages/gui-plugins/src/file/browser.tsx index 6616b26e854e..8fd3e2b99d02 100644 --- a/packages/gui-plugins/src/file/browser.tsx +++ b/packages/gui-plugins/src/file/browser.tsx @@ -29,6 +29,7 @@ import { } from "@opencode/plugin/gui" import { current, useShared } from "./context" import SessionFileList, { applyFileListKeyDown } from "./list" +import OpenInAppButton, { useOpenInApp } from "./open-in-app" import { fileTabPath, isFileTab } from "./path" import PierreFileTree from "./pierre-tree" import { @@ -71,6 +72,19 @@ export function SessionFileBrowserTab(props: { const sidebarOpened = () => props.placeholder || props.state.opened() const query = createMemo(() => filter().trim()) + const openIn = ctx.desktop + ? useOpenInApp({ + get session() { + return props.session + }, + path: () => file.root, + }) + : undefined + + // A file tab's header ends with "Open in": the tree's header while the tree is open, else the preview's. + const openInShown = () => !props.mobile && !props.placeholder && !!file.root && !!openIn?.canOpen() + const openInButton = () => + // The filter's search. It asks the server directly: the screen's search reads a failure as no results. const [searchState, setSearchState] = createSignal(INITIAL_FILE_SEARCH_STATE) const searches = new Map }>() @@ -226,6 +240,7 @@ export function SessionFileBrowserTab(props: { {title()}} + toolbarEnd={openInShown() ? openInButton() : undefined} sidebarCollapsed={!sidebarOpened()} sidebarToggleGap={10} sidebarToggle={ @@ -242,6 +257,7 @@ export function SessionFileBrowserTab(props: { open={sidebarOpened()} transition={props.state.transition()} title={{title()}} + actions={openInShown() ? openInButton() : undefined} filter={filter()} onFilterChange={setFilter} onFilterKeyDown={onFilterKeyDown} diff --git a/packages/gui-plugins/src/file/renderer.tsx b/packages/gui-plugins/src/file/renderer.tsx index 1ccb33fb6ca1..7d7837d7ad1a 100644 --- a/packages/gui-plugins/src/file/renderer.tsx +++ b/packages/gui-plugins/src/file/renderer.tsx @@ -9,7 +9,6 @@ import { Suspense, untrack, type ParentProps, - type Signal, } from "solid-js" import { createStore, produce, unwrap } from "solid-js/store" import { Icon } from "@opencode/ui/icon" @@ -20,7 +19,6 @@ import { bindPlugin, LinkHandler, MenuItem, - Slot, Style, usePanel, type Files, @@ -534,21 +532,6 @@ const setup: Plugin.Setup = (ctx) => { import("./open-in-app").then((module) => ({ default: module.OpenInAppMenuItem })), ) - // How many "Open in" buttons each screen's panel headers show; the tab strip shows its own only while none do. - const headers = new WeakMap>() - - const headerButtons = (screen: SessionScreen) => { - const existing = headers.get(screen) - - if (existing) return existing - - const created = createSignal(0) - - headers.set(screen, created) - - return created - } - if (desktop) { onCleanup( onIdle(() => { @@ -566,37 +549,18 @@ const setup: Plugin.Setup = (ctx) => { ), }) - ctx.add(Slot, { - at: "session.panel.end", - render: (input) => ( - - - - - - - - ), - }) } ctx.provide(OpenInApp, { - Button: bindPlugin((props: OpenInAppProps) => { - const count = headerButtons(props.screen) - - count[1]((value) => value + 1) - onCleanup(() => count[1]((value) => value - 1)) - - return ( - - - - - - - - ) - }), + Button: bindPlugin((props: OpenInAppProps) => ( + + + + + + + + )), }) // The review panel lists its changed files with the browser's tree; it renders under this plugin, on the diff --git a/packages/gui-plugins/src/review/panel.tsx b/packages/gui-plugins/src/review/panel.tsx index 1e512f54a310..6cbff3e0b4a6 100644 --- a/packages/gui-plugins/src/review/panel.tsx +++ b/packages/gui-plugins/src/review/panel.tsx @@ -149,6 +149,12 @@ function ReviewPanel(props: ReviewPanelProps) { return live.status === "active" ? live.value : undefined } + const openInButton = () => ( + + {(openIn) => } + + ) + const diffs = () => props.diffs const searching = createMemo(() => props.state.filter().trim().length > 0) @@ -313,11 +319,7 @@ function ReviewPanel(props: ReviewPanelProps) { sidebarToggle={ } - toolbarAction={ - - {(openIn) => } - - } + toolbarAction={openInButton()} sidebar={ // Always mounted: the sidebar header hosts the changes-mode dropdown, // which must stay reachable when the current mode has zero diffs. @@ -334,6 +336,8 @@ function ReviewPanel(props: ReviewPanelProps) { kinds={treeKinds()} folderLabels={props.folderLabels} activeDiff={activeDiff()} + // Without diffs, the preview has no header while the tree is open. + actions={diffs().length === 0 ? openInButton() : undefined} /> } activeFile={activeDiff()} @@ -388,6 +392,7 @@ function ReviewPanelSidebar(props: { kinds: ReturnType folderLabels?: ReadonlyMap activeDiff: string | undefined + actions?: JSX.Element }) { const ctx = usePlugin() @@ -426,6 +431,7 @@ function ReviewPanelSidebar(props: { transition={props.state.sidebar.transition()} title={props.title} stats={} + actions={props.actions} filter={props.state.filter()} onFilterChange={props.state.setFilter} onFilterKeyDown={onFilterKeyDown} diff --git a/packages/plugin/src/gui/file.ts b/packages/plugin/src/gui/file.ts index 8497f1d1f480..aac5247c4729 100644 --- a/packages/plugin/src/gui/file.ts +++ b/packages/plugin/src/gui/file.ts @@ -77,10 +77,10 @@ export interface OpenInAppProps { readonly session: MountedSession } -/** The desktop "Open in" button, for a panel header that shows it in place of the tab strip's. */ +/** The desktop "Open in" button, for a panel header such as the review's. */ export interface OpenInApp { /** - * Renders nothing on the web or for a remote server. While it is mounted, the tab strip leaves out its own. + * Renders nothing on the web or for a remote server. * * @param props - The workspace to open. * @returns The button. diff --git a/packages/session-ui/src/v2/components/session-review-v2.tsx b/packages/session-ui/src/v2/components/session-review-v2.tsx index c6879d438ae2..274ff46a8716 100644 --- a/packages/session-ui/src/v2/components/session-review-v2.tsx +++ b/packages/session-ui/src/v2/components/session-review-v2.tsx @@ -45,6 +45,8 @@ export type SessionReviewV2SidebarProps = { transition: boolean title?: JSX.Element stats?: JSX.Element + /** Controls at the header's end, after the stats. */ + actions?: JSX.Element filter: string onFilterChange: (value: string) => void onFilterKeyDown?: JSX.EventHandlerUnion @@ -80,6 +82,7 @@ export function SessionReviewV2Sidebar(props: SessionReviewV2SidebarProps) { {props.title} {props.stats} + {props.actions}