diff options
| author | Adam <[email protected]> | 2026-01-02 20:17:02 -0600 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-01-02 20:17:02 -0600 |
| commit | 052de3c556097a983c7c27e9a9a9cbc5d22b13c5 (patch) | |
| tree | 6b2f8d7d74e1027a110f8ee17ab9fd538c25ccfd /packages/app/src/pages | |
| parent | f6fe709f6ee75427ba64829af25b64d9a3111569 (diff) | |
| download | opencode-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.tsx | 170 | ||||
| -rw-r--r-- | packages/app/src/pages/session.tsx | 28 |
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> |
