mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-12 18:40:54 +00:00
feat(tui): reopen closed session tabs (#39731)
This commit is contained in:
parent
8ebc4c6f85
commit
cc883058df
5 changed files with 105 additions and 0 deletions
|
|
@ -105,6 +105,7 @@ const sessionTabBindingCommands = [
|
|||
"session.tab.next_unread",
|
||||
"session.tab.previous_unread",
|
||||
"session.tab.close",
|
||||
"session.tab.reopen",
|
||||
"session.tab.select.1",
|
||||
"session.tab.select.2",
|
||||
"session.tab.select.3",
|
||||
|
|
@ -721,6 +722,13 @@ function App(props: { pair?: DialogPairCredentials }) {
|
|||
enabled: sessionTabs.enabled,
|
||||
run: () => sessionTabs.close(),
|
||||
},
|
||||
{
|
||||
name: "session.tab.reopen",
|
||||
title: "Reopen closed tab",
|
||||
category: "Session",
|
||||
enabled: sessionTabs.enabled,
|
||||
run: () => sessionTabs.reopen(),
|
||||
},
|
||||
...Array.from({ length: 9 }, (_, i) => ({
|
||||
name: `session.tab.select.${i + 1}`,
|
||||
title: `Switch to tab ${i + 1}`,
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ export const Definitions = {
|
|||
session_tab_next_unread: keybind("<leader>down", "Switch to next unread tab"),
|
||||
session_tab_previous_unread: keybind("<leader>up", "Switch to previous unread tab"),
|
||||
session_tab_close: keybind("<leader>w", "Close current tab"),
|
||||
session_tab_reopen: keybind("ctrl+shift+t", "Reopen last closed tab"),
|
||||
session_timeline: keybind("<leader>g", "Show session timeline"),
|
||||
session_fork: keybind("none", "Fork session from message"),
|
||||
session_rename: keybind("ctrl+r", "Rename session"),
|
||||
|
|
@ -297,6 +298,7 @@ export const CommandMap = {
|
|||
session_tab_next_unread: "session.tab.next_unread",
|
||||
session_tab_previous_unread: "session.tab.previous_unread",
|
||||
session_tab_close: "session.tab.close",
|
||||
session_tab_reopen: "session.tab.reopen",
|
||||
session_timeline: "session.timeline",
|
||||
session_fork: "session.fork",
|
||||
session_rename: "session.rename",
|
||||
|
|
|
|||
|
|
@ -36,6 +36,39 @@ export function closeSessionTab(tabs: readonly SessionTab[], sessionID: string)
|
|||
}
|
||||
}
|
||||
|
||||
export type ClosedSessionTab = {
|
||||
tab: SessionTab
|
||||
index: number
|
||||
}
|
||||
|
||||
const CLOSED_SESSION_TAB_LIMIT = 10
|
||||
|
||||
export function recordClosedSessionTab(
|
||||
stack: readonly ClosedSessionTab[],
|
||||
tab: SessionTab,
|
||||
index: number,
|
||||
): ClosedSessionTab[] {
|
||||
return [...stack.filter((entry) => entry.tab.sessionID !== tab.sessionID), { tab, index }].slice(
|
||||
-CLOSED_SESSION_TAB_LIMIT,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pop the most recently closed tab that is not already open and restore it at its original
|
||||
* position. Entries for already-open sessions are consumed so repeated reopens walk the stack.
|
||||
*/
|
||||
export function reopenSessionTab(stack: readonly ClosedSessionTab[], tabs: readonly SessionTab[]) {
|
||||
const remaining = [...stack]
|
||||
while (remaining.length > 0) {
|
||||
const entry = remaining.pop()!
|
||||
if (tabs.some((tab) => tab.sessionID === entry.tab.sessionID)) continue
|
||||
const next = [...tabs]
|
||||
next.splice(Math.min(entry.index, tabs.length), 0, entry.tab)
|
||||
return { stack: remaining, tabs: next, sessionID: entry.tab.sessionID }
|
||||
}
|
||||
return { stack: remaining, tabs: undefined, sessionID: undefined }
|
||||
}
|
||||
|
||||
export function moveSessionTab(tabs: SessionTab[], sessionID: string, index: number): SessionTab[] {
|
||||
const from = tabs.findIndex((tab) => tab.sessionID === sessionID)
|
||||
const to = Math.max(0, Math.min(tabs.length - 1, index))
|
||||
|
|
|
|||
|
|
@ -14,7 +14,10 @@ import {
|
|||
moveSessionTab,
|
||||
moveSessionTabHistory,
|
||||
openSessionTab,
|
||||
recordClosedSessionTab,
|
||||
recordSessionTabHistory,
|
||||
reopenSessionTab,
|
||||
type ClosedSessionTab,
|
||||
type SessionTab,
|
||||
type SessionTabHistory,
|
||||
type SessionTabUnread,
|
||||
|
|
@ -57,6 +60,8 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp
|
|||
const fallback = empty()
|
||||
const [promptPulses, setPromptPulses] = createSignal<Record<string, number>>({})
|
||||
let history: SessionTabHistory = { entries: [], index: -1 }
|
||||
// User-closed tabs eligible for reopening; in-memory like history, deleted sessions pruned.
|
||||
let closedTabs: ClosedSessionTab[] = []
|
||||
|
||||
function state() {
|
||||
if (config.tabs?.scope === "global") return store.global
|
||||
|
|
@ -193,6 +198,8 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp
|
|||
)
|
||||
onCleanup(
|
||||
event.on("session.deleted", (evt) => {
|
||||
const target = root(evt.data.sessionID)
|
||||
closedTabs = closedTabs.filter((entry) => entry.tab.sessionID !== target)
|
||||
remove(evt.data.sessionID, enabled())
|
||||
}),
|
||||
)
|
||||
|
|
@ -244,8 +251,22 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp
|
|||
if (route.data.type === "home" && session) route.navigate({ type: "session", sessionID: session })
|
||||
return
|
||||
}
|
||||
const index = state().tabs.findIndex((tab) => tab.sessionID === target)
|
||||
const tab = state().tabs[index]
|
||||
if (tab) closedTabs = recordClosedSessionTab(closedTabs, tab, index)
|
||||
remove(target, true)
|
||||
},
|
||||
reopen() {
|
||||
if (!enabled()) return
|
||||
const result = reopenSessionTab(closedTabs, state().tabs)
|
||||
closedTabs = result.stack
|
||||
const tabs = result.tabs
|
||||
if (!tabs || !result.sessionID) return
|
||||
update((draft) => {
|
||||
draft.tabs = tabs
|
||||
})
|
||||
route.navigate({ type: "session", sessionID: result.sessionID })
|
||||
},
|
||||
move(sessionID: string, index: number) {
|
||||
if (!enabled()) return
|
||||
const session = root(sessionID)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue