|
|
@@ -1,28 +1,46 @@
|
|
|
import { For, Match, Show, Switch, createEffect, createMemo, onCleanup, type JSX } from "solid-js"
|
|
|
import { createStore } from "solid-js/store"
|
|
|
import { createMediaQuery } from "@solid-primitives/media"
|
|
|
+import { DragDropProvider as DndKitProvider, PointerSensor } from "@dnd-kit/solid"
|
|
|
+import { isSortable } from "@dnd-kit/solid/sortable"
|
|
|
+import { Accessibility, AutoScroller, Feedback, PointerActivationConstraints } from "@dnd-kit/dom"
|
|
|
+import { RestrictToHorizontalAxis } from "@dnd-kit/abstract/modifiers"
|
|
|
+import { RestrictToElement } from "@dnd-kit/dom/modifiers"
|
|
|
+import {
|
|
|
+ DragDropProvider,
|
|
|
+ DragDropSensors,
|
|
|
+ DragOverlay,
|
|
|
+ SortableProvider,
|
|
|
+ closestCenter,
|
|
|
+ type DragEvent,
|
|
|
+} from "@thisbeyond/solid-dnd"
|
|
|
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"
|
|
|
-import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
|
|
|
-import type { DragEvent } from "@thisbeyond/solid-dnd"
|
|
|
+import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
|
|
+import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
|
|
|
+import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
|
|
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
|
|
|
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
|
|
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|
|
|
|
|
import FileTree from "@/components/file-tree"
|
|
|
+import { normalizeFileTreeV2Path } from "@/components/file-tree-v2-model"
|
|
|
import { SessionContextUsage } from "@/components/session-context-usage"
|
|
|
|
|
|
const reviewTabID = "session-side-panel-review-tab"
|
|
|
const reviewTabPanelID = "session-side-panel-review-tabpanel"
|
|
|
-import { SessionContextTab, SortableTab, FileVisual } from "@/components/session"
|
|
|
+const fileBrowserTabPanelID = "session-side-panel-file-browser-tabpanel"
|
|
|
+import { SessionContextTab, SortableTab, SortableTabV2, FileVisual } from "@/components/session"
|
|
|
+import { OpenInAppV2 } from "@/components/session/open-in-app-v2"
|
|
|
import { useCommand } from "@/context/command"
|
|
|
import { useFile, type SelectedLineRange } from "@/context/file"
|
|
|
import { useLanguage } from "@/context/language"
|
|
|
import { useLayout } from "@/context/layout"
|
|
|
+import { useSDK } from "@/context/sdk"
|
|
|
import { useSettings } from "@/context/settings"
|
|
|
import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
|
|
|
import { FileTabContent } from "@/pages/session/file-tabs"
|
|
|
@@ -53,6 +71,7 @@ export function SessionSidePanel(props: {
|
|
|
reviewHasFocusableContent: () => boolean
|
|
|
reviewCount: () => number
|
|
|
reviewPanel: () => JSX.Element
|
|
|
+ reviewSidebarToggle?: (disabled: boolean) => JSX.Element
|
|
|
fileBrowserState?: SessionFileBrowserState
|
|
|
activeDiff?: string
|
|
|
focusReviewDiff: (path: string) => void
|
|
|
@@ -66,7 +85,9 @@ export function SessionSidePanel(props: {
|
|
|
const language = useLanguage()
|
|
|
const command = useCommand()
|
|
|
const dialog = useDialog()
|
|
|
+ const sdk = useSDK()
|
|
|
const { sessionKey, tabs, view, params } = useSessionLayout()
|
|
|
+ const projectDirectory = createMemo(() => sdk().directory)
|
|
|
|
|
|
const isDesktop = createMediaQuery("(min-width: 768px)")
|
|
|
const shown = settings.visibility.fileTree
|
|
|
@@ -98,11 +119,9 @@ export function SessionSidePanel(props: {
|
|
|
return "mix" as const
|
|
|
}
|
|
|
|
|
|
- const normalize = (p: string) => p.replaceAll("\\\\", "/").replace(/\/+$/, "")
|
|
|
-
|
|
|
const out = new Map<string, "add" | "del" | "mix">()
|
|
|
for (const diff of diffs()) {
|
|
|
- const file = normalize(diff.file)
|
|
|
+ const file = normalizeFileTreeV2Path(diff.file)
|
|
|
const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix"
|
|
|
|
|
|
out.set(file, kind)
|
|
|
@@ -159,6 +178,7 @@ export function SessionSidePanel(props: {
|
|
|
fileBrowser: () => !!props.fileBrowserState,
|
|
|
})
|
|
|
const contextOpen = tabState.contextOpen
|
|
|
+ const openFileOpen = tabState.openFileOpen
|
|
|
const panelTabs = tabState.panelTabs
|
|
|
const openedTabs = tabState.openedTabs
|
|
|
const activeTab = tabState.activeTab
|
|
|
@@ -176,10 +196,8 @@ export function SessionSidePanel(props: {
|
|
|
layout.fileTree.setTab("all")
|
|
|
}
|
|
|
|
|
|
- const [store, setStore] = createStore({
|
|
|
- activeDraggable: undefined as string | undefined,
|
|
|
- })
|
|
|
let fileFilter: HTMLInputElement | undefined
|
|
|
+ let tabList: HTMLDivElement | undefined
|
|
|
const temporaryTab = tabs().preview
|
|
|
const previewTab = (value: string) => {
|
|
|
const next = normalizeTab(value)
|
|
|
@@ -202,10 +220,26 @@ export function SessionSidePanel(props: {
|
|
|
}
|
|
|
const browserTab = createMemo(() => {
|
|
|
if (!props.fileBrowserState) return undefined
|
|
|
- if (activeTab() === SESSION_OPEN_FILE_TAB) return SESSION_OPEN_FILE_TAB
|
|
|
+ const active = activeTab()
|
|
|
+ if (active === SESSION_OPEN_FILE_TAB) return SESSION_OPEN_FILE_TAB
|
|
|
+ if (active && file.pathFromTab(active)) return active
|
|
|
return activeFileTab()
|
|
|
})
|
|
|
- const browserKinds = createMemo(() => new Map([...kinds()].filter(([, kind]) => kind !== "mix")))
|
|
|
+ // Keep the file-browser shell mounted while any file tab exists. Kobalte briefly
|
|
|
+ // selects Review while the tab For replaces a preview trigger, which would
|
|
|
+ // otherwise dispose the sidebar and reset scroll.
|
|
|
+ const fileBrowserMounted = createMemo(() => {
|
|
|
+ if (!props.fileBrowserState) return false
|
|
|
+ return openedTabs().length > 0 || openFileOpen() || !!browserTab()
|
|
|
+ })
|
|
|
+ const fileBrowserVisible = createMemo(() => {
|
|
|
+ const active = activeTab()
|
|
|
+ return active !== "review" && active !== "context" && active !== "empty"
|
|
|
+ })
|
|
|
+ const openFileKeybind = createMemo(() => command.keybindParts("file.open"))
|
|
|
+ const [store, setStore] = createStore({
|
|
|
+ activeDraggable: undefined as string | undefined,
|
|
|
+ })
|
|
|
|
|
|
const handleDragStart = (event: unknown) => {
|
|
|
const id = getDraggableId(event)
|
|
|
@@ -291,72 +325,283 @@ export function SessionSidePanel(props: {
|
|
|
"bg-background-base": !settings.general.newLayoutDesigns(),
|
|
|
}}
|
|
|
>
|
|
|
- <DragDropProvider
|
|
|
- onDragStart={handleDragStart}
|
|
|
- onDragEnd={handleDragEnd}
|
|
|
- onDragOver={handleDragOver}
|
|
|
- collisionDetector={closestCenter}
|
|
|
- >
|
|
|
- <DragDropSensors />
|
|
|
- <ConstrainDragYAxis />
|
|
|
- <Tabs value={activeTab()} onChange={activateTab}>
|
|
|
- <div class="sticky top-0 shrink-0 flex">
|
|
|
- <Tabs.List
|
|
|
- ref={(el: HTMLDivElement) => {
|
|
|
- const stop = createFileTabListSync({ el, contextOpen })
|
|
|
- onCleanup(stop)
|
|
|
- }}
|
|
|
- >
|
|
|
- <Show when={reviewTab() && props.canReview()}>
|
|
|
- <Tabs.Trigger
|
|
|
- value="review"
|
|
|
- id={reviewTabID}
|
|
|
- aria-controls={activeTab() === "review" ? reviewTabPanelID : undefined}
|
|
|
+ <Show
|
|
|
+ when={props.fileBrowserState}
|
|
|
+ fallback={
|
|
|
+ <DragDropProvider
|
|
|
+ onDragStart={handleDragStart}
|
|
|
+ onDragEnd={handleDragEnd}
|
|
|
+ onDragOver={handleDragOver}
|
|
|
+ collisionDetector={closestCenter}
|
|
|
+ >
|
|
|
+ <DragDropSensors />
|
|
|
+ <ConstrainDragYAxis />
|
|
|
+ <Tabs value={activeTab()} onChange={activateTab}>
|
|
|
+ <div class="sticky top-0 shrink-0 flex">
|
|
|
+ <Tabs.List
|
|
|
+ ref={(el: HTMLDivElement) => {
|
|
|
+ const stop = createFileTabListSync({ el, contextOpen })
|
|
|
+ onCleanup(stop)
|
|
|
+ }}
|
|
|
>
|
|
|
- <div class="flex items-center gap-1.5">
|
|
|
- <div>{language.t("session.tab.review")}</div>
|
|
|
- <Show when={props.hasReview()}>
|
|
|
- <div>{props.reviewCount()}</div>
|
|
|
- </Show>
|
|
|
- </div>
|
|
|
- </Tabs.Trigger>
|
|
|
- </Show>
|
|
|
- <Show when={contextOpen()}>
|
|
|
- <Tabs.Trigger
|
|
|
- value="context"
|
|
|
- closeButton={
|
|
|
+ <Show when={reviewTab() && props.canReview()}>
|
|
|
+ <Tabs.Trigger
|
|
|
+ value="review"
|
|
|
+ id={reviewTabID}
|
|
|
+ aria-controls={activeTab() === "review" ? reviewTabPanelID : undefined}
|
|
|
+ >
|
|
|
+ <div class="flex items-center gap-1.5">
|
|
|
+ <div>{language.t("session.tab.review")}</div>
|
|
|
+ <Show when={props.hasReview()}>
|
|
|
+ <div>{props.reviewCount()}</div>
|
|
|
+ </Show>
|
|
|
+ </div>
|
|
|
+ </Tabs.Trigger>
|
|
|
+ </Show>
|
|
|
+ <Show when={contextOpen()}>
|
|
|
+ <Tabs.Trigger
|
|
|
+ value="context"
|
|
|
+ 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("context")}
|
|
|
+ aria-label={language.t("common.closeTab")}
|
|
|
+ />
|
|
|
+ </TooltipKeybind>
|
|
|
+ }
|
|
|
+ hideCloseButton
|
|
|
+ onMiddleClick={() => tabs().close("context")}
|
|
|
+ >
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
+ <SessionContextUsage variant="indicator" />
|
|
|
+ <div>{language.t("session.tab.context")}</div>
|
|
|
+ </div>
|
|
|
+ </Tabs.Trigger>
|
|
|
+ </Show>
|
|
|
+ <SortableProvider ids={openedTabs()}>
|
|
|
+ <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="h-full shrink-0 sticky right-0 z-10 flex items-center justify-center pr-3"
|
|
|
+ classList={{
|
|
|
+ "bg-v2-background-bg-base": settings.general.newLayoutDesigns(),
|
|
|
+ "bg-background-stronger": !settings.general.newLayoutDesigns(),
|
|
|
+ }}
|
|
|
+ >
|
|
|
<TooltipKeybind
|
|
|
- title={language.t("common.closeTab")}
|
|
|
- keybind={command.keybind("tab.close")}
|
|
|
- placement="bottom"
|
|
|
- gutter={10}
|
|
|
+ title={language.t("command.file.open")}
|
|
|
+ keybind={command.keybind("file.open")}
|
|
|
+ class="flex items-center"
|
|
|
>
|
|
|
<IconButton
|
|
|
- icon="close-small"
|
|
|
+ icon="plus-small"
|
|
|
variant="ghost"
|
|
|
- class="h-5 w-5"
|
|
|
- onClick={() => tabs().close("context")}
|
|
|
- aria-label={language.t("common.closeTab")}
|
|
|
+ iconSize="large"
|
|
|
+ class="!rounded-md"
|
|
|
+ onClick={() => {
|
|
|
+ void import("@/components/dialog-select-file").then((x) => {
|
|
|
+ dialog.show(() => <x.DialogSelectFile mode="files" onOpenFile={showAllFiles} />)
|
|
|
+ })
|
|
|
+ }}
|
|
|
+ aria-label={language.t("command.file.open")}
|
|
|
/>
|
|
|
</TooltipKeybind>
|
|
|
- }
|
|
|
- hideCloseButton
|
|
|
- onMiddleClick={() => tabs().close("context")}
|
|
|
+ </div>
|
|
|
+ </Tabs.List>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <Show when={reviewTab() && props.canReview() && activeTab() === "review"}>
|
|
|
+ <div
|
|
|
+ id={reviewTabPanelID}
|
|
|
+ role="tabpanel"
|
|
|
+ aria-labelledby={reviewTabID}
|
|
|
+ tabIndex={props.reviewHasFocusableContent() ? undefined : 0}
|
|
|
+ data-slot="tabs-content"
|
|
|
+ class="flex flex-col h-full overflow-hidden contain-strict"
|
|
|
>
|
|
|
- <div class="flex items-center gap-2">
|
|
|
- <SessionContextUsage variant="indicator" />
|
|
|
- <div>{language.t("session.tab.context")}</div>
|
|
|
+ {props.reviewPanel()}
|
|
|
+ </div>
|
|
|
+ </Show>
|
|
|
+
|
|
|
+ <Show when={activeTab() === "empty"}>
|
|
|
+ <Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict">
|
|
|
+ <div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
|
|
+ <div class="h-full px-6 pb-42 -mt-4 flex flex-col items-center justify-center text-center gap-6">
|
|
|
+ <Mark class="w-14 opacity-10" />
|
|
|
+ <div class="text-14-regular text-text-weak max-w-56">
|
|
|
+ {language.t("session.files.selectToOpen")}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </Tabs.Content>
|
|
|
+ </Show>
|
|
|
+
|
|
|
+ <Show when={activeTab() === "context"}>
|
|
|
+ <Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict">
|
|
|
+ <div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
|
|
+ <SessionContextTab />
|
|
|
</div>
|
|
|
- </Tabs.Trigger>
|
|
|
+ </Tabs.Content>
|
|
|
+ </Show>
|
|
|
+
|
|
|
+ <Show when={activeFileTab()} keyed>
|
|
|
+ {(tab) => <FileTabContent tab={tab} />}
|
|
|
+ </Show>
|
|
|
+ </Tabs>
|
|
|
+ <DragOverlay>
|
|
|
+ <Show when={store.activeDraggable} keyed>
|
|
|
+ {(tab) => {
|
|
|
+ const path = file.pathFromTab(tab)
|
|
|
+ return (
|
|
|
+ <div data-component="tabs-drag-preview">
|
|
|
+ <Show when={path}>
|
|
|
+ {(p) => <FileVisual active path={p()} temporary={temporaryTab() === tab} />}
|
|
|
+ </Show>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+ }}
|
|
|
</Show>
|
|
|
- <SortableProvider ids={openedTabs()}>
|
|
|
+ </DragOverlay>
|
|
|
+ </DragDropProvider>
|
|
|
+ }
|
|
|
+ >
|
|
|
+ <DndKitProvider
|
|
|
+ sensors={[
|
|
|
+ PointerSensor.configure({
|
|
|
+ activationConstraints: [new PointerActivationConstraints.Distance({ value: 4 })],
|
|
|
+ preventActivation: (event) =>
|
|
|
+ event.target instanceof Element &&
|
|
|
+ (!!event.target.closest('[data-slot="tabs-trigger-close-button"]') ||
|
|
|
+ !!event.target.closest(".session-review-v2-open-in-app-slot")),
|
|
|
+ }),
|
|
|
+ ]}
|
|
|
+ modifiers={[
|
|
|
+ RestrictToHorizontalAxis,
|
|
|
+ RestrictToElement.configure({ element: () => tabList ?? null }),
|
|
|
+ ]}
|
|
|
+ plugins={(defaults) => [
|
|
|
+ ...defaults.filter((plugin) => plugin !== Accessibility),
|
|
|
+ AutoScroller.configure({ acceleration: 8, threshold: { x: 0.05, y: 0 } }),
|
|
|
+ Feedback.configure({ dropAnimation: null }),
|
|
|
+ ]}
|
|
|
+ onDragEnd={(event) => {
|
|
|
+ const source = event.operation.source
|
|
|
+ if (event.canceled || !isSortable(source) || source.initialIndex === source.index) return
|
|
|
+ tabs().move(source.id.toString(), source.index)
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <Tabs value={activeTab()} onChange={activateTab}>
|
|
|
+ <div class="session-review-v2-tabs-bar sticky top-0 shrink-0 flex items-center">
|
|
|
+ <Tabs.List
|
|
|
+ ref={(el: HTMLDivElement) => {
|
|
|
+ tabList = el
|
|
|
+ const stop = createFileTabListSync({ el, contextOpen })
|
|
|
+ onCleanup(stop)
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <Show when={props.reviewSidebarToggle}>
|
|
|
+ {(toggle) => (
|
|
|
+ <div class="h-full shrink-0 flex items-center justify-center">
|
|
|
+ {toggle()(activeTab() === SESSION_OPEN_FILE_TAB)}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </Show>
|
|
|
+ <Show when={reviewTab() && props.canReview()}>
|
|
|
+ <Tabs.Trigger
|
|
|
+ value="review"
|
|
|
+ id={reviewTabID}
|
|
|
+ aria-controls={activeTab() === "review" ? reviewTabPanelID : undefined}
|
|
|
+ >
|
|
|
+ {props.hasReview()
|
|
|
+ ? language.t("session.review.filesChanged", { count: props.reviewCount() })
|
|
|
+ : language.t("session.tab.review")}
|
|
|
+ </Tabs.Trigger>
|
|
|
+ </Show>
|
|
|
+ <Show when={contextOpen()}>
|
|
|
+ <Tabs.Trigger
|
|
|
+ value="context"
|
|
|
+ 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("context")}
|
|
|
+ aria-label={language.t("common.closeTab")}
|
|
|
+ />
|
|
|
+ </TooltipKeybind>
|
|
|
+ }
|
|
|
+ hideCloseButton
|
|
|
+ onMiddleClick={() => tabs().close("context")}
|
|
|
+ >
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
+ <SessionContextUsage variant="indicator" />
|
|
|
+ <div>{language.t("session.tab.context")}</div>
|
|
|
+ </div>
|
|
|
+ </Tabs.Trigger>
|
|
|
+ </Show>
|
|
|
<For each={panelTabs()}>
|
|
|
{(tab) => (
|
|
|
<Show
|
|
|
when={tab === SESSION_OPEN_FILE_TAB}
|
|
|
fallback={
|
|
|
- <SortableTab
|
|
|
+ <SortableTabV2
|
|
|
tab={tab}
|
|
|
+ index={() => tabs().all().indexOf(tab)}
|
|
|
temporary={temporaryTab() === tab}
|
|
|
onTabClose={tabs().close}
|
|
|
onTabDoubleClick={temporaryTab() === tab ? openTab : undefined}
|
|
|
@@ -392,106 +637,104 @@ export function SessionSidePanel(props: {
|
|
|
</Show>
|
|
|
)}
|
|
|
</For>
|
|
|
- </SortableProvider>
|
|
|
+ <div
|
|
|
+ class="h-full shrink-0 sticky right-0 z-10 flex items-center justify-center"
|
|
|
+ classList={{
|
|
|
+ "bg-v2-background-bg-base": settings.general.newLayoutDesigns(),
|
|
|
+ "bg-background-stronger": !settings.general.newLayoutDesigns(),
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <TooltipV2
|
|
|
+ value={
|
|
|
+ <>
|
|
|
+ {language.t("command.file.open")}
|
|
|
+ <Show when={openFileKeybind().length > 0}>
|
|
|
+ <KeybindV2 keys={openFileKeybind()} variant="neutral" />
|
|
|
+ </Show>
|
|
|
+ </>
|
|
|
+ }
|
|
|
+ placement="bottom"
|
|
|
+ class="flex items-center"
|
|
|
+ >
|
|
|
+ <IconButtonV2
|
|
|
+ icon={<Icon name="plus-small" />}
|
|
|
+ variant="ghost-muted"
|
|
|
+ size="large"
|
|
|
+ onClick={() => openFileBrowser()}
|
|
|
+ aria-label={language.t("command.file.open")}
|
|
|
+ />
|
|
|
+ </TooltipV2>
|
|
|
+ </div>
|
|
|
+ </Tabs.List>
|
|
|
<div
|
|
|
- class="h-full shrink-0 sticky right-0 z-10 flex items-center justify-center pr-3"
|
|
|
- classList={{
|
|
|
- "bg-v2-background-bg-base": settings.general.newLayoutDesigns(),
|
|
|
- "bg-background-stronger": !settings.general.newLayoutDesigns(),
|
|
|
- }}
|
|
|
+ class="session-review-v2-open-in-app-slot shrink-0 flex items-center pr-3"
|
|
|
+ onPointerDown={(event) => event.stopPropagation()}
|
|
|
+ onClick={(event) => event.stopPropagation()}
|
|
|
>
|
|
|
- <TooltipKeybind
|
|
|
- title={language.t("command.file.open")}
|
|
|
- keybind={command.keybind("file.open")}
|
|
|
- class="flex items-center"
|
|
|
- >
|
|
|
- <IconButton
|
|
|
- icon="plus-small"
|
|
|
- variant="ghost"
|
|
|
- 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} />)
|
|
|
- })
|
|
|
- }}
|
|
|
- aria-label={language.t("command.file.open")}
|
|
|
- />
|
|
|
- </TooltipKeybind>
|
|
|
+ <OpenInAppV2 directory={projectDirectory} />
|
|
|
</div>
|
|
|
- </Tabs.List>
|
|
|
- </div>
|
|
|
-
|
|
|
- <Show when={reviewTab() && props.canReview() && activeTab() === "review"}>
|
|
|
- <div
|
|
|
- id={reviewTabPanelID}
|
|
|
- role="tabpanel"
|
|
|
- aria-labelledby={reviewTabID}
|
|
|
- tabIndex={props.reviewHasFocusableContent() ? undefined : 0}
|
|
|
- data-slot="tabs-content"
|
|
|
- class="flex flex-col h-full overflow-hidden contain-strict"
|
|
|
- >
|
|
|
- {props.reviewPanel()}
|
|
|
</div>
|
|
|
- </Show>
|
|
|
-
|
|
|
- <Show when={activeTab() === "empty"}>
|
|
|
- <Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict">
|
|
|
- <div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
|
|
- <div class="h-full px-6 pb-42 -mt-4 flex flex-col items-center justify-center text-center gap-6">
|
|
|
- <Mark class="w-14 opacity-10" />
|
|
|
- <div class="text-14-regular text-text-weak max-w-56">
|
|
|
- {language.t("session.files.selectToOpen")}
|
|
|
+
|
|
|
+ <Show when={reviewTab() && props.canReview() && activeTab() === "review"}>
|
|
|
+ <div
|
|
|
+ id={reviewTabPanelID}
|
|
|
+ role="tabpanel"
|
|
|
+ aria-labelledby={reviewTabID}
|
|
|
+ tabIndex={props.reviewHasFocusableContent() ? undefined : 0}
|
|
|
+ data-slot="tabs-content"
|
|
|
+ class="flex flex-col h-full overflow-hidden contain-strict"
|
|
|
+ >
|
|
|
+ {props.reviewPanel()}
|
|
|
+ </div>
|
|
|
+ </Show>
|
|
|
+
|
|
|
+ <Show when={activeTab() === "empty"}>
|
|
|
+ <Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict">
|
|
|
+ <div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
|
|
+ <div class="h-full px-6 pb-42 -mt-4 flex flex-col items-center justify-center text-center gap-6">
|
|
|
+ <Mark class="w-14 opacity-10" />
|
|
|
+ <div class="text-14-regular text-text-weak max-w-56">
|
|
|
+ {language.t("session.files.selectToOpen")}
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- </Tabs.Content>
|
|
|
- </Show>
|
|
|
+ </Tabs.Content>
|
|
|
+ </Show>
|
|
|
|
|
|
- <Show when={activeTab() === "context"}>
|
|
|
- <Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict">
|
|
|
- <div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
|
|
- <SessionContextTab />
|
|
|
- </div>
|
|
|
- </Tabs.Content>
|
|
|
- </Show>
|
|
|
-
|
|
|
- <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>
|
|
|
- <DragOverlay>
|
|
|
- <Show when={store.activeDraggable} keyed>
|
|
|
- {(tab) => {
|
|
|
- const path = file.pathFromTab(tab)
|
|
|
- return (
|
|
|
- <div data-component="tabs-drag-preview">
|
|
|
- <Show when={path}>
|
|
|
- {(p) => <FileVisual active path={p()} temporary={temporaryTab() === tab} />}
|
|
|
- </Show>
|
|
|
+ <Show when={activeTab() === "context"}>
|
|
|
+ <Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict">
|
|
|
+ <div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
|
|
+ <SessionContextTab />
|
|
|
</div>
|
|
|
- )
|
|
|
- }}
|
|
|
- </Show>
|
|
|
- </DragOverlay>
|
|
|
- </DragDropProvider>
|
|
|
+ </Tabs.Content>
|
|
|
+ </Show>
|
|
|
+
|
|
|
+ <Show when={fileBrowserMounted()}>
|
|
|
+ <div
|
|
|
+ id={fileBrowserTabPanelID}
|
|
|
+ role="tabpanel"
|
|
|
+ data-slot="tabs-content"
|
|
|
+ class="h-full min-h-0 overflow-hidden"
|
|
|
+ classList={{ hidden: !fileBrowserVisible() }}
|
|
|
+ inert={!fileBrowserVisible() || undefined}
|
|
|
+ >
|
|
|
+ <SessionFileBrowserTab
|
|
|
+ tab={browserTab() ?? activeFileTab() ?? SESSION_OPEN_FILE_TAB}
|
|
|
+ placeholder={
|
|
|
+ (browserTab() ?? activeFileTab() ?? SESSION_OPEN_FILE_TAB) === SESSION_OPEN_FILE_TAB
|
|
|
+ }
|
|
|
+ active={file.pathFromTab(browserTab() ?? activeFileTab() ?? "")}
|
|
|
+ kinds={kinds()}
|
|
|
+ state={props.fileBrowserState!}
|
|
|
+ onSelect={(path) => previewTab(file.tab(path))}
|
|
|
+ onSelectPermanent={(path) => openTab(file.tab(path))}
|
|
|
+ filterRef={(element) => (fileFilter = element)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </Show>
|
|
|
+ </Tabs>
|
|
|
+ </DndKitProvider>
|
|
|
+ </Show>
|
|
|
</div>
|
|
|
</div>
|
|
|
</Show>
|
|
|
@@ -519,10 +762,19 @@ export function SessionSidePanel(props: {
|
|
|
>
|
|
|
<Tabs.List>
|
|
|
<Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
|
|
|
- {props.reviewCount()}{" "}
|
|
|
- {language.t(
|
|
|
- props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other",
|
|
|
- )}
|
|
|
+ <Show
|
|
|
+ when={settings.general.newLayoutDesigns()}
|
|
|
+ fallback={
|
|
|
+ <>
|
|
|
+ {props.reviewCount()}{" "}
|
|
|
+ {language.t(
|
|
|
+ props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other",
|
|
|
+ )}
|
|
|
+ </>
|
|
|
+ }
|
|
|
+ >
|
|
|
+ {language.t("session.review.filesChanged", { count: props.reviewCount() })}
|
|
|
+ </Show>
|
|
|
</Tabs.Trigger>
|
|
|
<Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}>
|
|
|
{language.t("session.files.all")}
|