summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/pages
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-01-02 20:17:02 -0600
committerGitHub <[email protected]>2026-01-02 20:17:02 -0600
commit052de3c556097a983c7c27e9a9a9cbc5d22b13c5 (patch)
tree6b2f8d7d74e1027a110f8ee17ab9fd538c25ccfd /packages/app/src/pages
parentf6fe709f6ee75427ba64829af25b64d9a3111569 (diff)
downloadopencode-052de3c556097a983c7c27e9a9a9cbc5d22b13c5.tar.gz
opencode-052de3c556097a983c7c27e9a9a9cbc5d22b13c5.zip
feat: add managed git worktrees (#6674)
Diffstat (limited to 'packages/app/src/pages')
-rw-r--r--packages/app/src/pages/layout.tsx170
-rw-r--r--packages/app/src/pages/session.tsx28
2 files changed, 108 insertions, 90 deletions
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx
index e237d2184..91df9259e 100644
--- a/packages/app/src/pages/layout.tsx
+++ b/packages/app/src/pages/layout.tsx
@@ -172,9 +172,9 @@ export default function Layout(props: ParentProps) {
const perm = e.details.properties
if (permission.autoResponds(perm)) return
- const sessionKey = `${directory}:${perm.sessionID}`
const [store] = globalSync.child(directory)
const session = store.session.find((s) => s.id === perm.sessionID)
+ const sessionKey = `${directory}:${perm.sessionID}`
const sessionTitle = session?.title ?? "New session"
const projectName = getFilename(directory)
@@ -665,14 +665,13 @@ export default function Layout(props: ParentProps) {
<>
<div
data-session-id={props.session.id}
- class="group/session relative w-full pr-2 py-1 rounded-md cursor-default transition-colors
+ class="group/session relative w-full rounded-md cursor-default transition-colors
hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-raised-base-hover"
- style={{ "padding-left": "16px" }}
>
<Tooltip placement={props.mobile ? "bottom" : "right"} value={props.session.title} gutter={10}>
<A
href={`${props.slug}/session/${props.session.id}`}
- class="flex flex-col min-w-0 text-left w-full focus:outline-none"
+ class="flex flex-col min-w-0 text-left w-full focus:outline-none pl-4 pr-2 py-1"
>
<div class="flex items-center self-stretch gap-6 justify-between transition-[padding] group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7">
<span
@@ -740,10 +739,17 @@ export default function Layout(props: ParentProps) {
const SortableProject = (props: { project: LocalProject; mobile?: boolean }): JSX.Element => {
const sortable = createSortable(props.project.worktree)
const showExpanded = createMemo(() => props.mobile || layout.sidebar.opened())
- const slug = createMemo(() => base64Encode(props.project.worktree))
+ const defaultWorktree = createMemo(() => base64Encode(props.project.worktree))
const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
const [store, setProjectStore] = globalSync.child(props.project.worktree)
- const sessions = createMemo(() => store.session.toSorted(sortSessions))
+ const stores = createMemo(() =>
+ [props.project.worktree, ...(props.project.sandboxes ?? [])].map((dir) => globalSync.child(dir)[0]),
+ )
+ const sessions = createMemo(() =>
+ stores()
+ .flatMap((store) => store.session.filter((session) => session.directory === store.path.directory))
+ .toSorted(sortSessions),
+ )
const rootSessions = createMemo(() => sessions().filter((s) => !s.parentID))
const hasMoreSessions = createMemo(() => store.session.length >= store.limit)
const loadMoreSessions = async () => {
@@ -799,7 +805,7 @@ export default function Layout(props: ParentProps) {
</DropdownMenu.Portal>
</DropdownMenu>
<TooltipKeybind placement="top" title="New session" keybind={command.keybind("session.new")}>
- <IconButton as={A} href={`${slug()}/session`} icon="plus-small" variant="ghost" />
+ <IconButton as={A} href={`${defaultWorktree()}/session`} icon="plus-small" variant="ghost" />
</TooltipKeybind>
</div>
</Button>
@@ -807,7 +813,12 @@ export default function Layout(props: ParentProps) {
<nav class="hidden @[4rem]:flex w-full flex-col gap-1.5">
<For each={rootSessions()}>
{(session) => (
- <SessionItem session={session} slug={slug()} project={props.project} mobile={props.mobile} />
+ <SessionItem
+ session={session}
+ slug={base64Encode(session.directory)}
+ project={props.project}
+ mobile={props.mobile}
+ />
)}
</For>
<Show when={rootSessions().length === 0}>
@@ -819,7 +830,7 @@ export default function Layout(props: ParentProps) {
<div class="flex-1 min-w-0">
<Tooltip placement={props.mobile ? "bottom" : "right"} value="New session">
<A
- href={`${slug()}/session`}
+ href={`${defaultWorktree()}/session`}
class="flex flex-col gap-1 min-w-0 text-left w-full focus:outline-none"
>
<div class="flex items-center self-stretch gap-6 justify-between">
@@ -875,76 +886,85 @@ export default function Layout(props: ParentProps) {
const SidebarContent = (sidebarProps: { mobile?: boolean }) => {
const expanded = () => sidebarProps.mobile || layout.sidebar.opened()
return (
- <>
- <div class="flex flex-col items-start self-stretch gap-4 p-2 min-h-0 overflow-hidden">
+ <div class="flex flex-col self-stretch h-full items-center justify-between overflow-hidden min-h-0">
+ <div class="flex flex-col items-start self-stretch gap-4 min-h-0">
<Show when={!sidebarProps.mobile}>
- <A href="/" class="shrink-0 h-8 flex items-center justify-start px-2" data-tauri-drag-region>
- <Mark class="shrink-0" />
- </A>
- </Show>
- <Show when={!sidebarProps.mobile}>
- <TooltipKeybind
- class="shrink-0"
- placement="right"
- title="Toggle sidebar"
- keybind={command.keybind("sidebar.toggle")}
- inactive={expanded()}
- >
- <Button
- variant="ghost"
- size="large"
- class="group/sidebar-toggle shrink-0 w-full text-left justify-start rounded-lg px-2"
- onClick={layout.sidebar.toggle}
- >
- <div class="relative -ml-px flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
- <Icon
- name={layout.sidebar.opened() ? "layout-left" : "layout-right"}
- size="small"
- class="group-hover/sidebar-toggle:hidden"
- />
- <Icon
- name={layout.sidebar.opened() ? "layout-left-partial" : "layout-right-partial"}
- size="small"
- class="hidden group-hover/sidebar-toggle:inline-block"
- />
- <Icon
- name={layout.sidebar.opened() ? "layout-left-full" : "layout-right-full"}
- size="small"
- class="hidden group-active/sidebar-toggle:inline-block"
- />
- </div>
- <Show when={layout.sidebar.opened()}>
- <div class="hidden group-hover/sidebar-toggle:block group-active/sidebar-toggle:block text-text-base">
- Toggle sidebar
- </div>
- </Show>
- </Button>
- </TooltipKeybind>
- </Show>
- <DragDropProvider
- onDragStart={handleDragStart}
- onDragEnd={handleDragEnd}
- onDragOver={handleDragOver}
- collisionDetector={closestCenter}
- >
- <DragDropSensors />
- <ConstrainDragXAxis />
<div
- ref={(el) => {
- if (!sidebarProps.mobile) scrollContainerRef = el
+ classList={{
+ "border-b border-border-weak-base w-full h-12 ml-px flex items-center pl-1.75 shrink-0": true,
+ "justify-start": expanded(),
}}
- class="w-full min-w-8 flex flex-col gap-2 min-h-0 overflow-y-auto no-scrollbar"
>
- <SortableProvider ids={layout.projects.list().map((p) => p.worktree)}>
- <For each={layout.projects.list()}>
- {(project) => <SortableProject project={project} mobile={sidebarProps.mobile} />}
- </For>
- </SortableProvider>
+ <A href="/" class="shrink-0 h-8 flex items-center justify-start px-2 w-full" data-tauri-drag-region>
+ <Mark class="shrink-0" />
+ </A>
</div>
- <DragOverlay>
- <ProjectDragOverlay />
- </DragOverlay>
- </DragDropProvider>
+ </Show>
+ <div class="flex flex-col items-start self-stretch gap-4 px-2 overflow-hidden min-h-0">
+ <Show when={!sidebarProps.mobile}>
+ <TooltipKeybind
+ class="shrink-0"
+ placement="right"
+ title="Toggle sidebar"
+ keybind={command.keybind("sidebar.toggle")}
+ inactive={expanded()}
+ >
+ <Button
+ variant="ghost"
+ size="large"
+ class="group/sidebar-toggle shrink-0 w-full text-left justify-start rounded-lg px-2"
+ onClick={layout.sidebar.toggle}
+ >
+ <div class="relative -ml-px flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
+ <Icon
+ name={layout.sidebar.opened() ? "layout-left" : "layout-right"}
+ size="small"
+ class="group-hover/sidebar-toggle:hidden"
+ />
+ <Icon
+ name={layout.sidebar.opened() ? "layout-left-partial" : "layout-right-partial"}
+ size="small"
+ class="hidden group-hover/sidebar-toggle:inline-block"
+ />
+ <Icon
+ name={layout.sidebar.opened() ? "layout-left-full" : "layout-right-full"}
+ size="small"
+ class="hidden group-active/sidebar-toggle:inline-block"
+ />
+ </div>
+ <Show when={layout.sidebar.opened()}>
+ <div class="hidden group-hover/sidebar-toggle:block group-active/sidebar-toggle:block text-text-base">
+ Toggle sidebar
+ </div>
+ </Show>
+ </Button>
+ </TooltipKeybind>
+ </Show>
+ <DragDropProvider
+ onDragStart={handleDragStart}
+ onDragEnd={handleDragEnd}
+ onDragOver={handleDragOver}
+ collisionDetector={closestCenter}
+ >
+ <DragDropSensors />
+ <ConstrainDragXAxis />
+ <div
+ ref={(el) => {
+ if (!sidebarProps.mobile) scrollContainerRef = el
+ }}
+ class="w-full min-w-8 flex flex-col gap-2 min-h-0 overflow-y-auto no-scrollbar"
+ >
+ <SortableProvider ids={layout.projects.list().map((p) => p.worktree)}>
+ <For each={layout.projects.list()}>
+ {(project) => <SortableProject project={project} mobile={sidebarProps.mobile} />}
+ </For>
+ </SortableProvider>
+ </div>
+ <DragOverlay>
+ <ProjectDragOverlay />
+ </DragOverlay>
+ </DragDropProvider>
+ </div>
</div>
<div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
<Switch>
@@ -1017,7 +1037,7 @@ export default function Layout(props: ParentProps) {
</Button>
</Tooltip>
</div>
- </>
+ </div>
)
}
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index 0a0cd519f..883565dd3 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -218,20 +218,12 @@ export default function Page() {
return sync.data.message[id] !== undefined
})
const emptyUserMessages: UserMessage[] = []
- const userMessages = createMemo(
- () => messages().filter((m) => m.role === "user") as UserMessage[],
- emptyUserMessages,
- { equals: same },
- )
- const visibleUserMessages = createMemo(
- () => {
- const revert = revertMessageID()
- if (!revert) return userMessages()
- return userMessages().filter((m) => m.id < revert)
- },
- emptyUserMessages,
- { equals: same },
- )
+ const userMessages = createMemo(() => messages().filter((m) => m.role === "user") as UserMessage[], emptyUserMessages)
+ const visibleUserMessages = createMemo(() => {
+ const revert = revertMessageID()
+ if (!revert) return userMessages()
+ return userMessages().filter((m) => m.id < revert)
+ }, emptyUserMessages)
const lastUserMessage = createMemo(() => visibleUserMessages().at(-1))
createEffect(
@@ -256,6 +248,7 @@ export default function Page() {
mobileTab: "session" as "session" | "review",
ignoreScrollSpy: false,
initialScrollDone: !params.id,
+ newSessionWorktree: "main",
})
const activeMessage = createMemo(() => {
@@ -1017,7 +1010,10 @@ export default function Page() {
</Show>
</Match>
<Match when={true}>
- <NewSessionView />
+ <NewSessionView
+ worktree={store.newSessionWorktree}
+ onWorktreeChange={(value) => setStore("newSessionWorktree", value)}
+ />
</Match>
</Switch>
</div>
@@ -1034,6 +1030,8 @@ export default function Page() {
ref={(el) => {
inputRef = el
}}
+ newSessionWorktree={store.newSessionWorktree}
+ onNewSessionWorktreeReset={() => setStore("newSessionWorktree", "main")}
/>
</div>
</div>