summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/app/src/pages/layout.tsx2
-rw-r--r--packages/app/src/pages/session.tsx35
-rw-r--r--packages/app/src/pages/session/helpers.ts104
-rw-r--r--packages/app/src/pages/session/session-side-panel.tsx325
-rw-r--r--packages/app/src/pages/session/terminal-panel.tsx239
5 files changed, 450 insertions, 255 deletions
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx
index 9c359aafb..70114623e 100644
--- a/packages/app/src/pages/layout.tsx
+++ b/packages/app/src/pages/layout.tsx
@@ -2252,7 +2252,7 @@ export default function Layout(props: ParentProps) {
>
<main
classList={{
- "size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base xl:border-l xl:rounded-tl-[12px]": true,
+ "size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base bg-background-base xl:border-l xl:rounded-tl-[12px]": true,
}}
>
<Show when={!autoselecting()} fallback={<div class="size-full" />}>
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index 077ab544d..cba49f5fb 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -33,7 +33,7 @@ import { usePrompt } from "@/context/prompt"
import { useSDK } from "@/context/sdk"
import { useSync } from "@/context/sync"
import { createSessionComposerState, SessionComposerRegion } from "@/pages/session/composer"
-import { createOpenReviewFile } from "@/pages/session/helpers"
+import { createOpenReviewFile, createSizing } from "@/pages/session/helpers"
import { MessageTimeline } from "@/pages/session/message-timeline"
import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab"
import { createScrollSpy } from "@/pages/session/scroll-spy"
@@ -332,6 +332,7 @@ export default function Page() {
)
const isDesktop = createMediaQuery("(min-width: 768px)")
+ const size = createSizing()
const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened())
const desktopSidePanelOpen = createMemo(() => desktopReviewOpen() || desktopFileTreeOpen())
@@ -1252,9 +1253,9 @@ export default function Page() {
{/* Session panel */}
<div
classList={{
- "@container relative shrink-0 flex flex-col min-h-0 h-full bg-background-stronger": true,
- "flex-1": true,
- "md:flex-none": desktopSidePanelOpen(),
+ "@container relative shrink-0 flex flex-col min-h-0 h-full bg-background-stronger flex-1 md:flex-none": true,
+ "transition-[width] duration-[240ms] ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
+ !size.active(),
}}
style={{
width: sessionPanelWidth(),
@@ -1351,17 +1352,27 @@ export default function Page() {
/>
<Show when={desktopReviewOpen()}>
- <ResizeHandle
- direction="horizontal"
- size={layout.session.width()}
- min={450}
- max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.45}
- onResize={layout.session.resize}
- />
+ <div onPointerDown={() => size.start()}>
+ <ResizeHandle
+ direction="horizontal"
+ size={layout.session.width()}
+ min={450}
+ max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.45}
+ onResize={(width) => {
+ size.touch()
+ layout.session.resize(width)
+ }}
+ />
+ </div>
</Show>
</div>
- <SessionSidePanel reviewPanel={reviewPanel} activeDiff={tree.activeDiff} focusReviewDiff={focusReviewDiff} />
+ <SessionSidePanel
+ reviewPanel={reviewPanel}
+ activeDiff={tree.activeDiff}
+ focusReviewDiff={focusReviewDiff}
+ size={size}
+ />
</div>
<TerminalPanel />
diff --git a/packages/app/src/pages/session/helpers.ts b/packages/app/src/pages/session/helpers.ts
index 60b26cdf4..be9656900 100644
--- a/packages/app/src/pages/session/helpers.ts
+++ b/packages/app/src/pages/session/helpers.ts
@@ -1,4 +1,5 @@
-import { batch } from "solid-js"
+import { batch, createEffect, on, onCleanup, onMount, type Accessor } from "solid-js"
+import { createStore } from "solid-js/store"
export const focusTerminalById = (id: string) => {
const wrapper = document.getElementById(`terminal-wrapper-${id}`)
@@ -69,3 +70,104 @@ export const getTabReorderIndex = (tabs: readonly string[], from: string, to: st
if (fromIndex === -1 || toIndex === -1 || fromIndex === toIndex) return undefined
return toIndex
}
+
+export const createSizing = () => {
+ const [state, setState] = createStore({ active: false })
+ let t: number | undefined
+
+ const stop = () => {
+ if (t !== undefined) {
+ clearTimeout(t)
+ t = undefined
+ }
+ setState("active", false)
+ }
+
+ const start = () => {
+ if (t !== undefined) {
+ clearTimeout(t)
+ t = undefined
+ }
+ setState("active", true)
+ }
+
+ onMount(() => {
+ window.addEventListener("pointerup", stop)
+ window.addEventListener("pointercancel", stop)
+ window.addEventListener("blur", stop)
+ onCleanup(() => {
+ window.removeEventListener("pointerup", stop)
+ window.removeEventListener("pointercancel", stop)
+ window.removeEventListener("blur", stop)
+ })
+ })
+
+ onCleanup(() => {
+ if (t !== undefined) clearTimeout(t)
+ })
+
+ return {
+ active: () => state.active,
+ start,
+ touch() {
+ start()
+ t = window.setTimeout(stop, 120)
+ },
+ }
+}
+
+export type Sizing = ReturnType<typeof createSizing>
+
+export const createPresence = (open: Accessor<boolean>, wait = 200) => {
+ const [state, setState] = createStore({
+ show: open(),
+ open: open(),
+ })
+ let frame: number | undefined
+ let t: number | undefined
+
+ const clear = () => {
+ if (frame !== undefined) {
+ cancelAnimationFrame(frame)
+ frame = undefined
+ }
+ if (t !== undefined) {
+ clearTimeout(t)
+ t = undefined
+ }
+ }
+
+ createEffect(
+ on(open, (next) => {
+ clear()
+
+ if (next) {
+ if (state.show) {
+ setState("open", true)
+ return
+ }
+
+ setState({ show: true, open: false })
+ frame = requestAnimationFrame(() => {
+ frame = undefined
+ setState("open", true)
+ })
+ return
+ }
+
+ if (!state.show) return
+ setState("open", false)
+ t = window.setTimeout(() => {
+ t = undefined
+ setState("show", false)
+ }, wait)
+ }),
+ )
+
+ onCleanup(clear)
+
+ return {
+ show: () => state.show,
+ open: () => state.open,
+ }
+}
diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx
index ffb6ab2e7..173b3db36 100644
--- a/packages/app/src/pages/session/session-side-panel.tsx
+++ b/packages/app/src/pages/session/session-side-panel.tsx
@@ -23,7 +23,7 @@ import { useLayout } from "@/context/layout"
import { useSync } from "@/context/sync"
import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
import { FileTabContent } from "@/pages/session/file-tabs"
-import { createOpenSessionFileTab, getTabReorderIndex } from "@/pages/session/helpers"
+import { createOpenSessionFileTab, getTabReorderIndex, type Sizing } from "@/pages/session/helpers"
import { StickyAddButton } from "@/pages/session/review-tab"
import { setSessionHandoff } from "@/pages/session/handoff"
@@ -31,6 +31,7 @@ export function SessionSidePanel(props: {
reviewPanel: () => JSX.Element
activeDiff?: string
focusReviewDiff: (path: string) => void
+ size: Sizing
}) {
const params = useParams()
const layout = useLayout()
@@ -46,8 +47,20 @@ export function SessionSidePanel(props: {
const view = createMemo(() => layout.view(sessionKey))
const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
- const open = createMemo(() => isDesktop() && (view().reviewPanel.opened() || layout.fileTree.opened()))
+ const fileOpen = createMemo(() => isDesktop() && layout.fileTree.opened())
+ const open = createMemo(() => reviewOpen() || fileOpen())
const reviewTab = createMemo(() => isDesktop())
+ const panelWidth = createMemo(() => {
+ if (!open()) return "0px"
+ if (reviewOpen()) return `calc(100% - ${layout.session.width()}px)`
+ return `${layout.fileTree.width()}px`
+ })
+ const reviewWidth = createMemo(() => {
+ if (!reviewOpen()) return "0px"
+ if (!fileOpen()) return "100%"
+ return `calc(100% - ${layout.fileTree.width()}px)`
+ })
+ const treeWidth = createMemo(() => (fileOpen() ? `${layout.fileTree.width()}px` : "0px"))
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
@@ -210,146 +223,175 @@ export function SessionSidePanel(props: {
})
return (
- <Show when={open()}>
+ <Show when={isDesktop()}>
<aside
id="review-panel"
aria-label={language.t("session.panel.reviewAndFiles")}
- class="relative min-w-0 h-full border-l border-border-weaker-base flex"
+ aria-hidden={!open()}
+ inert={!open()}
+ class="relative min-w-0 h-full flex shrink-0 overflow-hidden bg-background-base"
classList={{
- "flex-1": reviewOpen(),
- "shrink-0": !reviewOpen(),
+ "opacity-100": open(),
+ "opacity-0 pointer-events-none": !open(),
+ "transition-[width,opacity] duration-[240ms] ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
+ !props.size.active(),
}}
- style={{ width: reviewOpen() ? undefined : `${layout.fileTree.width()}px` }}
+ style={{ width: panelWidth() }}
>
- <Show when={reviewOpen()}>
- <div class="flex-1 min-w-0 h-full">
- <DragDropProvider
- onDragStart={handleDragStart}
- onDragEnd={handleDragEnd}
- onDragOver={handleDragOver}
- collisionDetector={closestCenter}
- >
- <DragDropSensors />
- <ConstrainDragYAxis />
- <Tabs value={activeTab()} onChange={openTab}>
- <div class="sticky top-0 shrink-0 flex">
- <Tabs.List
- ref={(el: HTMLDivElement) => {
- const stop = createFileTabListSync({ el, contextOpen })
- onCleanup(stop)
- }}
- >
- <Show when={reviewTab()}>
- <Tabs.Trigger value="review">
- <div class="flex items-center gap-1.5">
- <div>{language.t("session.tab.review")}</div>
- <Show when={hasReview()}>
- <div>{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={openedTabs()}>{(tab) => <SortableTab tab={tab} onTabClose={tabs().close} />}</For>
- </SortableProvider>
- <StickyAddButton>
- <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={() => dialog.show(() => <DialogSelectFile mode="files" onOpenFile={showAllFiles} />)}
- aria-label={language.t("command.file.open")}
- />
- </TooltipKeybind>
- </StickyAddButton>
- </Tabs.List>
- </div>
-
- <Show when={reviewTab()}>
- <Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
- <Show when={activeTab() === "review"}>{props.reviewPanel()}</Show>
- </Tabs.Content>
- </Show>
-
- <Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict">
- <Show when={activeTab() === "empty"}>
- <div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
- <div class="h-full px-6 pb-42 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 class="size-full flex border-l border-border-weaker-base">
+ <div
+ aria-hidden={!reviewOpen()}
+ inert={!reviewOpen()}
+ class="relative min-w-0 h-full shrink-0 overflow-hidden bg-background-base"
+ classList={{
+ "opacity-100": reviewOpen(),
+ "opacity-0 pointer-events-none": !reviewOpen(),
+ "transition-[width,opacity] duration-[240ms] ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
+ !props.size.active(),
+ }}
+ style={{ width: reviewWidth() }}
+ >
+ <div class="size-full min-w-0 h-full bg-background-base">
+ <DragDropProvider
+ onDragStart={handleDragStart}
+ onDragEnd={handleDragEnd}
+ onDragOver={handleDragOver}
+ collisionDetector={closestCenter}
+ >
+ <DragDropSensors />
+ <ConstrainDragYAxis />
+ <Tabs value={activeTab()} onChange={openTab}>
+ <div class="sticky top-0 shrink-0 flex">
+ <Tabs.List
+ ref={(el: HTMLDivElement) => {
+ const stop = createFileTabListSync({ el, contextOpen })
+ onCleanup(stop)
+ }}
+ >
+ <Show when={reviewTab()}>
+ <Tabs.Trigger value="review">
+ <div class="flex items-center gap-1.5">
+ <div>{language.t("session.tab.review")}</div>
+ <Show when={hasReview()}>
+ <div>{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={openedTabs()}>{(tab) => <SortableTab tab={tab} onTabClose={tabs().close} />}</For>
+ </SortableProvider>
+ <StickyAddButton>
+ <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={() =>
+ dialog.show(() => <DialogSelectFile mode="files" onOpenFile={showAllFiles} />)
+ }
+ aria-label={language.t("command.file.open")}
+ />
+ </TooltipKeybind>
+ </StickyAddButton>
+ </Tabs.List>
+ </div>
+
+ <Show when={reviewTab()}>
+ <Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
+ <Show when={activeTab() === "review"}>{props.reviewPanel()}</Show>
+ </Tabs.Content>
</Show>
- </Tabs.Content>
- <Show when={contextOpen()}>
- <Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict">
- <Show when={activeTab() === "context"}>
+ <Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict">
+ <Show when={activeTab() === "empty"}>
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
- <SessionContextTab />
+ <div class="h-full px-6 pb-42 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>
</Show>
</Tabs.Content>
- </Show>
- <Show when={activeFileTab()} keyed>
- {(tab) => <FileTabContent tab={tab} />}
- </Show>
- </Tabs>
- <DragOverlay>
- <Show when={store.activeDraggable} keyed>
- {(tab) => {
- const path = createMemo(() => file.pathFromTab(tab))
- return (
- <div data-component="tabs-drag-preview">
- <Show when={path()}>{(p) => <FileVisual active path={p()} />}</Show>
- </div>
- )
- }}
- </Show>
- </DragOverlay>
- </DragDropProvider>
+ <Show when={contextOpen()}>
+ <Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict">
+ <Show when={activeTab() === "context"}>
+ <div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
+ <SessionContextTab />
+ </div>
+ </Show>
+ </Tabs.Content>
+ </Show>
+
+ <Show when={activeFileTab()} keyed>
+ {(tab) => <FileTabContent tab={tab} />}
+ </Show>
+ </Tabs>
+ <DragOverlay>
+ <Show when={store.activeDraggable} keyed>
+ {(tab) => {
+ const path = createMemo(() => file.pathFromTab(tab))
+ return (
+ <div data-component="tabs-drag-preview">
+ <Show when={path()}>{(p) => <FileVisual active path={p()} />}</Show>
+ </div>
+ )
+ }}
+ </Show>
+ </DragOverlay>
+ </DragDropProvider>
+ </div>
</div>
- </Show>
- <Show when={layout.fileTree.opened()}>
- <div id="file-tree-panel" class="relative shrink-0 h-full" style={{ width: `${layout.fileTree.width()}px` }}>
+ <div
+ id="file-tree-panel"
+ aria-hidden={!fileOpen()}
+ inert={!fileOpen()}
+ class="relative min-w-0 h-full shrink-0 overflow-hidden"
+ classList={{
+ "opacity-100": fileOpen(),
+ "opacity-0 pointer-events-none": !fileOpen(),
+ "transition-[width,opacity] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
+ !props.size.active(),
+ }}
+ style={{ width: treeWidth() }}
+ >
<div
class="h-full flex flex-col overflow-hidden group/filetree"
classList={{ "border-l border-border-weaker-base": reviewOpen() }}
@@ -412,18 +454,25 @@ export function SessionSidePanel(props: {
</Tabs.Content>
</Tabs>
</div>
- <ResizeHandle
- direction="horizontal"
- edge="start"
- size={layout.fileTree.width()}
- min={200}
- max={480}
- collapseThreshold={160}
- onResize={layout.fileTree.resize}
- onCollapse={layout.fileTree.close}
- />
+ <Show when={fileOpen()}>
+ <div onPointerDown={() => props.size.start()}>
+ <ResizeHandle
+ direction="horizontal"
+ edge="start"
+ size={layout.fileTree.width()}
+ min={200}
+ max={480}
+ collapseThreshold={160}
+ onResize={(width) => {
+ props.size.touch()
+ layout.fileTree.resize(width)
+ }}
+ onCollapse={layout.fileTree.close}
+ />
+ </div>
+ </Show>
</div>
- </Show>
+ </div>
</aside>
</Show>
)
diff --git a/packages/app/src/pages/session/terminal-panel.tsx b/packages/app/src/pages/session/terminal-panel.tsx
index 69c8aefcc..d5eac2322 100644
--- a/packages/app/src/pages/session/terminal-panel.tsx
+++ b/packages/app/src/pages/session/terminal-panel.tsx
@@ -17,7 +17,7 @@ import { useLanguage } from "@/context/language"
import { useLayout } from "@/context/layout"
import { useTerminal, type LocalPTY } from "@/context/terminal"
import { terminalTabLabel } from "@/pages/session/terminal-label"
-import { focusTerminalById } from "@/pages/session/helpers"
+import { createPresence, createSizing, focusTerminalById } from "@/pages/session/helpers"
import { getTerminalHandoff, setTerminalHandoff } from "@/pages/session/handoff"
export function TerminalPanel() {
@@ -33,8 +33,11 @@ export function TerminalPanel() {
const opened = createMemo(() => view().terminal.opened())
const open = createMemo(() => isDesktop() && opened())
+ const panel = createPresence(open)
+ const size = createSizing()
const height = createMemo(() => layout.terminal.height())
const close = () => view().terminal.close()
+ let root: HTMLDivElement | undefined
const [store, setStore] = createStore({
autoCreated: false,
@@ -67,7 +70,7 @@ export function TerminalPanel() {
on(
() => terminal.active(),
(activeId) => {
- if (!activeId || !open()) return
+ if (!activeId || !panel.open()) return
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur()
}
@@ -77,6 +80,14 @@ export function TerminalPanel() {
)
createEffect(() => {
+ if (panel.open()) return
+ const active = document.activeElement
+ if (!(active instanceof HTMLElement)) return
+ if (!root?.contains(active)) return
+ active.blur()
+ })
+
+ createEffect(() => {
const dir = params.dir
if (!dir) return
if (!terminal.ready()) return
@@ -133,120 +144,142 @@ export function TerminalPanel() {
}
return (
- <Show when={open()}>
+ <Show when={panel.show()}>
<div
+ ref={root}
id="terminal-panel"
role="region"
aria-label={language.t("terminal.title")}
- class="relative w-full flex flex-col shrink-0 border-t border-border-weak-base"
- style={{ height: `${height()}px` }}
+ aria-hidden={!panel.open()}
+ inert={!panel.open()}
+ class="relative w-full shrink-0 overflow-hidden"
+ classList={{
+ "opacity-100": panel.open(),
+ "opacity-0 pointer-events-none": !panel.open(),
+ "transition-[height,opacity] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[height] motion-reduce:transition-none":
+ !size.active(),
+ }}
+ style={{ height: panel.open() ? `${height()}px` : "0px" }}
>
- <ResizeHandle
- direction="vertical"
- size={height()}
- min={100}
- max={typeof window === "undefined" ? 1000 : window.innerHeight * 0.6}
- collapseThreshold={50}
- onResize={layout.terminal.resize}
- onCollapse={close}
- />
- <Show
- when={terminal.ready()}
- fallback={
- <div class="flex flex-col h-full pointer-events-none">
- <div class="h-10 flex items-center gap-2 px-2 border-b border-border-weaker-base bg-background-stronger overflow-hidden">
- <For each={handoff()}>
- {(title) => (
- <div class="px-2 py-1 rounded-md bg-surface-base text-14-regular text-text-weak truncate max-w-40">
- {title}
- </div>
- )}
- </For>
- <div class="flex-1" />
- <div class="text-text-weak pr-2">
- {language.t("common.loading")}
- {language.t("common.loading.ellipsis")}
+ <div class="size-full flex flex-col border-t border-border-weak-base">
+ <div onPointerDown={() => size.start()}>
+ <ResizeHandle
+ direction="vertical"
+ size={height()}
+ min={100}
+ max={typeof window === "undefined" ? 1000 : window.innerHeight * 0.6}
+ collapseThreshold={50}
+ onResize={(next) => {
+ size.touch()
+ layout.terminal.resize(next)
+ }}
+ onCollapse={close}
+ />
+ </div>
+ <Show
+ when={terminal.ready()}
+ fallback={
+ <div class="flex flex-col h-full pointer-events-none">
+ <div class="h-10 flex items-center gap-2 px-2 border-b border-border-weaker-base bg-background-stronger overflow-hidden">
+ <For each={handoff()}>
+ {(title) => (
+ <div class="px-2 py-1 rounded-md bg-surface-base text-14-regular text-text-weak truncate max-w-40">
+ {title}
+ </div>
+ )}
+ </For>
+ <div class="flex-1" />
+ <div class="text-text-weak pr-2">
+ {language.t("common.loading")}
+ {language.t("common.loading.ellipsis")}
+ </div>
+ </div>
+ <div class="flex-1 flex items-center justify-center text-text-weak">
+ {language.t("terminal.loading")}
</div>
</div>
- <div class="flex-1 flex items-center justify-center text-text-weak">{language.t("terminal.loading")}</div>
- </div>
- }
- >
- <DragDropProvider
- onDragStart={handleTerminalDragStart}
- onDragEnd={handleTerminalDragEnd}
- onDragOver={handleTerminalDragOver}
- collisionDetector={closestCenter}
+ }
>
- <DragDropSensors />
- <ConstrainDragYAxis />
- <div class="flex flex-col h-full">
- <Tabs
- variant="alt"
- value={terminal.active()}
- onChange={(id) => terminal.open(id)}
- class="!h-auto !flex-none"
- >
- <Tabs.List class="h-10 border-b border-border-weaker-base">
- <SortableProvider ids={ids()}>
- <For each={ids()}>
- {(id) => (
- <Show when={byId().get(id)}>
- {(pty) => <SortableTerminalTab terminal={pty()} onClose={close} />}
- </Show>
- )}
- </For>
- </SortableProvider>
- <div class="h-full flex items-center justify-center">
- <TooltipKeybind
- title={language.t("command.terminal.new")}
- keybind={command.keybind("terminal.new")}
- class="flex items-center"
- >
- <IconButton
- icon="plus-small"
- variant="ghost"
- iconSize="large"
- onClick={terminal.new}
- aria-label={language.t("command.terminal.new")}
- />
- </TooltipKeybind>
- </div>
- </Tabs.List>
- </Tabs>
- <div class="flex-1 min-h-0 relative">
- <Show when={terminal.active()} keyed>
- {(id) => (
- <Show when={byId().get(id)}>
- {(pty) => (
- <div id={`terminal-wrapper-${id}`} class="absolute inset-0">
- <Terminal pty={pty()} onCleanup={terminal.update} onConnectError={() => terminal.clone(id)} />
+ <DragDropProvider
+ onDragStart={handleTerminalDragStart}
+ onDragEnd={handleTerminalDragEnd}
+ onDragOver={handleTerminalDragOver}
+ collisionDetector={closestCenter}
+ >
+ <DragDropSensors />
+ <ConstrainDragYAxis />
+ <div class="flex flex-col h-full">
+ <Tabs
+ variant="alt"
+ value={terminal.active()}
+ onChange={(id) => terminal.open(id)}
+ class="!h-auto !flex-none"
+ >
+ <Tabs.List class="h-10 border-b border-border-weaker-base">
+ <SortableProvider ids={ids()}>
+ <For each={ids()}>
+ {(id) => (
+ <Show when={byId().get(id)}>
+ {(pty) => <SortableTerminalTab terminal={pty()} onClose={close} />}
+ </Show>
+ )}
+ </For>
+ </SortableProvider>
+ <div class="h-full flex items-center justify-center">
+ <TooltipKeybind
+ title={language.t("command.terminal.new")}
+ keybind={command.keybind("terminal.new")}
+ class="flex items-center"
+ >
+ <IconButton
+ icon="plus-small"
+ variant="ghost"
+ iconSize="large"
+ onClick={terminal.new}
+ aria-label={language.t("command.terminal.new")}
+ />
+ </TooltipKeybind>
+ </div>
+ </Tabs.List>
+ </Tabs>
+ <div class="flex-1 min-h-0 relative">
+ <Show when={terminal.active()} keyed>
+ {(id) => (
+ <Show when={byId().get(id)}>
+ {(pty) => (
+ <div id={`terminal-wrapper-${id}`} class="absolute inset-0">
+ <Terminal
+ pty={pty()}
+ onCleanup={terminal.update}
+ onConnectError={() => terminal.clone(id)}
+ />
+ </div>
+ )}
+ </Show>
+ )}
+ </Show>
+ </div>
+ </div>
+ <DragOverlay>
+ <Show when={store.activeDraggable}>
+ {(draggedId) => (
+ <Show when={byId().get(draggedId())}>
+ {(t) => (
+ <div class="relative p-1 h-10 flex items-center bg-background-stronger text-14-regular">
+ {terminalTabLabel({
+ title: t().title,
+ titleNumber: t().titleNumber,
+ t: language.t as (key: string, vars?: Record<string, string | number | boolean>) => string,
+ })}
</div>
)}
</Show>
)}
</Show>
- </div>
- </div>
- <DragOverlay>
- <Show when={store.activeDraggable}>
- {(draggedId) => (
- <Show when={byId().get(draggedId())}>
- {(t) => (
- <div class="relative p-1 h-10 flex items-center bg-background-stronger text-14-regular">
- {terminalTabLabel({
- title: t().title,
- titleNumber: t().titleNumber,
- t: language.t as (key: string, vars?: Record<string, string | number | boolean>) => string,
- })}
- </div>
- )}
- </Show>
- )}
- </Show>
- </DragOverlay>
- </DragDropProvider>
- </Show>
+ </DragOverlay>
+ </DragDropProvider>
+ </Show>
+ </div>
</div>
</Show>
)