feat(app): add inline file browser tabs (#35829)
This commit is contained in:
@@ -1019,6 +1019,7 @@ export default function Page() {
|
||||
setActiveMessage,
|
||||
focusInput,
|
||||
review: reviewTab,
|
||||
fileBrowser: () => newSessionDesign() && isDesktop() && !!params.id,
|
||||
})
|
||||
|
||||
const openReviewFile = createOpenReviewFile({
|
||||
@@ -2177,6 +2178,7 @@ export default function Page() {
|
||||
reviewHasFocusableContent={() => hasReview() || reviewV2State.sidebarOpened()}
|
||||
reviewCount={reviewCount}
|
||||
reviewPanel={reviewPanelV2}
|
||||
fileBrowserState={reviewV2State}
|
||||
activeDiff={tree.activeDiff}
|
||||
focusReviewDiff={focusReviewDiff}
|
||||
reviewSnap={ui.reviewSnap}
|
||||
|
||||
@@ -172,6 +172,14 @@ function createScrollSync(input: { tab: () => string; view: ReturnType<typeof us
|
||||
}
|
||||
|
||||
export function FileTabContent(props: { tab: string }) {
|
||||
return (
|
||||
<Tabs.Content value={props.tab}>
|
||||
<SessionFileView tab={props.tab} />
|
||||
</Tabs.Content>
|
||||
)
|
||||
}
|
||||
|
||||
export function SessionFileView(props: { tab: string }) {
|
||||
const file = useFile()
|
||||
const comments = useComments()
|
||||
const language = useLanguage()
|
||||
@@ -439,8 +447,8 @@ export function FileTabContent(props: { tab: string }) {
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<Tabs.Content value={props.tab} class="mt-3 relative h-full">
|
||||
const content = () => (
|
||||
<div class="mt-3 relative h-full min-h-0">
|
||||
<ScrollView class="h-full" viewportRef={scrollSync.setViewport} onScroll={scrollSync.handleScroll as any}>
|
||||
<Switch>
|
||||
<Match when={state()?.loaded}>{renderFile(contents())}</Match>
|
||||
@@ -450,6 +458,8 @@ export function FileTabContent(props: { tab: string }) {
|
||||
<Match when={state()?.error}>{(err) => <div class="px-6 py-4 text-text-weak">{err()}</div>}</Match>
|
||||
</Switch>
|
||||
</ScrollView>
|
||||
</Tabs.Content>
|
||||
</div>
|
||||
)
|
||||
|
||||
return content()
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
|
||||
import { createMemo, createRoot } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import {
|
||||
SESSION_OPEN_FILE_TAB,
|
||||
createOpenReviewFile,
|
||||
createOpenSessionFileTab,
|
||||
createSessionTabs,
|
||||
@@ -165,4 +166,49 @@ describe("createSessionTabs", () => {
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
|
||||
test("exposes the Open File tab without treating it as a file tab", () => {
|
||||
createRoot((dispose) => {
|
||||
const [state] = createStore({
|
||||
active: SESSION_OPEN_FILE_TAB as string | undefined,
|
||||
all: ["file://src/a.ts", SESSION_OPEN_FILE_TAB],
|
||||
})
|
||||
const tabs = createMemo(() => ({ active: () => state.active, all: () => state.all }))
|
||||
const result = createSessionTabs({
|
||||
tabs,
|
||||
pathFromTab: (tab) => (tab.startsWith("file://") ? tab.slice("file://".length) : undefined),
|
||||
normalizeTab: (tab) => tab,
|
||||
fileBrowser: () => true,
|
||||
})
|
||||
|
||||
expect(result.openFileOpen()).toBe(true)
|
||||
expect(result.panelTabs()).toEqual(["file://src/a.ts", SESSION_OPEN_FILE_TAB])
|
||||
expect(result.openedTabs()).toEqual(["file://src/a.ts"])
|
||||
expect(result.activeTab()).toBe(SESSION_OPEN_FILE_TAB)
|
||||
expect(result.activeFileTab()).toBeUndefined()
|
||||
expect(result.closableTab()).toBe(SESSION_OPEN_FILE_TAB)
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
|
||||
test("hides the Open File placeholder when the file browser is unavailable", () => {
|
||||
createRoot((dispose) => {
|
||||
const [state] = createStore({
|
||||
active: SESSION_OPEN_FILE_TAB as string | undefined,
|
||||
all: ["file://src/a.ts", SESSION_OPEN_FILE_TAB],
|
||||
})
|
||||
const tabs = createMemo(() => ({ active: () => state.active, all: () => state.all }))
|
||||
const result = createSessionTabs({
|
||||
tabs,
|
||||
pathFromTab: (tab) => (tab.startsWith("file://") ? tab.slice("file://".length) : undefined),
|
||||
normalizeTab: (tab) => tab,
|
||||
fileBrowser: () => false,
|
||||
})
|
||||
|
||||
expect(result.openFileOpen()).toBe(false)
|
||||
expect(result.panelTabs()).toEqual(["file://src/a.ts"])
|
||||
expect(result.activeTab()).toBe("file://src/a.ts")
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,6 +2,9 @@ import { batch, createMemo, onCleanup, onMount, type Accessor } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import { same } from "@/utils/same"
|
||||
import { SESSION_OPEN_FILE_TAB } from "@/context/layout-tabs"
|
||||
|
||||
export { SESSION_OPEN_FILE_TAB } from "@/context/layout-tabs"
|
||||
|
||||
const emptyTabs: string[] = []
|
||||
|
||||
@@ -16,6 +19,7 @@ type TabsInput = {
|
||||
normalizeTab: (tab: string) => string
|
||||
review?: Accessor<boolean>
|
||||
hasReview?: Accessor<boolean>
|
||||
fileBrowser?: Accessor<boolean>
|
||||
}
|
||||
|
||||
export const getSessionKey = (dir: string | undefined, id: string | undefined) => `${dir ?? ""}${id ? `/${id}` : ""}`
|
||||
@@ -27,8 +31,14 @@ export function shouldShowFileTree(input: { visible: boolean; opened: boolean })
|
||||
export const createSessionTabs = (input: TabsInput) => {
|
||||
const review = input.review ?? (() => false)
|
||||
const hasReview = input.hasReview ?? (() => false)
|
||||
const fileBrowser = input.fileBrowser ?? (() => false)
|
||||
const contextOpen = createMemo(() => input.tabs().active() === "context" || input.tabs().all().includes("context"))
|
||||
const openedTabs = createMemo(
|
||||
const openFileOpen = createMemo(
|
||||
() =>
|
||||
fileBrowser() &&
|
||||
(input.tabs().active() === SESSION_OPEN_FILE_TAB || input.tabs().all().includes(SESSION_OPEN_FILE_TAB)),
|
||||
)
|
||||
const panelTabs = createMemo(
|
||||
() => {
|
||||
const seen = new Set<string>()
|
||||
return input
|
||||
@@ -36,6 +46,7 @@ export const createSessionTabs = (input: TabsInput) => {
|
||||
.all()
|
||||
.flatMap((tab) => {
|
||||
if (tab === "context" || tab === "review") return []
|
||||
if (tab === SESSION_OPEN_FILE_TAB && !fileBrowser()) return []
|
||||
const value = input.pathFromTab(tab) ? input.normalizeTab(tab) : tab
|
||||
if (seen.has(value)) return []
|
||||
seen.add(value)
|
||||
@@ -45,9 +56,13 @@ export const createSessionTabs = (input: TabsInput) => {
|
||||
emptyTabs,
|
||||
{ equals: same },
|
||||
)
|
||||
const openedTabs = createMemo(() => panelTabs().filter((tab) => tab !== SESSION_OPEN_FILE_TAB), emptyTabs, {
|
||||
equals: same,
|
||||
})
|
||||
const activeTab = createMemo(() => {
|
||||
const active = input.tabs().active()
|
||||
if (active === "context") return active
|
||||
if (active === SESSION_OPEN_FILE_TAB && openFileOpen()) return active
|
||||
if (active === "review" && review()) return active
|
||||
if (active && input.pathFromTab(active)) return input.normalizeTab(active)
|
||||
|
||||
@@ -65,12 +80,15 @@ export const createSessionTabs = (input: TabsInput) => {
|
||||
const closableTab = createMemo(() => {
|
||||
const active = activeTab()
|
||||
if (active === "context") return active
|
||||
if (active === SESSION_OPEN_FILE_TAB && openFileOpen()) return active
|
||||
if (!openedTabs().includes(active)) return
|
||||
return active
|
||||
})
|
||||
|
||||
return {
|
||||
contextOpen,
|
||||
openFileOpen,
|
||||
panelTabs,
|
||||
openedTabs,
|
||||
activeTab,
|
||||
activeFileTab,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createStore } from "solid-js/store"
|
||||
import { createMediaQuery } from "@solid-primitives/media"
|
||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||
import { Mark } from "@opencode-ai/ui/logo"
|
||||
@@ -23,10 +24,10 @@ import { useFile, type SelectedLineRange } from "@/context/file"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
|
||||
import { FileTabContent } from "@/pages/session/file-tabs"
|
||||
import {
|
||||
SESSION_OPEN_FILE_TAB,
|
||||
createOpenSessionFileTab,
|
||||
createSessionTabs,
|
||||
getTabReorderIndex,
|
||||
@@ -35,6 +36,7 @@ import {
|
||||
} from "@/pages/session/helpers"
|
||||
import { setSessionHandoff } from "@/pages/session/handoff"
|
||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||
import { SessionFileBrowserTab, type SessionFileBrowserState } from "@/pages/session/v2/session-file-browser-tab"
|
||||
|
||||
type RenderDiff = (SnapshotFileDiff & { file: string }) | VcsFileDiff
|
||||
|
||||
@@ -51,6 +53,7 @@ export function SessionSidePanel(props: {
|
||||
reviewHasFocusableContent: () => boolean
|
||||
reviewCount: () => number
|
||||
reviewPanel: () => JSX.Element
|
||||
fileBrowserState?: SessionFileBrowserState
|
||||
activeDiff?: string
|
||||
focusReviewDiff: (path: string) => void
|
||||
reviewSnap: boolean
|
||||
@@ -59,7 +62,6 @@ export function SessionSidePanel(props: {
|
||||
}) {
|
||||
const layout = useLayout()
|
||||
const settings = useSettings()
|
||||
const sync = useSync()
|
||||
const file = useFile()
|
||||
const language = useLanguage()
|
||||
const command = useCommand()
|
||||
@@ -155,8 +157,10 @@ export function SessionSidePanel(props: {
|
||||
normalizeTab,
|
||||
review: reviewTab,
|
||||
hasReview: props.canReview,
|
||||
fileBrowser: () => !!props.fileBrowserState,
|
||||
})
|
||||
const contextOpen = tabState.contextOpen
|
||||
const panelTabs = tabState.panelTabs
|
||||
const openedTabs = tabState.openedTabs
|
||||
const activeTab = tabState.activeTab
|
||||
const activeFileTab = tabState.activeFileTab
|
||||
@@ -180,6 +184,33 @@ export function SessionSidePanel(props: {
|
||||
const [store, setStore] = createStore({
|
||||
activeDraggable: undefined as string | undefined,
|
||||
})
|
||||
let fileFilter: HTMLInputElement | undefined
|
||||
const temporaryTab = tabs().preview
|
||||
const previewTab = (value: string) => {
|
||||
const next = normalizeTab(value)
|
||||
tabs().previewTab(next)
|
||||
const path = file.pathFromTab(next)
|
||||
if (path) void file.load(path)
|
||||
openReviewPanel()
|
||||
queueMicrotask(() => tabs().setActive(next))
|
||||
}
|
||||
const openFileBrowser = () => {
|
||||
previewTab(SESSION_OPEN_FILE_TAB)
|
||||
queueMicrotask(() => fileFilter?.focus())
|
||||
}
|
||||
const activateTab = (value: string) => {
|
||||
const next = normalizeTab(value)
|
||||
const path = file.pathFromTab(next)
|
||||
if (path) void file.load(path)
|
||||
openReviewPanel()
|
||||
tabs().setActive(next)
|
||||
}
|
||||
const browserTab = createMemo(() => {
|
||||
if (!props.fileBrowserState) return undefined
|
||||
if (activeTab() === SESSION_OPEN_FILE_TAB) return SESSION_OPEN_FILE_TAB
|
||||
return activeFileTab()
|
||||
})
|
||||
const browserKinds = createMemo(() => new Map([...kinds()].filter(([, kind]) => kind !== "mix")))
|
||||
|
||||
const handleDragStart = (event: unknown) => {
|
||||
const id = getDraggableId(event)
|
||||
@@ -265,7 +296,7 @@ export function SessionSidePanel(props: {
|
||||
>
|
||||
<DragDropSensors />
|
||||
<ConstrainDragYAxis />
|
||||
<Tabs value={activeTab()} onChange={openTab}>
|
||||
<Tabs value={activeTab()} onChange={activateTab}>
|
||||
<div class="sticky top-0 shrink-0 flex">
|
||||
<Tabs.List
|
||||
ref={(el: HTMLDivElement) => {
|
||||
@@ -316,7 +347,48 @@ export function SessionSidePanel(props: {
|
||||
</Tabs.Trigger>
|
||||
</Show>
|
||||
<SortableProvider ids={openedTabs()}>
|
||||
<For each={openedTabs()}>{(tab) => <SortableTab tab={tab} onTabClose={tabs().close} />}</For>
|
||||
<For each={panelTabs()}>
|
||||
{(tab) => (
|
||||
<Show
|
||||
when={tab === SESSION_OPEN_FILE_TAB}
|
||||
fallback={
|
||||
<SortableTab
|
||||
tab={tab}
|
||||
temporary={temporaryTab() === tab}
|
||||
onTabClose={tabs().close}
|
||||
onTabDoubleClick={temporaryTab() === tab ? openTab : undefined}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Tabs.Trigger
|
||||
value={SESSION_OPEN_FILE_TAB}
|
||||
closeButton={
|
||||
<TooltipKeybind
|
||||
title={language.t("common.closeTab")}
|
||||
keybind={command.keybind("tab.close")}
|
||||
placement="bottom"
|
||||
gutter={10}
|
||||
>
|
||||
<IconButton
|
||||
icon="close-small"
|
||||
variant="ghost"
|
||||
class="h-5 w-5"
|
||||
onClick={() => tabs().close(SESSION_OPEN_FILE_TAB)}
|
||||
aria-label={language.t("common.closeTab")}
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
}
|
||||
hideCloseButton
|
||||
onMiddleClick={() => tabs().close(SESSION_OPEN_FILE_TAB)}
|
||||
>
|
||||
<div class="flex items-center gap-1.5 italic">
|
||||
<Icon name="open-file" size="small" />
|
||||
<span>{language.t("command.file.open")}</span>
|
||||
</div>
|
||||
</Tabs.Trigger>
|
||||
</Show>
|
||||
)}
|
||||
</For>
|
||||
</SortableProvider>
|
||||
<div class="bg-background-stronger h-full shrink-0 sticky right-0 z-10 flex items-center justify-center pr-3">
|
||||
<TooltipKeybind
|
||||
@@ -330,6 +402,10 @@ export function SessionSidePanel(props: {
|
||||
iconSize="large"
|
||||
class="!rounded-md"
|
||||
onClick={() => {
|
||||
if (props.fileBrowserState) {
|
||||
openFileBrowser()
|
||||
return
|
||||
}
|
||||
void import("@/components/dialog-select-file").then((x) => {
|
||||
dialog.show(() => <x.DialogSelectFile mode="files" onOpenFile={showAllFiles} />)
|
||||
})
|
||||
@@ -380,7 +456,20 @@ export function SessionSidePanel(props: {
|
||||
</Tabs.Content>
|
||||
</Show>
|
||||
|
||||
<Show when={activeFileTab()} keyed>
|
||||
<Show when={browserTab()}>
|
||||
<SessionFileBrowserTab
|
||||
tab={browserTab()!}
|
||||
placeholder={browserTab() === SESSION_OPEN_FILE_TAB}
|
||||
active={file.pathFromTab(browserTab()!)}
|
||||
kinds={browserKinds()}
|
||||
state={props.fileBrowserState!}
|
||||
onSelect={(path) => previewTab(file.tab(path))}
|
||||
onSelectPermanent={(path) => openTab(file.tab(path))}
|
||||
filterRef={(element) => (fileFilter = element)}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={!props.fileBrowserState && activeFileTab()} keyed>
|
||||
{(tab) => <FileTabContent tab={tab} />}
|
||||
</Show>
|
||||
</Tabs>
|
||||
@@ -390,7 +479,9 @@ export function SessionSidePanel(props: {
|
||||
const path = file.pathFromTab(tab)
|
||||
return (
|
||||
<div data-component="tabs-drag-preview">
|
||||
<Show when={path}>{(p) => <FileVisual active path={p()} />}</Show>
|
||||
<Show when={path}>
|
||||
{(p) => <FileVisual active path={p()} temporary={temporaryTab() === tab} />}
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
|
||||
@@ -25,6 +25,7 @@ export type SessionCommandContext = {
|
||||
setActiveMessage: (message: UserMessage | undefined) => void
|
||||
focusInput: () => void
|
||||
review?: () => boolean
|
||||
fileBrowser?: () => boolean
|
||||
}
|
||||
|
||||
const withCategory = (category: string) => {
|
||||
@@ -83,6 +84,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
normalizeTab,
|
||||
review: actions.review,
|
||||
hasReview,
|
||||
fileBrowser: actions.fileBrowser,
|
||||
})
|
||||
const activeFileTab = tabState.activeFileTab
|
||||
const closableTab = tabState.closableTab
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import { createMemo, createSignal, createUniqueId, Show } from "solid-js"
|
||||
import { createQuery } from "@tanstack/solid-query"
|
||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import {
|
||||
SessionFilePanelV2,
|
||||
SessionFilePanelV2Empty,
|
||||
SessionFilePanelV2Title,
|
||||
} from "@opencode-ai/session-ui/v2/session-file-panel-v2"
|
||||
import { SessionReviewV2Sidebar, SessionReviewV2SidebarToggle } from "@opencode-ai/session-ui/v2/session-review-v2"
|
||||
import FileTree, { type Kind } from "@/components/file-tree"
|
||||
import { useFile } from "@/context/file"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { displayName } from "@/pages/layout/helpers"
|
||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||
import { SessionFileView } from "@/pages/session/file-tabs"
|
||||
import { applyFileListKeyDown, SessionFileListV2 } from "@/pages/session/v2/session-file-list-v2"
|
||||
import { pathKey } from "@/utils/path-key"
|
||||
|
||||
const emptyFiles: string[] = []
|
||||
|
||||
export type SessionFileBrowserState = {
|
||||
sidebarOpened: () => boolean
|
||||
sidebarWidth: () => number
|
||||
resizeSidebar: (width: number) => void
|
||||
toggleSidebar: () => void
|
||||
}
|
||||
|
||||
export function SessionFileBrowserTab(props: {
|
||||
tab: string
|
||||
placeholder: boolean
|
||||
active?: string
|
||||
kinds: ReadonlyMap<string, Kind>
|
||||
state: SessionFileBrowserState
|
||||
onSelect: (path: string) => void
|
||||
onSelectPermanent: (path: string) => void
|
||||
filterRef?: (element: HTMLInputElement) => void
|
||||
}) {
|
||||
const file = useFile()
|
||||
const language = useLanguage()
|
||||
const layout = useLayout()
|
||||
const sdk = useSDK()
|
||||
const { workspaceKey } = useSessionLayout()
|
||||
const resultsID = `session-file-browser-results-${createUniqueId()}`
|
||||
const [filter, setFilter] = createSignal("")
|
||||
const [explicitHighlight, setExplicitHighlight] = createSignal<string>()
|
||||
const query = createMemo(() => filter().trim())
|
||||
const search = createQuery(() => {
|
||||
const value = query()
|
||||
return {
|
||||
queryKey: ["session-open-file", workspaceKey(), value] as const,
|
||||
enabled: value.length > 0,
|
||||
queryFn: ({ signal }) => file.searchFiles(value, { limit: 200, signal }),
|
||||
}
|
||||
})
|
||||
const files = createMemo(() => {
|
||||
if (!query() || search.isPending) return emptyFiles
|
||||
return [...new Set(search.data ?? emptyFiles)]
|
||||
})
|
||||
const highlighted = createMemo(() => {
|
||||
const values = files()
|
||||
if (values.length === 0) return undefined
|
||||
const explicit = explicitHighlight()
|
||||
if (explicit && values.includes(explicit)) return explicit
|
||||
return values[0]
|
||||
})
|
||||
const loading = createMemo(() => query().length > 0 && search.isPending)
|
||||
const project = createMemo(() => {
|
||||
const directory = pathKey(sdk().directory)
|
||||
return layout.projects
|
||||
.list()
|
||||
.find(
|
||||
(item) =>
|
||||
pathKey(item.worktree) === directory || item.sandboxes?.some((sandbox) => pathKey(sandbox) === directory),
|
||||
)
|
||||
})
|
||||
const title = createMemo(() => displayName(project() ?? { worktree: sdk().directory }))
|
||||
const optionID = (path: string) => `${resultsID}-option-${files().indexOf(path)}`
|
||||
|
||||
const onFilterKeyDown = (event: KeyboardEvent & { currentTarget: HTMLInputElement }) => {
|
||||
if (event.key === "Escape" && query()) {
|
||||
event.preventDefault()
|
||||
setFilter("")
|
||||
return
|
||||
}
|
||||
if (!query()) return
|
||||
applyFileListKeyDown(event, files(), highlighted(), {
|
||||
onHighlight: setExplicitHighlight,
|
||||
onSelect: props.onSelectPermanent,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs.Content value={props.tab} class="h-full min-h-0 overflow-hidden">
|
||||
<SessionFilePanelV2
|
||||
toolbar
|
||||
toolbarStart={
|
||||
<>
|
||||
<SessionReviewV2SidebarToggle opened={props.state.sidebarOpened()} onToggle={props.state.toggleSidebar} />
|
||||
<Show when={!props.state.sidebarOpened()}>
|
||||
<SessionFilePanelV2Title>{title()}</SessionFilePanelV2Title>
|
||||
</Show>
|
||||
</>
|
||||
}
|
||||
sidebar={
|
||||
<SessionReviewV2Sidebar
|
||||
open={props.state.sidebarOpened()}
|
||||
title={<span class="truncate">{title()}</span>}
|
||||
filter={filter()}
|
||||
onFilterChange={setFilter}
|
||||
onFilterKeyDown={onFilterKeyDown}
|
||||
filterAutofocus={props.placeholder}
|
||||
filterRef={props.filterRef}
|
||||
filterControls={resultsID}
|
||||
filterActiveDescendant={highlighted() ? optionID(highlighted()!) : undefined}
|
||||
filterExpanded={query().length > 0 && files().length > 0}
|
||||
width={props.state.sidebarWidth()}
|
||||
onWidthChange={props.state.resizeSidebar}
|
||||
>
|
||||
<Show
|
||||
when={query()}
|
||||
fallback={
|
||||
<FileTree
|
||||
path=""
|
||||
class="pt-1"
|
||||
active={props.active}
|
||||
kinds={props.kinds}
|
||||
onFileClick={(node) => props.onSelect(node.path)}
|
||||
onFileDoubleClick={(node) => props.onSelectPermanent(node.path)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Show
|
||||
when={!loading()}
|
||||
fallback={
|
||||
<div role="status" class="px-2 py-2 text-12-regular text-text-weak">
|
||||
{language.t("common.loading")}
|
||||
{language.t("common.loading.ellipsis")}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Show
|
||||
when={files().length > 0}
|
||||
fallback={
|
||||
<div role="status" class="px-2 py-2 text-12-regular text-text-weak">
|
||||
{language.t("palette.empty")}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<SessionFileListV2
|
||||
id={resultsID}
|
||||
role="listbox"
|
||||
optionID={optionID}
|
||||
files={files()}
|
||||
kinds={props.kinds}
|
||||
active={props.active}
|
||||
highlighted={highlighted()}
|
||||
onFileClick={(path) => {
|
||||
setExplicitHighlight(path)
|
||||
props.onSelect(path)
|
||||
}}
|
||||
onFileDoubleClick={props.onSelectPermanent}
|
||||
/>
|
||||
</Show>
|
||||
</Show>
|
||||
</Show>
|
||||
</SessionReviewV2Sidebar>
|
||||
}
|
||||
>
|
||||
<Show
|
||||
when={!props.placeholder}
|
||||
fallback={
|
||||
<SessionFilePanelV2Empty>
|
||||
<div class="flex flex-col items-center gap-3 text-center text-text-weak">
|
||||
<Icon name="file-tree" size="large" />
|
||||
<div class="text-14-medium text-text-strong">{language.t("command.file.open")}</div>
|
||||
<div class="text-13-regular">{language.t("session.files.selectToOpen")}</div>
|
||||
</div>
|
||||
</SessionFilePanelV2Empty>
|
||||
}
|
||||
>
|
||||
<div class="min-h-0 flex-1">
|
||||
<Show when={props.tab} keyed>
|
||||
{(tab) => <SessionFileView tab={tab} />}
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</SessionFilePanelV2>
|
||||
</Tabs.Content>
|
||||
)
|
||||
}
|
||||
@@ -43,7 +43,11 @@ export function SessionFileListV2(props: {
|
||||
active?: string
|
||||
highlighted?: string
|
||||
kinds?: ReadonlyMap<string, Kind>
|
||||
id?: string
|
||||
role?: "listbox"
|
||||
optionID?: (path: string) => string
|
||||
onFileClick: (path: string) => void
|
||||
onFileDoubleClick?: (path: string) => void
|
||||
}) {
|
||||
const active = () => normalizePath(props.active ?? "")
|
||||
const highlighted = () => normalizePath(props.highlighted ?? "")
|
||||
@@ -88,6 +92,8 @@ export function SessionFileListV2(props: {
|
||||
return (
|
||||
<div
|
||||
ref={setRoot}
|
||||
id={props.id}
|
||||
role={props.role}
|
||||
data-component="file-tree-v2"
|
||||
data-total-rows={props.files.length}
|
||||
style={{ position: "relative", height: `${virtualizer.getTotalSize()}px` }}
|
||||
@@ -116,6 +122,9 @@ export function SessionFileListV2(props: {
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
id={props.optionID?.(path)}
|
||||
role={props.role ? "option" : undefined}
|
||||
aria-selected={props.role ? selected() : undefined}
|
||||
data-slot="file-tree-v2-row"
|
||||
data-path={path}
|
||||
data-selected={selected() ? "" : undefined}
|
||||
@@ -124,6 +133,7 @@ export function SessionFileListV2(props: {
|
||||
onFocus={() => setFocused(path)}
|
||||
onBlur={() => setFocused(undefined)}
|
||||
onClick={() => props.onFileClick(path)}
|
||||
onDblClick={() => props.onFileDoubleClick?.(path)}
|
||||
>
|
||||
<span class="filetree-iconpair size-4">
|
||||
<FileIcon node={{ path, type: "file" }} class="size-4 filetree-icon filetree-icon--color" />
|
||||
|
||||
Reference in New Issue
Block a user