Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ file is the human-readable summary of what changed and why.

### Desktop / Web UI

- **The sidebar collapses, and Search opens the recents list** — two buttons join
the titlebar. The leftmost one hides and restores the sidebar, so a session can
have the full window width; it remembers the choice for next launch, and it
lights up while the sidebar is open. Note that Projects live only in the
sidebar, so switching project means opening it again. The second, next to
Home, opens the recent-sessions list exactly as **View all** does and drops the
cursor straight into its search field. Neither has a keyboard shortcut, so
nothing you already press has changed meaning. **View → Toggle Sidebar** in the
app menu now works too — it had no effect on this layout, since the command it
dispatches was only ever registered by the old one.

### Desktop / Web UI

- **The sidebar is always on screen, and it carries your recent sessions** —
the Projects list plus Settings and Help used to be a column belonging to the
home screen, so opening a session left you with no navigation but the tab bar.
Expand Down
4 changes: 2 additions & 2 deletions DIVERGENCES.md

Large diffs are not rendered by default.

244 changes: 125 additions & 119 deletions packages/app/src/components/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useServerManagementController } from "@/components/dialog-select-server
import { DialogServerV2 } from "@/components/settings-v2/dialog-server-v2"
import { useSettingsCommand } from "@/components/settings-dialog"
import { ChannelIndicator } from "@/components/titlebar"
import { useAppSidebar } from "@/context/app-sidebar"
import { useGlobal } from "@/context/global"
import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
Expand Down Expand Up @@ -54,6 +55,7 @@ export function AppSidebar(props: { class?: string; debugTools?: { visible: bool
const global = useGlobal()
const dialog = useDialog()
const model = createHomeModel()
const sidebar = useAppSidebar()
const openSettings = useSettingsCommand()
const controller = useServerManagementController({ navigateOnAdd: false })
const [_state, setState, _, ready] = persisted(
Expand All @@ -66,138 +68,142 @@ export function AppSidebar(props: { class?: string; debugTools?: { visible: bool
{ initialValue: _state },
)

// Collapsed hides the column outright rather than narrowing it to a rail, so
// the content pane takes the full width. The titlebar toggle is the way back.
return (
<aside
data-component="app-sidebar"
class={`${props.class ?? ""} w-[280px] min-h-0 min-w-0 shrink-0 flex-col gap-4 overflow-hidden px-3 pb-4 pt-6`}
aria-label={language.t("home.projects")}
>
<div class="flex h-7 min-w-0 shrink-0 items-center justify-between pl-1.5">
<div class="text-v2-text-text-muted [font-weight:530]">{language.t("home.projects")}</div>
<Show
when={
global.servers.list().length === 1 &&
!(model.projects().length === 0 && model.recentlyClosed().length > 0)
}
>
<TooltipV2 placement="bottom" value={language.t("home.project.add")}>
<IconButtonV2
data-action="home-add-project"
variant="ghost-muted"
size="large"
class="titlebar-icon [&_[data-slot=icon-svg]]:text-v2-icon-icon-muted"
icon={<IconV2 name="folder-add-left" />}
disabled={global.servers.health[ServerConnection.key(global.servers.list()[0]!)]?.healthy === false}
onClick={() => model.chooseProject(global.servers.list()[0]!)}
aria-label={language.t("home.project.add")}
/>
</TooltipV2>
</Show>
</div>
<ScrollView data-slot="home-projects-scroll" class="min-h-0 min-w-0 shrink">
<Show
when={global.servers.list().length > 1}
fallback={
<Show
when={model.projects().length > 0}
fallback={
<HomeProjectEmpty
<Show when={!sidebar?.collapsed()}>
<aside
data-component="app-sidebar"
class={`${props.class ?? ""} w-[280px] min-h-0 min-w-0 shrink-0 flex-col gap-4 overflow-hidden px-3 pb-4 pt-6`}
aria-label={language.t("home.projects")}
>
<div class="flex h-7 min-w-0 shrink-0 items-center justify-between pl-1.5">
<div class="text-v2-text-text-muted [font-weight:530]">{language.t("home.projects")}</div>
<Show
when={
global.servers.list().length === 1 &&
!(model.projects().length === 0 && model.recentlyClosed().length > 0)
}
>
<TooltipV2 placement="bottom" value={language.t("home.project.add")}>
<IconButtonV2
data-action="home-add-project"
variant="ghost-muted"
size="large"
class="titlebar-icon [&_[data-slot=icon-svg]]:text-v2-icon-icon-muted"
icon={<IconV2 name="folder-add-left" />}
disabled={global.servers.health[ServerConnection.key(global.servers.list()[0]!)]?.healthy === false}
onClick={() => model.chooseProject(global.servers.list()[0]!)}
aria-label={language.t("home.project.add")}
/>
</TooltipV2>
</Show>
</div>
<ScrollView data-slot="home-projects-scroll" class="min-h-0 min-w-0 shrink">
<Show
when={global.servers.list().length > 1}
fallback={
<Show
when={model.projects().length > 0}
fallback={
<HomeProjectEmpty
server={global.servers.list()[0]!}
recentlyClosed={model.recentlyClosed()}
homedir={model.homedir()}
chooseProject={model.chooseProject}
openRecentProject={(conn, directory) => model.addProjects(conn, [directory])}
language={language}
/>
}
>
<HomeProjectList
server={global.servers.list()[0]!}
recentlyClosed={model.recentlyClosed()}
homedir={model.homedir()}
chooseProject={model.chooseProject}
openRecentProject={(conn, directory) => model.addProjects(conn, [directory])}
projects={model.projects()}
selected={model.selection()}
selectProject={model.selectProject}
openNewSession={model.openProjectNewSession}
editProject={model.editProject}
closeProject={model.closeProject}
clearNotifications={model.clearNotifications}
unseenCount={model.unseenCount}
language={language}
/>
}
>
<HomeProjectList
server={global.servers.list()[0]!}
projects={model.projects()}
selected={model.selection()}
selectProject={model.selectProject}
openNewSession={model.openProjectNewSession}
editProject={model.editProject}
closeProject={model.closeProject}
clearNotifications={model.clearNotifications}
unseenCount={model.unseenCount}
language={language}
/>
</Show>
}
>
<div class="flex min-w-0 flex-col gap-4">
<For each={global.servers.list()}>
{(item) => {
const key = ServerConnection.key(item)
const healthy = () => !!global.servers.health[key]?.healthy
const serverCtx = global.ensureServerCtx(item)
const projects = () => serverCtx.projects.list()
const hasProjects = () => projects().length > 0
const collapsed = () => !!state().collapsed[key]
return (
<div class="flex min-w-0 flex-col gap-1">
<HomeServerRow
server={item}
selected={model.selection().server === key && !model.selection().directory}
collapsed={collapsed()}
health={global.servers.health[key]}
controller={controller}
focusServer={model.focusServer}
chooseProject={model.chooseProject}
openEdit={(server) => dialog.show(() => <DialogServerV2 mode="edit" server={server} />)}
toggleCollapsed={() => setState("collapsed", key, !state().collapsed[key])}
language={language}
/>
<Show when={healthy() && hasProjects() && !collapsed()}>
<div class="mx-3 h-px bg-v2-border-border-base" />
<HomeProjectList
</Show>
}
>
<div class="flex min-w-0 flex-col gap-4">
<For each={global.servers.list()}>
{(item) => {
const key = ServerConnection.key(item)
const healthy = () => !!global.servers.health[key]?.healthy
const serverCtx = global.ensureServerCtx(item)
const projects = () => serverCtx.projects.list()
const hasProjects = () => projects().length > 0
const collapsed = () => !!state().collapsed[key]
return (
<div class="flex min-w-0 flex-col gap-1">
<HomeServerRow
server={item}
projects={projects()}
selected={model.selection()}
selectProject={model.selectProject}
openNewSession={model.openProjectNewSession}
editProject={model.editProject}
closeProject={model.closeProject}
clearNotifications={model.clearNotifications}
unseenCount={model.unseenCount}
selected={model.selection().server === key && !model.selection().directory}
collapsed={collapsed()}
health={global.servers.health[key]}
controller={controller}
focusServer={model.focusServer}
chooseProject={model.chooseProject}
openEdit={(server) => dialog.show(() => <DialogServerV2 mode="edit" server={server} />)}
toggleCollapsed={() => setState("collapsed", key, !state().collapsed[key])}
language={language}
/>
</Show>
</div>
)
}}
</For>
</div>
</Show>
</ScrollView>
<Show when={healthy() && hasProjects() && !collapsed()}>
<div class="mx-3 h-px bg-v2-border-border-base" />
<HomeProjectList
server={item}
projects={projects()}
selected={model.selection()}
selectProject={model.selectProject}
openNewSession={model.openProjectNewSession}
editProject={model.editProject}
closeProject={model.closeProject}
clearNotifications={model.clearNotifications}
unseenCount={model.unseenCount}
language={language}
/>
</Show>
</div>
)
}}
</For>
</div>
</Show>
</ScrollView>

<SidebarRecents
records={model.records()}
server={model.selection().server}
loading={model.sessionLoad.isLoading}
canCreate={!!model.newSessionProject()}
openNewSession={model.openNewSession}
openSession={model.openSession}
/>
<SidebarRecents
records={model.records()}
server={model.selection().server}
loading={model.sessionLoad.isLoading}
canCreate={!!model.newSessionProject()}
openNewSession={model.openNewSession}
openSession={model.openSession}
/>

<HomeUtilityNav
class="flex shrink-0"
openSettings={openSettings}
openHelp={() => platform.openLink(DOCS_URL)}
language={language}
/>
<HomeUtilityNav
class="flex shrink-0"
openSettings={openSettings}
openHelp={() => platform.openLink(DOCS_URL)}
language={language}
/>

{/* The DEV / BETA badge, moved out of the titlebar so Home can lead it.
{/* The DEV / BETA badge, moved out of the titlebar so Home can lead it.
The condition mirrors ChannelIndicator's own — kept here rather than
wrapping unconditionally so prod, where it renders nothing, does not
leave the column's gap-4 behind as dead space. */}
<Show when={CHANNEL === "dev" || CHANNEL === "beta"}>
<div class="flex shrink-0 pl-1.5">
<ChannelIndicator debugTools={props.debugTools} />
</div>
</Show>
</aside>
<Show when={CHANNEL === "dev" || CHANNEL === "beta"}>
<div class="flex shrink-0 pl-1.5">
<ChannelIndicator debugTools={props.debugTools} />
</div>
</Show>
</aside>
</Show>
)
}

Expand Down
42 changes: 40 additions & 2 deletions packages/app/src/components/titlebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { usePlatform } from "@/context/platform"
import { useCommand } from "@/context/command"
import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings"
import { useAppSidebar } from "@/context/app-sidebar"
import { WindowsAppMenu } from "./windows-app-menu"
import { applyPath, backPath, forwardPath } from "./titlebar-history"
import { TitlebarTabStrip } from "@/components/titlebar-tab-strip"
Expand Down Expand Up @@ -79,6 +80,9 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
const location = useLocation()
const params = useParams()
const useV2Titlebar = createMemo(() => settings.general.newLayoutDesigns())
// Fork: undefined in the legacy shell, which renders this titlebar without an
// AppSidebarProvider and has no sidebar of this kind to toggle.
const appSidebar = useAppSidebar()
const mobile = createMediaQuery("(max-width: 767px)")
const bottom = createMemo(() => useV2Titlebar() && mobile() && settings.general.mobileTitlebarPosition() === "bottom")

Expand Down Expand Up @@ -475,8 +479,27 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
}}
>
{/* Fork: the channel badge moved to the bottom of the sidebar
(components/app-sidebar.tsx) so Home leads the titlebar. The
legacy titlebar below still renders it inline. */}
(components/app-sidebar.tsx) so this row leads with the
sidebar toggle. The legacy titlebar below still renders the
badge inline. */}
<Show when={appSidebar}>
{(sidebar) => (
<TooltipV2 placement="bottom" value={language.t("command.sidebar.toggle")} class="shrink-0">
<IconButtonV2
type="button"
data-action="titlebar-sidebar-toggle"
variant="ghost-muted"
size="large"
class="!w-9 shrink-0"
icon={<IconV2 name="sidebar-right" />}
state={sidebar().collapsed() ? undefined : "pressed"}
onClick={() => sidebar().toggle()}
aria-label={language.t("command.sidebar.toggle")}
aria-pressed={!sidebar().collapsed()}
/>
</TooltipV2>
)}
</Show>
<Show when={windows() || linux()}>
<WindowsAppMenu command={command} platform={platform} variant="v2" />
</Show>
Expand All @@ -502,6 +525,21 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
aria-pressed={layout.route().type === "draft"}
/>
</TooltipV2>
{/* Fork: opens the recent-sessions list, like the sidebar's
"View all", and focuses its search field on arrival. */}
<TooltipV2 placement="bottom" value={language.t("home.sessions.search.placeholder")} class="shrink-0">
<IconButtonV2
type="button"
data-action="titlebar-search"
variant="ghost-muted"
size="large"
class="!w-9 shrink-0"
icon={<IconV2 name="magnifying-glass" />}
state={layout.route().type === "home" ? "pressed" : undefined}
onClick={() => navigate("/?focus=search")}
aria-label={language.t("home.sessions.search.placeholder")}
/>
</TooltipV2>

<TitlebarTabStrip
tabs={tabsStore}
Expand Down
Loading
Loading