feat(desktop): reopen closed tabs and background tab open (#35010)
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
This commit is contained in:
@@ -374,9 +374,16 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
keybind: "mod+w",
|
keybind: "mod+w",
|
||||||
hidden: true,
|
hidden: true,
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
tabsStoreActions.removeTab(tabsStore.findIndex((tab) => current === tab))
|
tabsStoreActions.closeTab(tabsStore.findIndex((tab) => current === tab))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "tab.reopenClosed",
|
||||||
|
category: language.t("command.category.file"),
|
||||||
|
title: language.t("command.tab.reopenClosed"),
|
||||||
|
keybind: "mod+shift+t",
|
||||||
|
onSelect: () => tabsStoreActions.reopenClosedTab(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: `tab.prev`,
|
id: `tab.prev`,
|
||||||
category: "tab",
|
category: "tab",
|
||||||
@@ -464,7 +471,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
}}
|
}}
|
||||||
onClose={(tab) => {
|
onClose={(tab) => {
|
||||||
const index = tabsStore.findIndex((item) => tabKey(item) === tabKey(tab))
|
const index = tabsStore.findIndex((item) => tabKey(item) === tabKey(tab))
|
||||||
if (index !== -1) tabsStoreActions.removeTab(index)
|
if (index !== -1) tabsStoreActions.closeTab(index)
|
||||||
}}
|
}}
|
||||||
onReorder={(keys) => tabsStoreActions.reorder(keys)}
|
onReorder={(keys) => tabsStoreActions.reorder(keys)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import type { SessionTab, Tab } from "./tabs"
|
||||||
|
|
||||||
|
export type ClosedTab = {
|
||||||
|
tab: SessionTab
|
||||||
|
index: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const CLOSED_TAB_LIMIT = 25
|
||||||
|
|
||||||
|
// Only session tabs are recorded; closing a draft tab deletes its persisted
|
||||||
|
// state, so a reopened draft would come back empty anyway.
|
||||||
|
export function pushClosedTab(stack: ClosedTab[], tab: Tab, index: number): ClosedTab[] {
|
||||||
|
if (tab.type !== "session") return stack
|
||||||
|
return [...stack, { tab: { ...tab }, index }].slice(-CLOSED_TAB_LIMIT)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pops the most recently closed tab that is not open again,
|
||||||
|
// discarding stale entries along the way.
|
||||||
|
export function takeClosedTab(stack: ClosedTab[], tabs: Tab[]): { entry?: ClosedTab; stack: ClosedTab[] } {
|
||||||
|
const remaining = [...stack]
|
||||||
|
while (remaining.length) {
|
||||||
|
const entry = remaining.pop()
|
||||||
|
if (entry && !isOpen(tabs, entry.tab)) return { entry, stack: remaining }
|
||||||
|
}
|
||||||
|
return { stack: remaining }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeClosedTabs(stack: ClosedTab[], server: SessionTab["server"], sessionIDs: string[]) {
|
||||||
|
const removed = new Set(sessionIDs)
|
||||||
|
return stack.filter((entry) => entry.tab.server !== server || !removed.has(entry.tab.sessionId))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function nextTabAfterClose(tabs: Tab[], index: number, active: boolean) {
|
||||||
|
if (!active) return undefined
|
||||||
|
return tabs[index + 1] ?? tabs[index - 1] ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOpen(tabs: Tab[], tab: SessionTab) {
|
||||||
|
return tabs.some((item) => item.type === "session" && item.server === tab.server && item.sessionId === tab.sessionId)
|
||||||
|
}
|
||||||
@@ -1,6 +1,21 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { createRoot, getOwner, onCleanup } from "solid-js"
|
import { createRoot, getOwner, onCleanup } from "solid-js"
|
||||||
import { createTabMemory } from "./tab-memory"
|
import { createTabMemory } from "./tab-memory"
|
||||||
|
import {
|
||||||
|
nextTabAfterClose,
|
||||||
|
pushClosedTab,
|
||||||
|
removeClosedTabs,
|
||||||
|
takeClosedTab,
|
||||||
|
type ClosedTab,
|
||||||
|
} from "./closed-tabs"
|
||||||
|
import type { SessionTab, Tab } from "./tabs"
|
||||||
|
import type { ServerConnection } from "./server"
|
||||||
|
|
||||||
|
const server = "local\nhttp://localhost:4096" as ServerConnection.Key
|
||||||
|
|
||||||
|
function sessionTab(sessionId: string): SessionTab {
|
||||||
|
return { type: "session", server, sessionId }
|
||||||
|
}
|
||||||
|
|
||||||
describe("tab memory", () => {
|
describe("tab memory", () => {
|
||||||
test("keeps state until its tab is removed", () => {
|
test("keeps state until its tab is removed", () => {
|
||||||
@@ -22,3 +37,75 @@ describe("tab memory", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("closed tab stack", () => {
|
||||||
|
test("records session tabs with their index", () => {
|
||||||
|
const stack = pushClosedTab([], sessionTab("a"), 2)
|
||||||
|
|
||||||
|
expect(stack).toEqual([{ tab: sessionTab("a"), index: 2 }])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("ignores draft tabs", () => {
|
||||||
|
const draft: Tab = { type: "draft", draftID: "d1", server, directory: "/tmp" }
|
||||||
|
|
||||||
|
expect(pushClosedTab([], draft, 0)).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("caps the stack size", () => {
|
||||||
|
const stack = Array.from({ length: 30 }, (_, i) => i).reduce<ClosedTab[]>(
|
||||||
|
(acc, i) => pushClosedTab(acc, sessionTab(`s${i}`), i),
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(stack).toHaveLength(25)
|
||||||
|
expect(stack[0]?.tab.sessionId).toBe("s5")
|
||||||
|
expect(stack.at(-1)?.tab.sessionId).toBe("s29")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("pops the most recently closed tab", () => {
|
||||||
|
const stack = [
|
||||||
|
{ tab: sessionTab("a"), index: 0 },
|
||||||
|
{ tab: sessionTab("b"), index: 1 },
|
||||||
|
]
|
||||||
|
const result = takeClosedTab(stack, [])
|
||||||
|
|
||||||
|
expect(result.entry?.tab.sessionId).toBe("b")
|
||||||
|
expect(result.stack).toEqual([{ tab: sessionTab("a"), index: 0 }])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("skips entries whose tab is already open", () => {
|
||||||
|
const stack = [
|
||||||
|
{ tab: sessionTab("a"), index: 0 },
|
||||||
|
{ tab: sessionTab("b"), index: 1 },
|
||||||
|
]
|
||||||
|
const result = takeClosedTab(stack, [sessionTab("b")])
|
||||||
|
|
||||||
|
expect(result.entry?.tab.sessionId).toBe("a")
|
||||||
|
expect(result.stack).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns no entry when everything is open or empty", () => {
|
||||||
|
expect(takeClosedTab([], []).entry).toBeUndefined()
|
||||||
|
|
||||||
|
const result = takeClosedTab([{ tab: sessionTab("a"), index: 0 }], [sessionTab("a")])
|
||||||
|
expect(result.entry).toBeUndefined()
|
||||||
|
expect(result.stack).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("purges removed sessions", () => {
|
||||||
|
const stack = [
|
||||||
|
{ tab: sessionTab("a"), index: 0 },
|
||||||
|
{ tab: sessionTab("b"), index: 1 },
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(removeClosedTabs(stack, server, ["a"])).toEqual([{ tab: sessionTab("b"), index: 1 }])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("does not navigate when a background tab closes", () => {
|
||||||
|
const tabs = [sessionTab("a"), sessionTab("b"), sessionTab("c")]
|
||||||
|
|
||||||
|
expect(nextTabAfterClose(tabs, 1, false)).toBeUndefined()
|
||||||
|
expect(nextTabAfterClose(tabs, 1, true)).toEqual(sessionTab("c"))
|
||||||
|
expect(nextTabAfterClose([sessionTab("a")], 0, true)).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { uuid } from "@/utils/uuid"
|
|||||||
import { SessionTabsRemovedDetail } from "@/components/titlebar-session-events"
|
import { SessionTabsRemovedDetail } from "@/components/titlebar-session-events"
|
||||||
import { sessionHref } from "@/utils/session-route"
|
import { sessionHref } from "@/utils/session-route"
|
||||||
import { createTabMemory } from "./tab-memory"
|
import { createTabMemory } from "./tab-memory"
|
||||||
|
import { nextTabAfterClose, pushClosedTab, removeClosedTabs, takeClosedTab, type ClosedTab } from "./closed-tabs"
|
||||||
|
|
||||||
export type SessionTab = {
|
export type SessionTab = {
|
||||||
type: "session"
|
type: "session"
|
||||||
@@ -63,6 +64,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
|||||||
createStore<Tab[]>([]),
|
createStore<Tab[]>([]),
|
||||||
)
|
)
|
||||||
const [recent, setRecent, , recentReady] = persisted(Persist.window("tabs.recent"), createStore<RecentTab>({}))
|
const [recent, setRecent, , recentReady] = persisted(Persist.window("tabs.recent"), createStore<RecentTab>({}))
|
||||||
|
const [closed, setClosed, , closedReady] = persisted(Persist.window("tabs.closed"), createStore<ClosedTab[]>([]))
|
||||||
|
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -87,6 +89,15 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateClosed = (update: (stack: ClosedTab[]) => ClosedTab[]) => {
|
||||||
|
const apply = () => setClosed((stack) => update(stack))
|
||||||
|
if (closedReady()) {
|
||||||
|
apply()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
void closedReady.promise?.then(apply)
|
||||||
|
}
|
||||||
|
|
||||||
const removeDraftPersisted = (draftID: string) => {
|
const removeDraftPersisted = (draftID: string) => {
|
||||||
for (const key of draftPersistedKeys()) removePersisted(Persist.draft(draftID, key), platform)
|
for (const key of draftPersistedKeys()) removePersisted(Persist.draft(draftID, key), platform)
|
||||||
}
|
}
|
||||||
@@ -106,6 +117,13 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
|||||||
if (recent.key && !next.some((tab) => tabKey(tab) === recent.key)) setRecentKey(undefined)
|
if (recent.key && !next.some((tab) => tabKey(tab) === recent.key)) setRecentKey(undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!closedReady()) return
|
||||||
|
const servers = new Set(server.list.map(ServerConnection.key))
|
||||||
|
const next = closed.filter((entry) => servers.has(entry.tab.server))
|
||||||
|
if (next.length !== closed.length) setClosed(() => next)
|
||||||
|
})
|
||||||
|
|
||||||
const navigateTab = (tab: Tab) => {
|
const navigateTab = (tab: Tab) => {
|
||||||
const href = tabHref(tab)
|
const href = tabHref(tab)
|
||||||
setRecentKey(tabKey(tab))
|
setRecentKey(tabKey(tab))
|
||||||
@@ -117,7 +135,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
|||||||
if (!tab) return
|
if (!tab) return
|
||||||
const key = tabKey(tab)
|
const key = tabKey(tab)
|
||||||
const draftID = tab.type === "draft" ? tab.draftID : undefined
|
const draftID = tab.type === "draft" ? tab.draftID : undefined
|
||||||
const nextTab = store[index + 1] ?? store[index - 1]
|
const nextTab = nextTabAfterClose(store, index, recentKey() === key && location.pathname !== "/")
|
||||||
closing.add(key)
|
closing.add(key)
|
||||||
void startTransition(() => {
|
void startTransition(() => {
|
||||||
setStore(
|
setStore(
|
||||||
@@ -125,9 +143,11 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
|||||||
tabs.splice(index, 1)
|
tabs.splice(index, 1)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
if (recent.key === key) setRecentKey(nextTab && tabKey(nextTab))
|
if (nextTab === null) {
|
||||||
|
setRecentKey(undefined)
|
||||||
|
navigate("/")
|
||||||
|
}
|
||||||
if (nextTab) navigateTab(nextTab)
|
if (nextTab) navigateTab(nextTab)
|
||||||
else navigate("/")
|
|
||||||
}).finally(() => closing.delete(key))
|
}).finally(() => closing.delete(key))
|
||||||
memory.remove(key)
|
memory.remove(key)
|
||||||
if (draftID) removeDraftPersisted(draftID)
|
if (draftID) removeDraftPersisted(draftID)
|
||||||
@@ -201,13 +221,45 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
|||||||
removeDraftPersisted(draftID)
|
removeDraftPersisted(draftID)
|
||||||
},
|
},
|
||||||
removeTab,
|
removeTab,
|
||||||
|
// User-initiated close: records the tab so it can be reopened.
|
||||||
|
// Cleanup paths (missing sessions, archive, server removal) go through
|
||||||
|
// removeTab and friends directly and are not recorded.
|
||||||
|
closeTab(index: number) {
|
||||||
|
const tab = store[index]
|
||||||
|
if (!tab) return
|
||||||
|
if (tab.type === "session") updateClosed((stack) => pushClosedTab(stack, tab, index))
|
||||||
|
removeTab(index)
|
||||||
|
},
|
||||||
|
reopenClosedTab() {
|
||||||
|
if (!closedReady()) {
|
||||||
|
void closedReady.promise?.then(() => actions.reopenClosedTab())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const result = takeClosedTab(closed, store)
|
||||||
|
if (result.stack.length === closed.length) return
|
||||||
|
setClosed(() => result.stack)
|
||||||
|
const entry = result.entry
|
||||||
|
if (!entry) return
|
||||||
|
const index = Math.min(entry.index, store.length)
|
||||||
|
void startTransition(() => {
|
||||||
|
setStore(
|
||||||
|
produce((tabs) => {
|
||||||
|
if (tabs.some((item) => tabKey(item) === tabKey(entry.tab))) return
|
||||||
|
tabs.splice(index, 0, entry.tab)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
navigateTab(entry.tab)
|
||||||
|
})
|
||||||
|
},
|
||||||
removeSessionTab(input: Omit<SessionTab, "type">) {
|
removeSessionTab(input: Omit<SessionTab, "type">) {
|
||||||
|
updateClosed((stack) => removeClosedTabs(stack, input.server, [input.sessionId]))
|
||||||
const index = store.findIndex(
|
const index = store.findIndex(
|
||||||
(tab) => tab.type === "session" && tab.server === input.server && tab.sessionId === input.sessionId,
|
(tab) => tab.type === "session" && tab.server === input.server && tab.sessionId === input.sessionId,
|
||||||
)
|
)
|
||||||
if (index !== -1) removeTab(index)
|
if (index !== -1) removeTab(index)
|
||||||
},
|
},
|
||||||
removeServer(key: ServerConnection.Key) {
|
removeServer(key: ServerConnection.Key) {
|
||||||
|
updateClosed((stack) => stack.filter((entry) => entry.tab.server !== key))
|
||||||
const drafts = store.flatMap((tab) => (tab.type === "draft" && tab.server === key ? [tab.draftID] : []))
|
const drafts = store.flatMap((tab) => (tab.type === "draft" && tab.server === key ? [tab.draftID] : []))
|
||||||
const removed = store.filter((tab) => tab.server === key).map(tabKey)
|
const removed = store.filter((tab) => tab.server === key).map(tabKey)
|
||||||
setStore((tabs) => tabs.filter((tab) => tab.server !== key))
|
setStore((tabs) => tabs.filter((tab) => tab.server !== key))
|
||||||
@@ -218,6 +270,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
|||||||
},
|
},
|
||||||
removeSessions: (input: SessionTabsRemovedDetail) => {
|
removeSessions: (input: SessionTabsRemovedDetail) => {
|
||||||
const targetServer = input.server ?? server.key
|
const targetServer = input.server ?? server.key
|
||||||
|
updateClosed((stack) => removeClosedTabs(stack, targetServer, input.sessionIDs))
|
||||||
const removed = store
|
const removed = store
|
||||||
.filter(
|
.filter(
|
||||||
(tab) => tab.type === "session" && tab.server === targetServer && input.sessionIDs.includes(tab.sessionId),
|
(tab) => tab.type === "session" && tab.server === targetServer && input.sessionIDs.includes(tab.sessionId),
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export const dict = {
|
|||||||
"command.session.new": "New session",
|
"command.session.new": "New session",
|
||||||
"command.file.open": "Open file",
|
"command.file.open": "Open file",
|
||||||
"command.tab.close": "Close tab",
|
"command.tab.close": "Close tab",
|
||||||
|
"command.tab.reopenClosed": "Reopen closed tab",
|
||||||
"command.context.addSelection": "Add selection to context",
|
"command.context.addSelection": "Add selection to context",
|
||||||
"command.context.addSelection.description": "Add selected lines from the current file",
|
"command.context.addSelection.description": "Add selected lines from the current file",
|
||||||
"command.input.focus": "Focus input",
|
"command.input.focus": "Focus input",
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { shouldOpenSessionInBackground } from "./home-session-open"
|
||||||
|
|
||||||
|
describe("shouldOpenSessionInBackground", () => {
|
||||||
|
test("requires only the platform primary modifier", () => {
|
||||||
|
expect(shouldOpenSessionInBackground({ mac: true, meta: true, ctrl: false, shift: false, alt: false })).toBe(true)
|
||||||
|
expect(shouldOpenSessionInBackground({ mac: false, meta: false, ctrl: true, shift: false, alt: false })).toBe(true)
|
||||||
|
expect(shouldOpenSessionInBackground({ mac: true, meta: true, ctrl: false, shift: true, alt: false })).toBe(false)
|
||||||
|
expect(shouldOpenSessionInBackground({ mac: false, meta: false, ctrl: true, shift: false, alt: true })).toBe(false)
|
||||||
|
expect(shouldOpenSessionInBackground({ mac: false, meta: true, ctrl: false, shift: false, alt: false })).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export function shouldOpenSessionInBackground(input: {
|
||||||
|
mac: boolean
|
||||||
|
meta: boolean
|
||||||
|
ctrl: boolean
|
||||||
|
shift: boolean
|
||||||
|
alt: boolean
|
||||||
|
}) {
|
||||||
|
if (input.shift || input.alt) return false
|
||||||
|
if (input.mac) return input.meta && !input.ctrl
|
||||||
|
return input.ctrl && !input.meta
|
||||||
|
}
|
||||||
@@ -66,6 +66,7 @@ import { Persist, persisted } from "@/utils/persist"
|
|||||||
import { useMarked } from "@opencode-ai/ui/context/marked"
|
import { useMarked } from "@opencode-ai/ui/context/marked"
|
||||||
import { preloadMarkdown } from "@opencode-ai/session-ui/markdown-cache"
|
import { preloadMarkdown } from "@opencode-ai/session-ui/markdown-cache"
|
||||||
import { archiveHomeSession } from "./home-session-archive"
|
import { archiveHomeSession } from "./home-session-archive"
|
||||||
|
import { shouldOpenSessionInBackground } from "./home-session-open"
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
|
|
||||||
const HOME_SESSION_LIMIT = 64
|
const HOME_SESSION_LIMIT = 64
|
||||||
@@ -238,6 +239,20 @@ function useHomeSessionHeaderOpacity(groups: () => HomeSessionGroup[]) {
|
|||||||
return { setViewport, setContentRef, setHeaderRef, update, titleOpacity }
|
return { setViewport, setContentRef, setHeaderRef, update, titleOpacity }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cmd+click on macOS (Ctrl+click elsewhere) opens a session tab in the
|
||||||
|
// background without navigating, matching browser conventions.
|
||||||
|
function isBackgroundOpen(event: MouseEvent) {
|
||||||
|
return shouldOpenSessionInBackground({
|
||||||
|
mac: typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform),
|
||||||
|
meta: event.metaKey,
|
||||||
|
ctrl: event.ctrlKey,
|
||||||
|
shift: event.shiftKey,
|
||||||
|
alt: event.altKey,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenSessionOptions = { background?: boolean }
|
||||||
|
|
||||||
export function NewHome() {
|
export function NewHome() {
|
||||||
const sync = useServerSync()
|
const sync = useServerSync()
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
@@ -377,9 +392,11 @@ export function NewHome() {
|
|||||||
setState("searchFocused", false)
|
setState("searchFocused", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectSearchSession(session: Session) {
|
function selectSearchSession(session: Session, options?: OpenSessionOptions) {
|
||||||
openSession(session)
|
openSession(session, options)
|
||||||
closeSearch()
|
// Background opens keep the search visible so several results can be
|
||||||
|
// opened in a row.
|
||||||
|
if (!options?.background) closeSearch()
|
||||||
}
|
}
|
||||||
|
|
||||||
command.register("home", () => [
|
command.register("home", () => [
|
||||||
@@ -464,13 +481,17 @@ export function NewHome() {
|
|||||||
.forEach((directory) => state.project.markViewed(directory))
|
.forEach((directory) => state.project.markViewed(directory))
|
||||||
}
|
}
|
||||||
|
|
||||||
function openSession(session: Session) {
|
function openSession(session: Session, options?: OpenSessionOptions) {
|
||||||
const project = projectForSession(session, projects(), projectByID())
|
const project = projectForSession(session, projects(), projectByID())
|
||||||
const conn = focusedServer()
|
const conn = focusedServer()
|
||||||
if (!conn) return
|
if (!conn) return
|
||||||
const directory = project?.worktree ?? session.directory
|
const directory = project?.worktree ?? session.directory
|
||||||
const ctx = global.ensureServerCtx(conn)
|
const ctx = global.ensureServerCtx(conn)
|
||||||
ctx.projects.open(directory)
|
ctx.projects.open(directory)
|
||||||
|
if (options?.background) {
|
||||||
|
tabs.addSessionTab({ server: ServerConnection.key(conn), sessionId: session.id })
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx.projects.touch(directory)
|
ctx.projects.touch(directory)
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
const tab = tabs.addSessionTab({ server: ServerConnection.key(conn), sessionId: session.id })
|
const tab = tabs.addSessionTab({ server: ServerConnection.key(conn), sessionId: session.id })
|
||||||
@@ -1130,7 +1151,7 @@ function HomeSessionSearch(props: {
|
|||||||
onInput: (value: string) => void
|
onInput: (value: string) => void
|
||||||
onFocus: () => void
|
onFocus: () => void
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onSelect: (session: Session) => void
|
onSelect: (session: Session, options?: OpenSessionOptions) => void
|
||||||
}) {
|
}) {
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const [store, setStore] = createStore({ active: "" })
|
const [store, setStore] = createStore({ active: "" })
|
||||||
@@ -1244,7 +1265,7 @@ function HomeSessionSearch(props: {
|
|||||||
server={props.server}
|
server={props.server}
|
||||||
selected={store.active === homeSessionSearchKey(record)}
|
selected={store.active === homeSessionSearchKey(record)}
|
||||||
onHighlight={() => setStore("active", homeSessionSearchKey(record))}
|
onHighlight={() => setStore("active", homeSessionSearchKey(record))}
|
||||||
onSelect={(session) => props.onSelect(session)}
|
onSelect={(session, options) => props.onSelect(session, options)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
@@ -1324,7 +1345,7 @@ function HomeSessionSearchResultRow(props: {
|
|||||||
server: ServerConnection.Key
|
server: ServerConnection.Key
|
||||||
selected: boolean
|
selected: boolean
|
||||||
onHighlight: () => void
|
onHighlight: () => void
|
||||||
onSelect: (session: Session) => void
|
onSelect: (session: Session, options?: OpenSessionOptions) => void
|
||||||
}) {
|
}) {
|
||||||
const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id)
|
const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id)
|
||||||
const showProjectName = () => props.showProjectName && props.record.projectName
|
const showProjectName = () => props.showProjectName && props.record.projectName
|
||||||
@@ -1345,7 +1366,7 @@ function HomeSessionSearchResultRow(props: {
|
|||||||
group: !!showProjectName(),
|
group: !!showProjectName(),
|
||||||
}}
|
}}
|
||||||
onMouseEnter={() => props.onHighlight()}
|
onMouseEnter={() => props.onHighlight()}
|
||||||
onClick={() => props.onSelect(props.record.session)}
|
onClick={(event) => props.onSelect(props.record.session, { background: isBackgroundOpen(event) })}
|
||||||
>
|
>
|
||||||
<HomeSessionLeading
|
<HomeSessionLeading
|
||||||
project={props.record.project}
|
project={props.record.project}
|
||||||
@@ -1389,7 +1410,7 @@ function HomeSessionRow(props: {
|
|||||||
record: HomeSessionRecord
|
record: HomeSessionRecord
|
||||||
showProjectName: boolean
|
showProjectName: boolean
|
||||||
server: ServerConnection.Key
|
server: ServerConnection.Key
|
||||||
openSession: (session: Session) => void
|
openSession: (session: Session, options?: OpenSessionOptions) => void
|
||||||
archiveSession: (session: Session) => Promise<void>
|
archiveSession: (session: Session) => Promise<void>
|
||||||
}) {
|
}) {
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
@@ -1405,7 +1426,7 @@ function HomeSessionRow(props: {
|
|||||||
type="button"
|
type="button"
|
||||||
data-component="home-session-row"
|
data-component="home-session-row"
|
||||||
class={`${HOME_ROW} h-10 min-w-0 flex-1 gap-2 py-3 pl-3 pr-10`}
|
class={`${HOME_ROW} h-10 min-w-0 flex-1 gap-2 py-3 pl-3 pr-10`}
|
||||||
onClick={() => props.openSession(props.record.session)}
|
onClick={(event) => props.openSession(props.record.session, { background: isBackgroundOpen(event) })}
|
||||||
>
|
>
|
||||||
<HomeSessionLeading
|
<HomeSessionLeading
|
||||||
project={props.record.project}
|
project={props.record.project}
|
||||||
|
|||||||
Reference in New Issue
Block a user