summaryrefslogtreecommitdiffhomepage
path: root/packages/desktop/src/pages
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-04 20:32:08 -0600
committerAdam <[email protected]>2025-12-04 20:32:08 -0600
commit09f522f0aa698be60c954e58bb7eee0e460c4439 (patch)
tree8b936f4ab3cbafab391551e898412d1617dbd66b /packages/desktop/src/pages
parentd82bd430f68b8227a93c39e0b7b617c9463ceea8 (diff)
downloadopencode-09f522f0aa698be60c954e58bb7eee0e460c4439.tar.gz
opencode-09f522f0aa698be60c954e58bb7eee0e460c4439.zip
Reapply "feat(desktop): terminal pane (#5081)"
This reverts commit f9dcd979364acc5172fd0044c1c8b04dcaec9229.
Diffstat (limited to 'packages/desktop/src/pages')
-rw-r--r--packages/desktop/src/pages/layout.tsx114
-rw-r--r--packages/desktop/src/pages/session.tsx649
2 files changed, 483 insertions, 280 deletions
diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx
index 15180c885..106a2e733 100644
--- a/packages/desktop/src/pages/layout.tsx
+++ b/packages/desktop/src/pages/layout.tsx
@@ -1,9 +1,9 @@
import { createMemo, For, ParentProps, Show } from "solid-js"
import { DateTime } from "luxon"
-import { A, useParams } from "@solidjs/router"
+import { A, useNavigate, useParams } from "@solidjs/router"
import { useLayout } from "@/context/layout"
import { useGlobalSync } from "@/context/global-sync"
-import { base64Encode } from "@/utils"
+import { base64Decode, base64Encode } from "@/utils"
import { Mark } from "@opencode-ai/ui/logo"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
@@ -12,11 +12,21 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
import { Collapsible } from "@opencode-ai/ui/collapsible"
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { getFilename } from "@opencode-ai/util/path"
+import { Select } from "@opencode-ai/ui/select"
+import { Session } from "@opencode-ai/sdk/client"
export default function Layout(props: ParentProps) {
+ const navigate = useNavigate()
const params = useParams()
const globalSync = useGlobalSync()
const layout = useLayout()
+ const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
+ const sessions = createMemo(() => globalSync.child(currentDirectory())[0].session ?? [])
+ const currentSession = createMemo(() => sessions().find((s) => s.id === params.id) ?? sessions().at(0))
+
+ function navigateToSession(session: Session | undefined) {
+ navigate(`/${params.dir}/session/${session?.id}`)
+ }
const handleOpenProject = async () => {
// layout.projects.open(dir.)
@@ -24,7 +34,7 @@ export default function Layout(props: ParentProps) {
return (
<div class="relative h-screen flex flex-col">
- <header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base">
+ <header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex">
<A
href="/"
classList={{
@@ -36,16 +46,110 @@ export default function Layout(props: ParentProps) {
>
<Mark class="shrink-0" />
</A>
+ <div class="pl-4 px-6 flex items-center justify-between gap-4 w-full">
+ <div class="flex items-center gap-3">
+ <div class="flex items-center gap-2">
+ <Select
+ options={layout.projects.list().map((project) => getFilename(project.directory))}
+ current={getFilename(currentDirectory())}
+ class="text-14-regular text-text-base"
+ variant="ghost"
+ />
+ <div class="text-text-weaker">/</div>
+ <Select
+ options={sessions()}
+ current={currentSession()}
+ label={(x) => x.title}
+ value={(x) => x.id}
+ onSelect={navigateToSession}
+ class="text-14-regular text-text-base max-w-3xs"
+ variant="ghost"
+ />
+ </div>
+ <Button as={A} href={`/${params.dir}/session`} icon="plus-small">
+ New session
+ </Button>
+ </div>
+ <div class="flex items-center gap-4">
+ <Tooltip
+ class="shrink-0"
+ value={
+ <div class="flex items-center gap-2">
+ <span>Toggle terminal</span>
+ <span class="text-icon-base text-12-medium">Ctrl `</span>
+ </div>
+ }
+ >
+ <Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
+ <div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
+ <Icon
+ size="small"
+ name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
+ class="group-hover/terminal-toggle:hidden"
+ />
+ <Icon
+ size="small"
+ name="layout-bottom-partial"
+ class="hidden group-hover/terminal-toggle:inline-block"
+ />
+ <Icon
+ size="small"
+ name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
+ class="hidden group-active/terminal-toggle:inline-block"
+ />
+ </div>
+ </Button>
+ </Tooltip>
+ </div>
+ </div>
</header>
<div class="h-[calc(100vh-3rem)] flex">
<div
classList={{
- "@container w-12 pb-5 shrink-0 bg-background-base": true,
+ "relative @container w-12 pb-5 shrink-0 bg-background-base": true,
"flex flex-col gap-5.5 items-start self-stretch justify-between": true,
"border-r border-border-weak-base": true,
}}
style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
>
+ <Show when={layout.sidebar.opened()}>
+ <div
+ class="absolute inset-y-0 right-0 z-10 w-2 translate-x-1/2 cursor-ew-resize"
+ onMouseDown={(e) => {
+ e.preventDefault()
+ const startX = e.clientX
+ const startWidth = layout.sidebar.width()
+ const maxWidth = window.innerWidth * 0.3
+ const minWidth = 150
+ const collapseThreshold = 80
+ let currentWidth = startWidth
+
+ document.body.style.userSelect = "none"
+ document.body.style.overflow = "hidden"
+
+ const onMouseMove = (moveEvent: MouseEvent) => {
+ const deltaX = moveEvent.clientX - startX
+ currentWidth = startWidth + deltaX
+ const clampedWidth = Math.min(maxWidth, Math.max(minWidth, currentWidth))
+ layout.sidebar.resize(clampedWidth)
+ }
+
+ const onMouseUp = () => {
+ document.body.style.userSelect = ""
+ document.body.style.overflow = ""
+ document.removeEventListener("mousemove", onMouseMove)
+ document.removeEventListener("mouseup", onMouseUp)
+
+ if (currentWidth < collapseThreshold) {
+ layout.sidebar.close()
+ }
+ }
+
+ document.addEventListener("mousemove", onMouseMove)
+ document.addEventListener("mouseup", onMouseUp)
+ }}
+ />
+ </Show>
<div class="grow flex flex-col items-start self-stretch gap-4 p-2 min-h-0">
<Tooltip class="shrink-0" placement="right" value="Toggle sidebar" inactive={layout.sidebar.opened()}>
<Button
@@ -197,7 +301,7 @@ export default function Layout(props: ParentProps) {
</Tooltip>
</div>
</div>
- <main class="size-full overflow-x-hidden">{props.children}</main>
+ <main class="size-full overflow-x-hidden flex flex-col items-start">{props.children}</main>
</div>
</div>
)
diff --git a/packages/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx
index d6ce62b70..773625334 100644
--- a/packages/desktop/src/pages/session.tsx
+++ b/packages/desktop/src/pages/session.tsx
@@ -1,4 +1,4 @@
-import { For, onCleanup, onMount, Show, Match, Switch, createResource, createMemo } from "solid-js"
+import { For, onCleanup, onMount, Show, Match, Switch, createResource, createMemo, createEffect } from "solid-js"
import { useLocal, type LocalFile } from "@/context/local"
import { createStore } from "solid-js/store"
import { PromptInput } from "@/components/prompt-input"
@@ -31,6 +31,7 @@ import { useSession } from "@/context/session"
import { useLayout } from "@/context/layout"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { Diff } from "@opencode-ai/ui/diff"
+import { Terminal } from "@/components/terminal"
export default function Page() {
const layout = useLayout()
@@ -54,6 +55,14 @@ export default function Page() {
document.removeEventListener("keydown", handleKeyDown)
})
+ createEffect(() => {
+ if (layout.terminal.opened()) {
+ if (session.terminal.all().length === 0) {
+ session.terminal.new()
+ }
+ }
+ })
+
const handleKeyDown = (event: KeyboardEvent) => {
if (event.getModifierState(MOD) && event.shiftKey && event.key.toLowerCase() === "p") {
event.preventDefault()
@@ -73,6 +82,16 @@ export default function Page() {
document.documentElement.setAttribute("data-theme", nextTheme)
return
}
+ if (event.ctrlKey && event.key.toLowerCase() === "`") {
+ event.preventDefault()
+ layout.terminal.toggle()
+ return
+ }
+
+ // @ts-expect-error
+ if (document.activeElement?.dataset?.component === "terminal") {
+ return
+ }
const focused = document.activeElement === inputRef
if (focused) {
@@ -141,7 +160,7 @@ export default function Page() {
const handleDragOver = (event: DragEvent) => {
const { draggable, droppable } = event
if (draggable && droppable) {
- const currentTabs = session.layout.tabs.opened
+ const currentTabs = session.layout.tabs.all
const fromIndex = currentTabs?.indexOf(draggable.id.toString())
const toIndex = currentTabs?.indexOf(droppable.id.toString())
if (fromIndex !== toIndex && toIndex !== undefined) {
@@ -259,317 +278,397 @@ export default function Page() {
const wide = createMemo(() => layout.review.state() === "tab" || !session.diffs().length)
return (
- <div class="relative bg-background-base size-full overflow-x-hidden">
- <DragDropProvider
- onDragStart={handleDragStart}
- onDragEnd={handleDragEnd}
- onDragOver={handleDragOver}
- collisionDetector={closestCenter}
- >
- <DragDropSensors />
- <ConstrainDragYAxis />
- <Tabs value={session.layout.tabs.active ?? "chat"} onChange={session.layout.openTab}>
- <div class="sticky top-0 shrink-0 flex">
- <Tabs.List>
- <Tabs.Trigger value="chat">
- <div class="flex gap-x-[17px] items-center">
- <div>Session</div>
- <Tooltip
- value={`${new Intl.NumberFormat("en-US", {
- notation: "compact",
- compactDisplay: "short",
- }).format(session.usage.tokens() ?? 0)} Tokens`}
- class="flex items-center gap-1.5"
+ <div class="relative bg-background-base size-full overflow-x-hidden flex flex-col items-start">
+ <div class="min-h-0 grow w-full">
+ <DragDropProvider
+ onDragStart={handleDragStart}
+ onDragEnd={handleDragEnd}
+ onDragOver={handleDragOver}
+ collisionDetector={closestCenter}
+ >
+ <DragDropSensors />
+ <ConstrainDragYAxis />
+ <Tabs value={session.layout.tabs.active ?? "chat"} onChange={session.layout.openTab}>
+ <div class="sticky top-0 shrink-0 flex">
+ <Tabs.List>
+ <Tabs.Trigger value="chat">
+ <div class="flex gap-x-[17px] items-center">
+ <div>Session</div>
+ <Tooltip
+ value={`${new Intl.NumberFormat("en-US", {
+ notation: "compact",
+ compactDisplay: "short",
+ }).format(session.usage.tokens() ?? 0)} Tokens`}
+ class="flex items-center gap-1.5"
+ >
+ <ProgressCircle percentage={session.usage.context() ?? 0} />
+ <div class="text-14-regular text-text-weak text-left w-7">{session.usage.context() ?? 0}%</div>
+ </Tooltip>
+ </div>
+ </Tabs.Trigger>
+ <Show when={layout.review.state() === "tab" && session.diffs().length}>
+ <Tabs.Trigger
+ value="review"
+ closeButton={
+ <IconButton icon="collapse" size="normal" variant="ghost" onClick={layout.review.pane} />
+ }
>
- <ProgressCircle percentage={session.usage.context() ?? 0} />
- <div class="text-14-regular text-text-weak text-left w-7">{session.usage.context() ?? 0}%</div>
- </Tooltip>
- </div>
- </Tabs.Trigger>
- <Show when={layout.review.state() === "tab" && session.diffs().length}>
- <Tabs.Trigger
- value="review"
- closeButton={
- <IconButton icon="collapse" size="normal" variant="ghost" onClick={layout.review.pane} />
- }
- >
- <div class="flex items-center gap-3">
- <Show when={session.diffs()}>
- <DiffChanges changes={session.diffs()} variant="bars" />
- </Show>
- <div class="flex items-center gap-1.5">
- <div>Review</div>
- <Show when={session.info()?.summary?.files}>
- <div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base">
- {session.info()?.summary?.files ?? 0}
- </div>
+ <div class="flex items-center gap-3">
+ <Show when={session.diffs()}>
+ <DiffChanges changes={session.diffs()} variant="bars" />
</Show>
+ <div class="flex items-center gap-1.5">
+ <div>Review</div>
+ <Show when={session.info()?.summary?.files}>
+ <div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base">
+ {session.info()?.summary?.files ?? 0}
+ </div>
+ </Show>
+ </div>
</div>
- </div>
- </Tabs.Trigger>
- </Show>
- <SortableProvider ids={session.layout.tabs.opened ?? []}>
- <For each={session.layout.tabs.opened ?? []}>
- {(tab) => <SortableTab tab={tab} onTabClick={handleTabClick} onTabClose={session.layout.closeTab} />}
- </For>
- </SortableProvider>
- <div class="bg-background-base h-full flex items-center justify-center border-b border-border-weak-base px-3">
- <Tooltip value="Open file" class="flex items-center">
- <IconButton
- icon="plus-small"
- variant="ghost"
- iconSize="large"
- onClick={() => setStore("fileSelectOpen", true)}
- />
- </Tooltip>
- </div>
- </Tabs.List>
- </div>
- <Tabs.Content value="chat" class="@container select-text flex flex-col flex-1 min-h-0 overflow-y-hidden">
- <div
- classList={{
- "w-full flex-1 min-h-0": true,
- grid: layout.review.state() === "tab",
- flex: layout.review.state() === "pane",
- }}
- >
+ </Tabs.Trigger>
+ </Show>
+ <SortableProvider ids={session.layout.tabs.all ?? []}>
+ <For each={session.layout.tabs.all ?? []}>
+ {(tab) => (
+ <SortableTab tab={tab} onTabClick={handleTabClick} onTabClose={session.layout.closeTab} />
+ )}
+ </For>
+ </SortableProvider>
+ <div class="bg-background-base h-full flex items-center justify-center border-b border-border-weak-base px-3">
+ <Tooltip value="Open file" class="flex items-center">
+ <IconButton
+ icon="plus-small"
+ variant="ghost"
+ iconSize="large"
+ onClick={() => setStore("fileSelectOpen", true)}
+ />
+ </Tooltip>
+ </div>
+ </Tabs.List>
+ </div>
+ <Tabs.Content value="chat" class="@container select-text flex flex-col flex-1 min-h-0 overflow-y-hidden">
<div
classList={{
- "relative shrink-0 py-3 flex flex-col gap-6 flex-1 min-h-0 w-full": true,
- "max-w-146 mx-auto": !wide(),
+ "w-full flex-1 min-h-0": true,
+ grid: layout.review.state() === "tab",
+ flex: layout.review.state() === "pane",
}}
>
- <Switch>
- <Match when={session.id}>
- <div class="flex items-start justify-start h-full min-h-0">
- <SessionMessageRail
- messages={session.messages.user()}
- current={session.messages.active()}
- onMessageSelect={session.messages.setActive}
- working={session.working()}
- wide={wide()}
- />
- <SessionTurn
- sessionID={session.id!}
- messageID={session.messages.active()?.id!}
- classes={{
- root: "pb-20 flex-1 min-w-0",
- content: "pb-20",
- container:
- "w-full " +
- (wide()
- ? "max-w-146 mx-auto px-6"
- : session.messages.user().length > 1
- ? "pr-6 pl-18"
- : "px-6"),
- }}
- diffComponent={Diff}
- />
- </div>
- </Match>
- <Match when={true}>
- <div class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-146 mx-auto px-6">
- <div class="text-20-medium text-text-weaker">New session</div>
- <div class="flex justify-center items-center gap-3">
- <Icon name="folder" size="small" />
- <div class="text-12-medium text-text-weak">
- {getDirectory(sync.data.path.directory)}
- <span class="text-text-strong">{getFilename(sync.data.path.directory)}</span>
- </div>
+ <div
+ classList={{
+ "relative shrink-0 py-3 flex flex-col gap-6 flex-1 min-h-0 w-full": true,
+ "max-w-146 mx-auto": !wide(),
+ }}
+ >
+ <Switch>
+ <Match when={session.id}>
+ <div class="flex items-start justify-start h-full min-h-0">
+ <SessionMessageRail
+ messages={session.messages.user()}
+ current={session.messages.active()}
+ onMessageSelect={session.messages.setActive}
+ working={session.working()}
+ wide={wide()}
+ />
+ <SessionTurn
+ sessionID={session.id!}
+ messageID={session.messages.active()?.id!}
+ classes={{
+ root: "pb-20 flex-1 min-w-0",
+ content: "pb-20",
+ container:
+ "w-full " +
+ (wide()
+ ? "max-w-146 mx-auto px-6"
+ : session.messages.user().length > 1
+ ? "pr-6 pl-18"
+ : "px-6"),
+ }}
+ diffComponent={Diff}
+ />
</div>
- <div class="flex justify-center items-center gap-3">
- <Icon name="pencil-line" size="small" />
- <div class="text-12-medium text-text-weak">
- Last modified&nbsp;
- <span class="text-text-strong">
- {DateTime.fromMillis(sync.data.project.time.created).toRelative()}
- </span>
+ </Match>
+ <Match when={true}>
+ <div class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-146 mx-auto px-6">
+ <div class="text-20-medium text-text-weaker">New session</div>
+ <div class="flex justify-center items-center gap-3">
+ <Icon name="folder" size="small" />
+ <div class="text-12-medium text-text-weak">
+ {getDirectory(sync.data.path.directory)}
+ <span class="text-text-strong">{getFilename(sync.data.path.directory)}</span>
+ </div>
+ </div>
+ <div class="flex justify-center items-center gap-3">
+ <Icon name="pencil-line" size="small" />
+ <div class="text-12-medium text-text-weak">
+ Last modified&nbsp;
+ <span class="text-text-strong">
+ {DateTime.fromMillis(sync.data.project.time.created).toRelative()}
+ </span>
+ </div>
</div>
</div>
+ </Match>
+ </Switch>
+ <div class="absolute inset-x-0 bottom-8 flex flex-col justify-center items-center z-50">
+ <div class="w-full max-w-146 px-6">
+ <PromptInput
+ ref={(el) => {
+ inputRef = el
+ }}
+ />
</div>
- </Match>
- </Switch>
- <div class="absolute inset-x-0 bottom-8 flex flex-col justify-center items-center z-50">
- <div class="w-full max-w-146 px-6">
- <PromptInput
- ref={(el) => {
- inputRef = el
+ </div>
+ </div>
+ <Show when={layout.review.state() === "pane" && session.diffs().length}>
+ <div
+ classList={{
+ "relative grow pt-3 flex-1 min-h-0 border-l border-border-weak-base": true,
+ }}
+ >
+ <SessionReview
+ classes={{
+ root: "pb-20",
+ header: "px-6",
+ container: "px-6",
}}
+ diffs={session.diffs()}
+ diffComponent={Diff}
+ actions={
+ <Tooltip value="Open in tab">
+ <IconButton
+ icon="expand"
+ variant="ghost"
+ onClick={() => {
+ layout.review.tab()
+ session.layout.setActiveTab("review")
+ }}
+ />
+ </Tooltip>
+ }
/>
</div>
- </div>
+ </Show>
</div>
- <Show when={layout.review.state() === "pane" && session.diffs().length}>
+ </Tabs.Content>
+ <Show when={layout.review.state() === "tab" && session.diffs().length}>
+ <Tabs.Content value="review" class="select-text flex flex-col h-full overflow-hidden">
<div
classList={{
- "relative grow pt-3 flex-1 min-h-0 border-l border-border-weak-base": true,
+ "relative pt-3 flex-1 min-h-0 overflow-hidden": true,
}}
>
<SessionReview
classes={{
- root: "pb-20",
+ root: "pb-40",
header: "px-6",
container: "px-6",
}}
diffs={session.diffs()}
diffComponent={Diff}
- actions={
- <Tooltip value="Open in tab">
- <IconButton
- icon="expand"
- variant="ghost"
- onClick={() => {
- layout.review.tab()
- session.layout.setActiveTab("review")
- }}
- />
- </Tooltip>
- }
+ split
/>
</div>
- </Show>
- </div>
- </Tabs.Content>
- <Show when={layout.review.state() === "tab" && session.diffs().length}>
- <Tabs.Content value="review" class="select-text flex flex-col h-full overflow-hidden">
+ </Tabs.Content>
+ </Show>
+ <For each={session.layout.tabs.all}>
+ {(tab) => {
+ const [file] = createResource(
+ () => tab,
+ async (tab) => {
+ if (tab.startsWith("file://")) {
+ return local.file.node(tab.replace("file://", ""))
+ }
+ return undefined
+ },
+ )
+ return (
+ <Tabs.Content value={tab} class="select-text mt-3">
+ <Switch>
+ <Match when={file()}>
+ {(f) => (
+ <Code
+ file={{ name: f().path, contents: f().content?.content ?? "" }}
+ overflow="scroll"
+ class="pb-40"
+ />
+ )}
+ </Match>
+ </Switch>
+ </Tabs.Content>
+ )
+ }}
+ </For>
+ </Tabs>
+ <DragOverlay>
+ <Show when={store.activeDraggable}>
+ {(draggedFile) => {
+ const [file] = createResource(
+ () => draggedFile(),
+ async (tab) => {
+ if (tab.startsWith("file://")) {
+ return local.file.node(tab.replace("file://", ""))
+ }
+ return undefined
+ },
+ )
+ return (
+ <div class="relative px-6 h-12 flex items-center bg-background-stronger border-x border-border-weak-base border-b border-b-transparent">
+ <Show when={file()}>{(f) => <FileVisual active file={f()} />}</Show>
+ </div>
+ )
+ }}
+ </Show>
+ </DragOverlay>
+ </DragDropProvider>
+ <Show when={session.layout.tabs.active}>
+ <div class="absolute inset-x-0 px-6 max-w-146 flex flex-col justify-center items-center z-50 mx-auto bottom-8">
+ <PromptInput
+ ref={(el) => {
+ inputRef = el
+ }}
+ />
+ </div>
+ </Show>
+ <div class="hidden shrink-0 w-56 p-2 h-full overflow-y-auto">
+ {/* <FileTree path="" onFileClick={ handleTabClick} /> */}
+ </div>
+ <div class="hidden shrink-0 w-56 p-2">
+ <Show
+ when={local.file.changes().length}
+ fallback={<div class="px-2 text-xs text-text-muted">No changes</div>}
+ >
+ <ul class="">
+ <For each={local.file.changes()}>
+ {(path) => (
+ <li>
+ <button
+ onClick={() => local.file.open(path, { view: "diff-unified", pinned: true })}
+ class="w-full flex items-center px-2 py-0.5 gap-x-2 text-text-muted grow min-w-0 hover:bg-background-element"
+ >
+ <FileIcon node={{ path, type: "file" }} class="shrink-0 size-3" />
+ <span class="text-xs text-text whitespace-nowrap">{getFilename(path)}</span>
+ <span class="text-xs text-text-muted/60 whitespace-nowrap truncate min-w-0">
+ {getDirectory(path)}
+ </span>
+ </button>
+ </li>
+ )}
+ </For>
+ </ul>
+ </Show>
+ </div>
+ <Show when={store.fileSelectOpen}>
+ <SelectDialog
+ defaultOpen
+ title="Select file"
+ placeholder="Search files"
+ emptyMessage="No files found"
+ items={local.file.searchFiles}
+ key={(x) => x}
+ onOpenChange={(open) => setStore("fileSelectOpen", open)}
+ onSelect={(x) => {
+ if (x) {
+ local.file.open(x)
+ return session.layout.openTab("file://" + x)
+ }
+ return undefined
+ }}
+ >
+ {(i) => (
<div
classList={{
- "relative pt-3 flex-1 min-h-0 overflow-hidden": true,
+ "w-full flex items-center justify-between rounded-md": true,
}}
>
- <SessionReview
- classes={{
- root: "pb-40",
- header: "px-6",
- container: "px-6",
- }}
- diffs={session.diffs()}
- diffComponent={Diff}
- split
- />
- </div>
- </Tabs.Content>
- </Show>
- <For each={session.layout.tabs.opened}>
- {(tab) => {
- const [file] = createResource(
- () => tab,
- async (tab) => {
- if (tab.startsWith("file://")) {
- return local.file.node(tab.replace("file://", ""))
- }
- return undefined
- },
- )
- return (
- <Tabs.Content value={tab} class="select-text mt-3">
- <Switch>
- <Match when={file()}>
- {(f) => (
- <Code
- file={{ name: f().path, contents: f().content?.content ?? "" }}
- overflow="scroll"
- class="pb-40"
- />
- )}
- </Match>
- </Switch>
- </Tabs.Content>
- )
- }}
- </For>
- </Tabs>
- <DragOverlay>
- <Show when={store.activeDraggable}>
- {(draggedFile) => {
- const [file] = createResource(
- () => draggedFile(),
- async (tab) => {
- if (tab.startsWith("file://")) {
- return local.file.node(tab.replace("file://", ""))
- }
- return undefined
- },
- )
- return (
- <div class="relative px-6 h-12 flex items-center bg-background-stronger border-x border-border-weak-base border-b border-b-transparent">
- <Show when={file()}>{(f) => <FileVisual active file={f()} />}</Show>
+ <div class="flex items-center gap-x-2 grow min-w-0">
+ <FileIcon node={{ path: i, type: "file" }} class="shrink-0 size-4" />
+ <div class="flex items-center text-14-regular">
+ <span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
+ {getDirectory(i)}
+ </span>
+ <span class="text-text-strong whitespace-nowrap">{getFilename(i)}</span>
+ </div>
</div>
- )
- }}
- </Show>
- </DragOverlay>
- </DragDropProvider>
- <Show when={session.layout.tabs.active}>
- <div class="absolute inset-x-0 px-6 max-w-146 flex flex-col justify-center items-center z-50 mx-auto bottom-8">
- <PromptInput
- ref={(el) => {
- inputRef = el
+ <div class="flex items-center gap-x-1 text-text-muted/40 shrink-0"></div>
+ </div>
+ )}
+ </SelectDialog>
+ </Show>
+ </div>
+ <Show when={layout.terminal.opened()}>
+ <div
+ class="relative w-full flex flex-col shrink-0 border-t border-border-weak-base"
+ style={{ height: `${layout.terminal.height()}px` }}
+ >
+ <div
+ class="absolute inset-x-0 top-0 z-10 h-2 -translate-y-1/2 cursor-ns-resize"
+ onMouseDown={(e) => {
+ e.preventDefault()
+ const startY = e.clientY
+ const startHeight = layout.terminal.height()
+ const maxHeight = window.innerHeight * 0.6
+ const minHeight = 100
+ const collapseThreshold = 50
+ let currentHeight = startHeight
+
+ document.body.style.userSelect = "none"
+ document.body.style.overflow = "hidden"
+
+ const onMouseMove = (moveEvent: MouseEvent) => {
+ const deltaY = startY - moveEvent.clientY
+ currentHeight = startHeight + deltaY
+ const clampedHeight = Math.min(maxHeight, Math.max(minHeight, currentHeight))
+ layout.terminal.resize(clampedHeight)
+ }
+
+ const onMouseUp = () => {
+ document.body.style.userSelect = ""
+ document.body.style.overflow = ""
+ document.removeEventListener("mousemove", onMouseMove)
+ document.removeEventListener("mouseup", onMouseUp)
+
+ if (currentHeight < collapseThreshold) {
+ layout.terminal.close()
+ }
+ }
+
+ document.addEventListener("mousemove", onMouseMove)
+ document.addEventListener("mouseup", onMouseUp)
}}
/>
- </div>
- </Show>
- <div class="hidden shrink-0 w-56 p-2 h-full overflow-y-auto">
- {/* <FileTree path="" onFileClick={ handleTabClick} /> */}
- </div>
- <div class="hidden shrink-0 w-56 p-2">
- <Show when={local.file.changes().length} fallback={<div class="px-2 text-xs text-text-muted">No changes</div>}>
- <ul class="">
- <For each={local.file.changes()}>
- {(path) => (
- <li>
- <button
- onClick={() => local.file.open(path, { view: "diff-unified", pinned: true })}
- class="w-full flex items-center px-2 py-0.5 gap-x-2 text-text-muted grow min-w-0 hover:bg-background-element"
+ <Tabs variant="alt" value={session.terminal.active()} onChange={session.terminal.open}>
+ <Tabs.List class="h-10">
+ <For each={session.terminal.all()}>
+ {(terminal) => (
+ <Tabs.Trigger
+ value={terminal.id}
+ closeButton={
+ session.terminal.all().length > 1 && (
+ <IconButton icon="close" variant="ghost" onClick={() => session.terminal.close(terminal.id)} />
+ )
+ }
>
- <FileIcon node={{ path, type: "file" }} class="shrink-0 size-3" />
- <span class="text-xs text-text whitespace-nowrap">{getFilename(path)}</span>
- <span class="text-xs text-text-muted/60 whitespace-nowrap truncate min-w-0">
- {getDirectory(path)}
- </span>
- </button>
- </li>
+ {terminal.title}
+ </Tabs.Trigger>
+ )}
+ </For>
+ <div class="h-full flex items-center justify-center">
+ <Tooltip value="Open file" class="flex items-center">
+ <IconButton icon="plus-small" variant="ghost" iconSize="large" onClick={session.terminal.new} />
+ </Tooltip>
+ </div>
+ </Tabs.List>
+ <For each={session.terminal.all()}>
+ {(terminal) => (
+ <Tabs.Content value={terminal.id}>
+ <Terminal pty={terminal} onCleanup={session.terminal.update} />
+ </Tabs.Content>
)}
</For>
- </ul>
- </Show>
- </div>
- <Show when={store.fileSelectOpen}>
- <SelectDialog
- defaultOpen
- title="Select file"
- placeholder="Search files"
- emptyMessage="No files found"
- items={local.file.searchFiles}
- key={(x) => x}
- onOpenChange={(open) => setStore("fileSelectOpen", open)}
- onSelect={(x) => {
- if (x) {
- local.file.open(x)
- return session.layout.openTab("file://" + x)
- }
- return undefined
- }}
- >
- {(i) => (
- <div
- classList={{
- "w-full flex items-center justify-between rounded-md": true,
- }}
- >
- <div class="flex items-center gap-x-2 grow min-w-0">
- <FileIcon node={{ path: i, type: "file" }} class="shrink-0 size-4" />
- <div class="flex items-center text-14-regular">
- <span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
- {getDirectory(i)}
- </span>
- <span class="text-text-strong whitespace-nowrap">{getFilename(i)}</span>
- </div>
- </div>
- <div class="flex items-center gap-x-1 text-text-muted/40 shrink-0"></div>
- </div>
- )}
- </SelectDialog>
+ </Tabs>
+ </div>
</Show>
</div>
)