From 5ef2337a9634173d7cd5cdbd4d5d9461d9f62858 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 19 Oct 2026 00:05:33 -0400 Subject: [PATCH] feat(app): simplify new session location row Show one clickable project/worktree pill aligned to the composer grid, with the session path right-aligned. Remove the base-branch selector; new worktrees start from the current branch. --- packages/app/src/new-session/view.tsx | 186 ++++++++++-------- .../src/new-session/workspace/controller.ts | 31 --- .../src/new-session/workspace/selector.tsx | 129 ------------ 3 files changed, 108 insertions(+), 238 deletions(-) delete mode 100644 packages/app/src/new-session/workspace/selector.tsx diff --git a/packages/app/src/new-session/view.tsx b/packages/app/src/new-session/view.tsx index 892053f623f..5374b481c79 100644 --- a/packages/app/src/new-session/view.tsx +++ b/packages/app/src/new-session/view.tsx @@ -2,6 +2,8 @@ import { useDialog } from "@opencode/ui/context/dialog" import { Tooltip } from "@opencode/ui/tooltip" import { Icon } from "@opencode/ui/icon" import { Show, createMemo, createSignal } from "solid-js" +import { createResizeObserver } from "@solid-primitives/resize-observer" +import { getFilename } from "@opencode/util/path" import { displayName, getProjectAvatarSource, @@ -13,7 +15,6 @@ import createPresence from "solid-presence" import { Composer } from "@/composer/composer" import { ComposerDropzone } from "@/composer/dropzone" import type { ComposerModel } from "@/composer/model" -import { PromptBranchSelector } from "@/new-session/workspace/selector" import { PromptProjectAddButton, type PromptProject, @@ -78,14 +79,17 @@ export function NewSessionView(props: {
- - - - - - {(project) => ( -
- + + + {(project) => ( + - - {(branch) => ( - - )} - -
- )} + )} +
+
+ +
@@ -267,7 +257,8 @@ function NewSessionTips(props: { ) } -function NewSessionLocationTrigger(props: { +/** The tray under the Composer: the location picker on the left and the session's directory on the right. */ +function NewSessionLocation(props: { project: PromptProject /** "main", "create", or a worktree directory. */ workspace: string @@ -276,61 +267,100 @@ function NewSessionLocationTrigger(props: { onClick: () => void }) { const language = useLanguage() + const [truncated, setTruncated] = createSignal(false) - const path = () => { - if (props.workspace === "main") return props.root + const worktree = () => { + if (props.workspace === "main") return if (props.workspace === "create") return language.t("workspace.new") - return props.workspace + return getFilename(props.workspace) + } + + const path = () => { + if (props.workspace === "create") return + + return props.workspace === "main" ? props.root : props.workspace } return ( - -
- - {language.t("workspace.onboarding.title")} -
- {language.t("workspace.onboarding.description")} +
+
+ +
+ + {language.t("workspace.onboarding.title")} +
+ {language.t("workspace.onboarding.description")} +
+ ) : ( + language.t("session.new.workspace.trigger.tooltip") + ) + } + contentClass={props.onboarding ? "max-w-[280px]" : undefined} + class="flex min-w-0 max-w-full" + > + + +
+ + {(directory) => ( +
+ + + createResizeObserver(element, () => setTruncated(element.scrollWidth > element.clientWidth)) + } + data-slot="new-session-location-path" + class="block min-w-0 truncate text-v2-text-text-faint [direction:rtl] [text-align:end]" + > + {directory()} + +
- ) : ( - language.t("session.new.workspace.trigger.tooltip") - ) - } - contentClass={props.onboarding ? "max-w-[280px]" : undefined} - class="min-w-0" - > - -
+ )} + + ) } diff --git a/packages/app/src/new-session/workspace/controller.ts b/packages/app/src/new-session/workspace/controller.ts index 89f2b0d4728..30a51968a2f 100644 --- a/packages/app/src/new-session/workspace/controller.ts +++ b/packages/app/src/new-session/workspace/controller.ts @@ -1,4 +1,3 @@ -import { debounce } from "@solid-primitives/scheduled" import { createEffect, createMemo, createResource, onCleanup } from "solid-js" import { createStore } from "solid-js/store" import { useWorkspaceLocation } from "@/workspaces/location" @@ -62,12 +61,9 @@ export function createNewSessionWorkspaceController(input: { const tabs = useTabs() const [state, setState] = createStore({ - search: "", existing: undefined as { projectID: string; directory: string } | undefined, }) - const searchBranches = debounce((search: string) => setState("search", search.trim()), 100) - const currentProject = createMemo(() => { const projectID = data.location.info({ directory: sdk().directory })?.project.id const current = projectID ? data.project.get(projectID) : undefined @@ -175,15 +171,6 @@ export function createNewSessionWorkspaceController(input: { const projectRoot = createMemo(() => currentProject()?.worktree ?? sdk().directory) - const [branches] = createResource( - () => (visible() ? { directory: projectRoot(), search: state.search } : undefined), - ({ directory, search }) => - serverSDK.api.vcs.branch - .list({ location: { directory }, search, limit: 50 }) - .then((response) => ({ directory, search, data: response.data })) - .catch(() => ({ directory, search, data: [] })), - ) - createEffect(() => { void Promise.all([ data.location.syncInfo({ directory: sdk().directory }), @@ -263,30 +250,12 @@ export function createNewSessionWorkspaceController(input: { remember, set: select, cycle: () => select(cycleNewSessionWorktree({ current: value(), existing: existing() })), - create: (branch: string) => { - input.setSelectedBranch(branch) - input.setSelectedWorktree("create") - remember("create") - }, }, project: { root: projectRoot, workspaces: worktreeDirectories, managed: managedWorktrees, git: visible, - branches: () => { - const current = data.location.vcs.info({ directory: sdk().directory })?.branch.current - const loaded = branches.latest - const list = loaded?.directory === projectRoot() ? loaded.data : [] - - return [ - ...new Set([ - ...list, - ...(current && current.toLowerCase().includes(state.search.toLowerCase()) ? [current] : []), - ]), - ].slice(0, 50) - }, - searchBranches, }, bar: { visible, diff --git a/packages/app/src/new-session/workspace/selector.tsx b/packages/app/src/new-session/workspace/selector.tsx deleted file mode 100644 index 6ee542596e2..00000000000 --- a/packages/app/src/new-session/workspace/selector.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { createSignal, For, Show } from "solid-js" -import { createStore } from "solid-js/store" -import { createResizeObserver } from "@solid-primitives/resize-observer" -import { Menu } from "@opencode/ui/menu" -import { Tooltip } from "@opencode/ui/tooltip" -import { Icon } from "@opencode/ui/icon" -import { useLanguage } from "@/runtime/i18n/language" - -/** The base branch for a new worktree. */ -export function PromptBranchSelector(props: { - branch: string - branches: string[] - onCreate: (branch: string) => void - onSearch: (search: string) => void - onDone?: () => void -}) { - const language = useLanguage() - const [search, setSearch] = createStore({ branches: "" }) - const truncation = createTruncatedText() - let input: HTMLInputElement | undefined - let pending: string | undefined - - return ( - - { - if (open) { - setSearch({ branches: "" }) - props.onSearch("") - - return - } - - const branch = pending - pending = undefined - - if (branch) props.onCreate(branch) - - props.onDone?.() - }} - > - - - - {language.t("session.new.workspace.fromBranch", { branch: props.branch })} - - - - - { - event.preventDefault() - // Kobalte defers its list autofocus until after the focus scope opens. - setTimeout(() => requestAnimationFrame(() => input?.focus({ preventScroll: true }))) - }} - > -
- - { - setSearch("branches", event.currentTarget.value) - props.onSearch(event.currentTarget.value) - }} - onKeyDown={(event) => { - if (["Escape", "ArrowDown", "ArrowUp", "Enter"].includes(event.key)) return - event.stopPropagation() - }} - /> - - - -
-
- - - {(branch) => ( - (pending = branch)} - > - {branch} - - )} - - -
-
-
-
-
- ) -} - -function createTruncatedText() { - const [truncated, setTruncated] = createSignal(false) - - return { - truncated, - observe: (element: HTMLSpanElement) => - createResizeObserver(element, () => setTruncated(element.scrollWidth > element.clientWidth)), - } -}