fix(app): show Open in only in file and review headers (#62174)

This commit is contained in:
Luke Parker 2026-11-05 14:18:02 -06:00 • committed by GitHub
parent 32ab1f3a9e
commit e23d54ca75
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 41 additions and 52 deletions

View file

@ -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 = () => <OpenInAppButton session={props.session} screen={props.screen} />
// The filter's search. It asks the server directly: the screen's search reads a failure as no results.
const [searchState, setSearchState] = createSignal<FileSearchState>(INITIAL_FILE_SEARCH_STATE)
const searches = new Map<number, { controller: AbortController; timer: ReturnType<typeof setTimeout> }>()
@ -226,6 +240,7 @@ export function SessionFileBrowserTab(props: {
<SessionFilePanelV2
toolbar={!props.mobile && !props.placeholder && !sidebarOpened()}
toolbarStart={<span class="truncate font-[530]">{title()}</span>}
toolbarEnd={openInShown() ? openInButton() : undefined}
sidebarCollapsed={!sidebarOpened()}
sidebarToggleGap={10}
sidebarToggle={
@ -242,6 +257,7 @@ export function SessionFileBrowserTab(props: {
open={sidebarOpened()}
transition={props.state.transition()}
title={<span class="truncate font-[530]">{title()}</span>}
actions={openInShown() ? openInButton() : undefined}
filter={filter()}
onFilterChange={setFilter}
onFilterKeyDown={onFilterKeyDown}

View file

@ -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<typeof File> = (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<SessionScreen, Signal<number>>()
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<typeof File> = (ctx) => {
</FileProvider>
),
})
ctx.add(Slot, {
at: "session.panel.end",
render: (input) => (
<Show when={headerButtons(input.screen)[0]() === 0}>
<FileProvider>
<Suspense>
<OpenInAppButton session={input.session} screen={input.screen} />
</Suspense>
</FileProvider>
</Show>
),
})
}
ctx.provide(OpenInApp, {
Button: bindPlugin((props: OpenInAppProps) => {
const count = headerButtons(props.screen)
count[1]((value) => value + 1)
onCleanup(() => count[1]((value) => value - 1))
return (
<Show when={desktop}>
<FileProvider>
<Suspense>
<OpenInAppButton session={props.session} screen={props.screen} />
</Suspense>
</FileProvider>
</Show>
)
}),
Button: bindPlugin((props: OpenInAppProps) => (
<Show when={desktop}>
<FileProvider>
<Suspense>
<OpenInAppButton session={props.session} screen={props.screen} />
</Suspense>
</FileProvider>
</Show>
)),
})
// The review panel lists its changed files with the browser's tree; it renders under this plugin, on the

View file

@ -149,6 +149,12 @@ function ReviewPanel(props: ReviewPanelProps) {
return live.status === "active" ? live.value : undefined
}
const openInButton = () => (
<Show when={openIn()}>
{(openIn) => <Dynamic component={openIn().Button} screen={props.screen} session={props.session} />}
</Show>
)
const diffs = () => props.diffs
const searching = createMemo(() => props.state.filter().trim().length > 0)
@ -313,11 +319,7 @@ function ReviewPanel(props: ReviewPanelProps) {
sidebarToggle={
<SessionReviewV2SidebarToggle opened={props.state.sidebar.opened()} onToggle={props.state.sidebar.toggle} />
}
toolbarAction={
<Show when={openIn()}>
{(openIn) => <Dynamic component={openIn().Button} screen={props.screen} session={props.session} />}
</Show>
}
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<typeof reviewDiffKinds>
folderLabels?: ReadonlyMap<string, string>
activeDiff: string | undefined
actions?: JSX.Element
}) {
const ctx = usePlugin<typeof Review>()
@ -426,6 +431,7 @@ function ReviewPanelSidebar(props: {
transition={props.state.sidebar.transition()}
title={props.title}
stats={<DiffChanges changes={props.diffs} />}
actions={props.actions}
filter={props.state.filter()}
onFilterChange={props.state.setFilter}
onFilterKeyDown={onFilterKeyDown}

View file

@ -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.

View file

@ -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<HTMLInputElement, KeyboardEvent>
@ -80,6 +82,7 @@ export function SessionReviewV2Sidebar(props: SessionReviewV2SidebarProps) {
<div data-slot="session-review-v2-sidebar-header">
<div data-slot="session-review-v2-sidebar-title">{props.title}</div>
{props.stats}
{props.actions}
</div>
<div data-slot="session-review-v2-sidebar-filter">
<TextInput