fix(app): improve tab handling (#30669)

This commit is contained in:
Brendan Allan 2026-06-05 11:41:30 +08:00 committed by GitHub
parent 3003867c25
commit 5426478e46
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 238 additions and 203 deletions

View File

@ -1,4 +1,4 @@
import { createEffect, createMemo, For, mapArray, Match, Show, startTransition, Switch, untrack } from "solid-js"
import { createEffect, createMemo, createResource, For, Match, Show, startTransition, Switch, untrack } from "solid-js"
import { createStore, produce } from "solid-js/store"
import { useLocation, useMatch, useNavigate, useParams } from "@solidjs/router"
import { IconButton } from "@opencode-ai/ui/icon-button"
@ -9,7 +9,7 @@ import { useTheme } from "@opencode-ai/ui/theme/context"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { getProjectAvatarVariant, useLayout, type LocalProject } from "@/context/layout"
import { getProjectAvatarVariant, LayoutRoute, useLayout, type LocalProject } from "@/context/layout"
import { usePlatform } from "@/context/platform"
import { useCommand } from "@/context/command"
import { useLanguage } from "@/context/language"
@ -17,7 +17,6 @@ import { useSettings } from "@/context/settings"
import { WindowsAppMenu } from "./windows-app-menu"
import { applyPath, backPath, forwardPath } from "./titlebar-history"
import { useServerSync } from "@/context/server-sync"
import { decodeDirectory } from "@/pages/directory-layout"
import { iife } from "@opencode-ai/core/util/iife"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
@ -29,6 +28,9 @@ import {
SESSION_TABS_REMOVED_EVENT,
type SessionTabsRemovedDetail,
} from "@/components/titlebar-session-events"
import { Persist, persisted } from "@/utils/persist"
import { useGlobal } from "@/context/global"
import { decode64 } from "@/utils/base64"
type TauriDesktopWindow = {
startDragging?: () => Promise<void>
@ -243,6 +245,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
const serverSync = useServerSync()
const navigate = useNavigate()
const homeMatch = useMatch(() => "/")
const layout = useLayout()
const newSessionHref = () => {
if (params.dir) return `/${params.dir}/session`
@ -253,41 +256,38 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
return `/${base64Encode(project.worktree)}/session`
}
type Tab = { dir: string; sessionId: string; href: string }
type SessionTab = { type: "session"; dirBase64: string; sessionId: string }
type Tab = SessionTab
const tabHref = (tab: Tab) => {
if (tab.type === "session") {
return makeSessionHref(tab.dirBase64, tab.sessionId)
}
return "/"
}
const [tabsStore, tabsStoreActions] = iife(() => {
const [store, setStore] = createStore<Tab[]>(
iife(() => {
if (!params.dir || !params.id) return []
return [
{
dir: decodeDirectory(params.dir) ?? "",
sessionId: params.id,
href: makeSessionHref(params.dir, params.id),
},
]
}),
)
const [store, setStore] = persisted(Persist.global("tabs"), createStore<Tab[]>([]))
const actions = {
addTab: (tab: Tab) => {
addSessionTab: (tab: Omit<SessionTab, "type">) => {
setStore(
produce((tabs) => {
if (tabs.some((t) => t.href === tab.href)) return
if (tabs.some((t) => t.type === "session" && tabHref(t) === tabHref({ type: "session", ...tab })))
return
tabs.push(tab)
tabs.push({ type: "session", ...tab })
}),
)
},
removeTab: (href: string) => {
removeTab: (index: number) => {
if (index < 0) return
void startTransition(() => {
setStore(
produce((tabs) => {
const index = tabs.findIndex((t) => t.href === href)
if (index === -1) return
const nextTab = tabs[index + 1] ?? tabs[index - 1]
tabs.splice(index, 1)
const nextTab = tabs[index] ?? tabs[tabs.length - 1]
if (nextTab) navigate(nextTab.href)
if (nextTab) navigate(tabHref(nextTab))
else navigate("/")
}),
)
@ -299,23 +299,28 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
produce((tabs) => {
const sessionIDs = new Set(input.sessionIDs)
const currentHref = params.dir && params.id ? makeSessionHref(params.dir, params.id) : undefined
const currentIndex = currentHref ? tabs.findIndex((tab) => tab.href === currentHref) : -1
const currentIndex = currentHref
? tabs.findIndex((tab) => tab.type === "session" && tabHref(tab) === currentHref)
: -1
const currentTab = tabs[currentIndex]
const removedCurrent =
currentIndex !== -1 &&
tabs[currentIndex]?.dir === input.directory &&
sessionIDs.has(tabs[currentIndex]?.sessionId ?? "")
currentTab?.type === "session" &&
atob(currentTab.dirBase64) === input.directory &&
sessionIDs.has(currentTab.sessionId)
for (let i = tabs.length - 1; i >= 0; i--) {
const tab = tabs[i]
if (!tab) continue
if (tab.dir !== input.directory) continue
if (!tab || tab.type !== "session") continue
if (atob(tab.dirBase64) !== input.directory) continue
if (!sessionIDs.has(tab.sessionId)) continue
tabs.splice(i, 1)
}
if (!removedCurrent) return
const nextTab = tabs[currentIndex] ?? tabs[tabs.length - 1]
if (nextTab) navigate(nextTab.href)
const nextTab =
tabs.slice(currentIndex).find((tab) => tab.type === "session") ??
tabs.slice(0, currentIndex).findLast((tab) => tab.type === "session")
if (nextTab) navigate(tabHref(nextTab))
else navigate("/")
}),
)
@ -326,55 +331,54 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
return [store, actions]
})
const matchRoute = (route: LayoutRoute) => {
if (route.type === "home") return
if (route.type === "dir-new-sesssion") {
}
if (route.type === "session") {
const main = tabsStore.find((s) => s.type === "session" && s.sessionId === route.sessionId)
if (main) return main
const sync = serverSync.createDirSyncContext(route.dir)
const session = sync.session.get(route.sessionId)
if (session?.parentID) {
const parentID = session.parentID
const parent = tabsStore.find((s) => s.type === "session" && s.sessionId === parentID)
if (parent) return parent
}
}
}
const currentTab = () => matchRoute(layout.route())
createEffect(() => {
const route = layout.route()
const tab = currentTab()
if (tab) return
if (route.type === "session") {
const sync = serverSync.createDirSyncContext(route.dir)
const session = sync.session.get(route.sessionId)
if (!session) return
const sessionId = session.parentID ?? session.id
tabsStoreActions.addSessionTab({
dirBase64: route.dirBase64,
sessionId,
})
}
})
makeEventListener(window, SESSION_TABS_REMOVED_EVENT, (event) => {
const detail = readSessionTabsRemovedDetail(event)
if (!detail) return
tabsStoreActions.removeSessions(detail)
})
createEffect(() => {
const params = useParams()
if (!(params.dir && params.id)) return
tabsStoreActions.addTab({
dir: decodeDirectory(params.dir) ?? "",
sessionId: params.id,
href: makeSessionHref(params.dir, params.id),
})
})
const projects = createMemo(() => layout.projects.list())
const projectByID = createMemo(
() => new Map(projects().flatMap((project) => (project.id ? [[project.id, project] as const] : []))),
)
const currentSessionTab = () => {
if (!params.dir || !params.id) return
const href = makeSessionHref(params.dir, params.id)
return tabsStore.find((tab) => tab.href === href)
}
const closeCurrentSessionTab = () => {
const tab = currentSessionTab()
if (!tab) return false
tabsStoreActions.removeTab(tab.href)
return true
}
const closeNewSessionTab = () => {
if (!(params.dir && !params.id)) return false
const last = tabsStore[tabsStore.length - 1]
if (last) navigate(last.href)
else navigate("/")
return true
}
const openNewTab = () => navigate(newSessionHref())
const closeActiveTab = () => closeCurrentSessionTab() || closeNewSessionTab()
command.register("tabs", () => {
const current = currentTab()
command.register(() => {
const commands = [
return [
{
id: "tab.new",
category: "tab",
@ -383,13 +387,15 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
hidden: true,
onSelect: openNewTab,
},
{
current && {
id: "tab.close",
category: "tab",
title: language.t("command.tab.close"),
keybind: "mod+w",
hidden: true,
onSelect: closeActiveTab,
onSelect: () => {
tabsStoreActions.removeTab(tabsStore.findIndex((tab) => current === tab))
},
},
{
id: `tab.prev`,
@ -398,14 +404,14 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
keybind: `mod+option+ArrowLeft`,
hidden: true,
onSelect: () => {
let index = tabsStore.findIndex((tab) => tab.href === currentSessionTab()?.href)
let index = tabsStore.findIndex((tab) => tab === currentTab())
if (index === -1) return
index -= 1
if (index === -1) index = tabsStore.length - 1
const next = tabsStore[index]
if (next) navigate(next.href)
if (next) navigate(tabHref(next))
},
},
{
@ -415,14 +421,14 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
keybind: `mod+option+ArrowRight`,
hidden: true,
onSelect: () => {
let index = tabsStore.findIndex((tab) => tab.href === currentSessionTab()?.href)
let index = tabsStore.findIndex((tab) => tab === currentTab())
if (index === -1) return
index += 1
if (index === tabsStore.length) index = 0
const next = tabsStore[index]
if (next) navigate(next.href)
if (next) navigate(tabHref(next))
},
},
...Array.from({ length: 9 }, (_, i) => {
@ -437,26 +443,11 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
hidden: true,
onSelect: () => {
const tab = tabsStore[index]
if (tab) navigate(tab.href)
if (tab) navigate(tabHref(tab))
},
}
}),
]
return commands
})
const tabsEnriched = iife(() => {
const base = mapArray(
() => tabsStore,
(tab) => {
const sync = serverSync.createDirSyncContext(tab.dir)
const session = sync.session.get(tab.sessionId)
return session ? { ...tab, info: session } : null
},
)
return () => base().flatMap((s) => (s ? [s] : []))
].filter((v) => v !== undefined)
})
return (
@ -483,19 +474,18 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
<div class="flex min-w-0 flex-1 flex-row items-center gap-1.5 overflow-hidden">
<div class="flex min-w-0 flex-row items-center gap-1.5 overflow-hidden">
<For each={tabsEnriched()}>
<For each={tabsStore}>
{(tab, i) => (
<>
{i() !== 0 && (
<div class="w-[1.5px] h-3 shrink-0 rounded-full bg-[var(--v2-background-bg-layer-02)]" />
)}
<TabNavItem
href={tab.href}
title={tab.info.title}
project={projectForSession(tab.info, projects(), projectByID())}
directory={tab.dir}
sessionId={tab.info.id}
onClose={() => tabsStoreActions.removeTab(tab.href)}
href={tabHref(tab)}
directory={decode64(tab.dirBase64)!}
sessionId={tab.sessionId}
onClose={() => tabsStoreActions.removeTab(i())}
active={currentTab() === tab}
/>
</>
)}
@ -519,7 +509,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
<NewSessionTabItem
href={`/${params.dir}/session`}
title={language.t("command.session.new")}
onClose={() => navigate(tabsEnriched().at(-1)?.href ?? "/")}
onClose={() => navigate(tabsStore.at(-1) ? tabHref(tabsStore.at(-1)!) : "/")}
/>
</Show>
<div class="min-w-0 flex-1" />
@ -746,38 +736,58 @@ function TitlebarUpdateIconButton(props: { state: TitlebarUpdatePillState }) {
function TabNavItem(props: {
href: string
title: string
project?: LocalProject
directory: string
sessionId: string
sessionId?: string
hideClose?: boolean
onClose: () => void
active?: boolean
}) {
const match = useMatch(() => props.href)
const isActive = () => !!match()
const closeTab = (event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
props.onClose()
}
const global = useGlobal()
const serverCtx = global.createServerCtx(global.servers.default())
const [session] = createResource(
() => {
const dirSyncCtx = serverCtx.sync.createDirSyncContext(props.directory)
return props.sessionId ? ([props.sessionId, dirSyncCtx] as const) : undefined
},
async ([sessionId, dirSyncCtx]) => {
await dirSyncCtx.session.sync(sessionId).catch(() => {})
return dirSyncCtx.session.get(sessionId)
},
)
return (
<div
class="group relative flex h-7 min-w-24 max-w-60 flex-row items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-[6px] bg-[var(--tab-bg)] px-1.5 [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)]"
data-active={isActive()}
data-active={props.active}
onMouseDown={(event) => {
if (event.button !== 1) return
closeTab(event)
}}
>
<a
href={props.href}
class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 text-[13px] font-medium text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base"
>
<span data-slot="project-avatar-slot">
<ProjectTabAvatar project={props.project} directory={props.directory} sessionId={props.sessionId} />
</span>
<span class="min-w-0 flex-1">{props.title}</span>
</a>
<Show when={session()}>
{(session) => {
const layout = useLayout()
const project = createMemo(() => projectForSession(session(), layout.projects.list()))
return (
<a
href={props.href}
class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 text-[13px] font-medium text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base"
>
<span data-slot="project-avatar-slot">
<ProjectTabAvatar project={project()} directory={props.directory} sessionId={session().id} />
</span>
<span class="min-w-0 flex-1">{session().title}</span>
</a>
)
}}
</Show>
<div class="absolute not-group-hover:not-group-data-[active=true]:left-52 group-hover:right-0 group-data-[active=true]:right-0 inset-y-0 flex flex-row items-center pr-1 py-1 w-8 pl-2">
<div

View File

@ -8,11 +8,11 @@ import {
getSessionPrefetchPromise,
setSessionPrefetch,
} from "./global-sync/session-prefetch"
import { createServerSyncContext } from "./server-sync"
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
import { SESSION_CACHE_LIMIT, dropSessionCaches, pickSessionCacheEvictions } from "./global-sync/session-cache"
import { diffs as list, message as clean } from "@/utils/diffs"
import { useServerSDK } from "./server-sdk"
import { createServerSdkContext, useServerSDK } from "./server-sdk"
import { type createServerSyncContextInner } from "./server-sync"
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
@ -171,8 +171,11 @@ function setOptimisticRemove(setStore: (...args: unknown[]) => void, input: Opti
})
}
export const createDirSyncContext = (directory: string, serverSync: ReturnType<typeof createServerSyncContext>) => {
const serverSDK = useServerSDK()
export const createDirSyncContext = (
directory: string,
serverSync: ReturnType<typeof createServerSyncContextInner>,
serverSDK: ReturnType<typeof createServerSdkContext> = useServerSDK(),
) => {
const client = serverSDK.createClient({ directory, throwOnError: true })
type Child = ReturnType<(typeof serverSync)["child"]>

View File

@ -73,6 +73,7 @@ export const { use: useGlobal, provider: GlobalProvider } = createSimpleContext(
servers: {
list: allServers,
health: serverHealth,
default: () => allServers().find((s) => ServerConnection.key(s) === props.defaultServer) ?? allServers()[0]!,
},
settings: {
server: {

View File

@ -0,0 +1,38 @@
import type { Accessor } from "solid-js"
export function ensureSessionKey(key: string, touch: (key: string) => void, seed: (key: string) => void) {
touch(key)
seed(key)
return key
}
export function createSessionKeyReader(sessionKey: string | Accessor<string>, ensure: (key: string) => void) {
const key = typeof sessionKey === "function" ? sessionKey : () => sessionKey
return () => {
const value = key()
ensure(value)
return value
}
}
export function pruneSessionKeys(input: {
keep?: string
max: number
used: Map<string, number>
view: string[]
tabs: string[]
}) {
if (!input.keep) return []
const keys = new Set<string>([...input.view, ...input.tabs])
if (keys.size <= input.max) return []
const score = (key: string) => {
if (key === input.keep) return Number.MAX_SAFE_INTEGER
return input.used.get(key) ?? 0
}
return Array.from(keys)
.sort((a, b) => score(b) - score(a))
.slice(input.max)
}

View File

@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test"
import { createRoot, createSignal } from "solid-js"
import { createSessionKeyReader, ensureSessionKey, pruneSessionKeys } from "./layout"
import { createSessionKeyReader, ensureSessionKey, pruneSessionKeys } from "./layout-helpers"
describe("layout session-key helpers", () => {
test("couples touch and scroll seed in order", () => {

View File

@ -1,10 +1,11 @@
import { createStore, produce } from "solid-js/store"
import { batch, createEffect, createMemo, onCleanup, onMount, type Accessor } from "solid-js"
import { useLocation } from "@solidjs/router"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { makeEventListener } from "@solid-primitives/event-listener"
import { useServerSync } from "./server-sync"
import { useServerSDK } from "./server-sdk"
import { useServer } from "./server"
import { ServerConnection, useServer } from "./server"
import { usePlatform } from "./platform"
import { Project } from "@opencode-ai/sdk/v2"
import { Persist, persisted, removePersisted } from "@/utils/persist"
@ -13,6 +14,9 @@ import { same } from "@/utils/same"
import { createScrollPersistence, type SessionScroll } from "./layout-scroll"
import { createPathHelpers } from "./file/path"
import type { ProjectAvatarVariant } from "@opencode-ai/ui/v2/project-avatar-v2"
import { createSessionKeyReader, ensureSessionKey, pruneSessionKeys } from "./layout-helpers"
export { createSessionKeyReader, ensureSessionKey, pruneSessionKeys }
export type { ProjectAvatarVariant }
@ -69,42 +73,10 @@ export type LocalProject = Partial<Project> & { worktree: string; expanded: bool
export type ReviewDiffStyle = "unified" | "split"
export function ensureSessionKey(key: string, touch: (key: string) => void, seed: (key: string) => void) {
touch(key)
seed(key)
return key
}
export function createSessionKeyReader(sessionKey: string | Accessor<string>, ensure: (key: string) => void) {
const key = typeof sessionKey === "function" ? sessionKey : () => sessionKey
return () => {
const value = key()
ensure(value)
return value
}
}
export function pruneSessionKeys(input: {
keep?: string
max: number
used: Map<string, number>
view: string[]
tabs: string[]
}) {
if (!input.keep) return []
const keys = new Set<string>([...input.view, ...input.tabs])
if (keys.size <= input.max) return []
const score = (key: string) => {
if (key === input.keep) return Number.MAX_SAFE_INTEGER
return input.used.get(key) ?? 0
}
return Array.from(keys)
.sort((a, b) => score(b) - score(a))
.slice(input.max)
}
export type LayoutRoute =
| { type: "home" }
| { type: "dir-new-sesssion"; dir: string; dirBase64: string; server?: ServerConnection.Key }
| { type: "session"; dir: string; dirBase64: string; sessionId: string; server?: ServerConnection.Key }
function nextSessionTabsForOpen(current: SessionTabs | undefined, tab: string): SessionTabs {
const all = current?.all ?? []
@ -146,6 +118,21 @@ const normalizeStoredSessionTabs = (key: string, tabs: SessionTabs) => {
}
}
const currentRoute = (pathname: string): LayoutRoute => {
const parts = pathname.split("/").filter(Boolean)
if (parts.length === 0) return { type: "home" }
const dirBase64 = parts[0]
const dir = decode64(dirBase64)
if (!dir) return { type: "home" }
if (parts[1] !== "session") return { type: "home" }
const id = parts[2]
if (id) return { type: "session", dir, dirBase64, sessionId: id }
return { type: "dir-new-sesssion", dir, dirBase64 }
}
export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
name: "Layout",
init: () => {
@ -153,6 +140,8 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
const serverSync = useServerSync()
const server = useServer()
const platform = usePlatform()
const location = useLocation()
const route = createMemo(() => currentRoute(location.pathname))
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value)
@ -557,6 +546,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
})
return {
route,
ready,
handoff: {
tabs: createMemo(() => store.handoff?.tabs),

View File

@ -1,17 +1,7 @@
import type { Config, OpencodeClient, Path, Project, ProviderAuthResponse, Todo } from "@opencode-ai/sdk/v2/client"
import { showToast } from "@/utils/toast"
import { getFilename } from "@opencode-ai/core/util/path"
import {
batch,
createContext,
createEffect,
getOwner,
onCleanup,
onMount,
type ParentProps,
untrack,
useContext,
} from "solid-js"
import { batch, getOwner, onCleanup, onMount, untrack } from "solid-js"
import { createStore, produce, reconcile } from "solid-js/store"
import { useLanguage } from "@/context/language"
import type { InitError } from "../pages/error"
@ -86,7 +76,7 @@ function makeQueryOptionsApi(serverSDK: () => OpencodeClient, sdkFor: (dir: Path
}
export type QueryOptionsApi = ReturnType<typeof makeQueryOptionsApi>
export function createServerSyncContext(_serverSDK?: ServerSDK) {
export function createServerSyncContextInner(_serverSDK?: ServerSDK) {
const serverSDK: ServerSDK = _serverSDK ?? useServerSDK()
const language = useLanguage()
const owner = getOwner()
@ -476,6 +466,17 @@ export function createServerSyncContext(_serverSDK?: ServerSDK) {
}
}
export function createServerSyncContext(_serverSDK?: ServerSDK) {
const inner = createServerSyncContextInner(_serverSDK)
return Object.assign(inner, {
createDirSyncContext: createRefCountMap(
(dir) => createDirSyncContext(dir, inner, _serverSDK),
(dir) => inner.disableMcp(dir),
directoryKey,
),
})
}
export const { use: useServerSync, provider: ServerSyncProvider } = createSimpleContext({
name: "ServerSync",
init: (props: { server?: ServerConnection.Any }) => {
@ -487,13 +488,7 @@ export const { use: useServerSync, provider: ServerSyncProvider } = createSimple
if (!conn) throw new Error(language.t("error.serverSDK.noServerAvailable"))
const ctx = global.createServerCtx(conn)
return Object.assign(ctx.sync, {
createDirSyncContext: createRefCountMap(
(dir) => createDirSyncContext(dir, ctx.sync),
(dir) => ctx.sync.disableMcp(dir),
directoryKey,
),
})
return ctx.sync
},
})

View File

@ -159,10 +159,6 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
init: () => {
const [store, setStore, _, ready] = persisted("settings.v3", createStore<Settings>(defaultSettings))
createEffect(() => {
console.log("settings", { ready: ready() })
})
createEffect(() => {
if (typeof document === "undefined") return
const root = document.documentElement

View File

@ -26,7 +26,7 @@ function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
createResource(
() => params.id,
(id) => sync.session.sync(id),
(id) => sync.session.sync(id).catch(() => {}),
)
return (

View File

@ -67,7 +67,7 @@ export function getProjectAvatarSource(id?: string, icon?: { color?: string; url
export function projectForSession<T extends { id?: string; worktree: string; sandboxes?: string[] }>(
session: Session,
projects: T[],
byID: Map<string, T>,
byID: Map<string, T> = new Map(projects.flatMap((project) => (project.id ? [[project.id, project] as const] : []))),
) {
const direct = byID.get(session.projectID)
if (direct) return direct

View File

@ -418,23 +418,26 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
}),
]
const fileCmds = () => [
fileCommand({
id: "file.open",
title: language.t("command.file.open"),
description: language.t("palette.search.placeholder"),
keybind: "mod+k,mod+p",
slash: "open",
onSelect: openFile,
}),
fileCommand({
id: "tab.close",
title: language.t("command.tab.close"),
keybind: "mod+w",
disabled: !closableTab(),
onSelect: closeTab,
}),
]
const fileCmds = () => {
const tab = closableTab()
return [
fileCommand({
id: "file.open",
title: language.t("command.file.open"),
description: language.t("palette.search.placeholder"),
keybind: "mod+k,mod+p",
slash: "open",
onSelect: openFile,
}),
tab &&
fileCommand({
id: "tab.close",
title: language.t("command.tab.close"),
keybind: "mod+w",
onSelect: closeTab,
}),
].filter((v) => !!v)
}
const contextCmds = () => [
contextCommand({

View File

@ -390,7 +390,6 @@ export const SessionReview = (props: SessionReviewProps) => {
<Accordion multiple value={open()} onChange={handleChange}>
<For each={files()}>
{(file) => {
console.log({ file })
const diff = () => itemsMap()[file]
// binary files have empty diffs that we can't render