mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-17 01:52:55 +00:00
fix(tui): keep session switching pinned-only (#27775)
This commit is contained in:
@@ -76,8 +76,6 @@ const appBindingCommands = [
|
||||
"command.palette.show",
|
||||
"session.list",
|
||||
"session.new",
|
||||
"session.cycle_recent",
|
||||
"session.cycle_recent_reverse",
|
||||
"session.quick_switch.1",
|
||||
"session.quick_switch.2",
|
||||
"session.quick_switch.3",
|
||||
@@ -482,35 +480,15 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||
},
|
||||
},
|
||||
...(Flag.OPENCODE_EXPERIMENTAL_SESSION_SWITCHING
|
||||
? [
|
||||
{
|
||||
name: "session.cycle_recent",
|
||||
title: "Cycle to previous recent session",
|
||||
category: "Session",
|
||||
hidden: true,
|
||||
run: () => {
|
||||
local.session.cycleRecent(1)
|
||||
},
|
||||
? Array.from({ length: 9 }, (_, i) => ({
|
||||
name: `session.quick_switch.${i + 1}`,
|
||||
title: `Switch to session in quick slot ${i + 1}`,
|
||||
category: "Session",
|
||||
hidden: true,
|
||||
run: () => {
|
||||
local.session.quickSwitch(i + 1)
|
||||
},
|
||||
{
|
||||
name: "session.cycle_recent_reverse",
|
||||
title: "Cycle to next recent session",
|
||||
category: "Session",
|
||||
hidden: true,
|
||||
run: () => {
|
||||
local.session.cycleRecent(-1)
|
||||
},
|
||||
},
|
||||
...Array.from({ length: 9 }, (_, i) => ({
|
||||
name: `session.quick_switch.${i + 1}`,
|
||||
title: `Switch to session in quick slot ${i + 1}`,
|
||||
category: "Session",
|
||||
hidden: true,
|
||||
run: () => {
|
||||
local.session.quickSwitch(i + 1)
|
||||
},
|
||||
})),
|
||||
]
|
||||
}))
|
||||
: []),
|
||||
{
|
||||
name: "model.list",
|
||||
@@ -830,9 +808,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||
"app",
|
||||
Flag.OPENCODE_EXPERIMENTAL_SESSION_SWITCHING
|
||||
? appBindingCommands
|
||||
: appBindingCommands.filter(
|
||||
(c) => !c.startsWith("session.cycle_recent") && !c.startsWith("session.quick_switch"),
|
||||
),
|
||||
: appBindingCommands.filter((c) => !c.startsWith("session.quick_switch")),
|
||||
),
|
||||
}))
|
||||
|
||||
|
||||
@@ -130,8 +130,6 @@ export function DialogSessionList() {
|
||||
|
||||
const [browseOrder] = createSignal<string[]>(orderByRecency(sync.data.session))
|
||||
|
||||
const RECENT_LIMIT = 5
|
||||
|
||||
const options = createMemo(() => {
|
||||
const enabled = Flag.OPENCODE_EXPERIMENTAL_SESSION_SWITCHING
|
||||
const today = new Date().toDateString()
|
||||
@@ -144,18 +142,12 @@ export function DialogSessionList() {
|
||||
const searchResult = searchResults()
|
||||
const displayOrder = searchResult ? orderByRecency(searchResult) : browseOrder()
|
||||
|
||||
const dismissed = enabled ? new Set(local.session.dismissedRecent()) : new Set<string>()
|
||||
const pinned = enabled ? local.session.pinned().filter((id) => sessionMap.has(id)) : []
|
||||
const pinnedSet = new Set(pinned)
|
||||
const slotByID = enabled
|
||||
? new Map<string, number>(local.session.slots().map((id, i) => [id, i + 1]))
|
||||
: new Map<string, number>()
|
||||
|
||||
const recent = enabled
|
||||
? displayOrder.filter((id) => !pinnedSet.has(id) && !dismissed.has(id)).slice(0, RECENT_LIMIT)
|
||||
: []
|
||||
const recentSet = new Set(recent)
|
||||
|
||||
function buildOption(id: string, category: string) {
|
||||
const x = sessionMap.get(id)
|
||||
if (!x) return undefined
|
||||
@@ -198,7 +190,7 @@ export function DialogSessionList() {
|
||||
}
|
||||
|
||||
const remaining = displayOrder
|
||||
.filter((id) => !pinnedSet.has(id) && !recentSet.has(id))
|
||||
.filter((id) => !pinnedSet.has(id))
|
||||
.map((id) => {
|
||||
const x = sessionMap.get(id)
|
||||
if (!x) return undefined
|
||||
@@ -209,7 +201,6 @@ export function DialogSessionList() {
|
||||
|
||||
return [
|
||||
...pinned.map((id) => buildOption(id, "Pinned")).filter((x) => x !== undefined),
|
||||
...recent.map((id) => buildOption(id, "Recent")).filter((x) => x !== undefined),
|
||||
...remaining,
|
||||
]
|
||||
})
|
||||
@@ -245,21 +236,6 @@ export function DialogSessionList() {
|
||||
local.session.togglePin(option.value)
|
||||
},
|
||||
},
|
||||
{
|
||||
command: "session.toggle.recent",
|
||||
title: "toggle recent",
|
||||
onTrigger: (option: { value: string }) => {
|
||||
if (local.session.isPinned(option.value)) {
|
||||
toast.show({
|
||||
variant: "info",
|
||||
message: "Unpin the session first to toggle it in Recent",
|
||||
duration: 3000,
|
||||
})
|
||||
return
|
||||
}
|
||||
local.session.toggleRecent(option.value)
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
|
||||
@@ -87,9 +87,6 @@ export const Definitions = {
|
||||
session_child_cycle_reverse: keybind("left", "Go to previous child session"),
|
||||
session_parent: keybind("up", "Go to parent session"),
|
||||
session_pin_toggle: keybind("ctrl+f", "Pin or unpin session in the session list"),
|
||||
session_toggle_recent: keybind("ctrl+h", "Show or hide session in the Recent group"),
|
||||
session_cycle_recent: keybind("<leader>]", "Cycle to the previous recent session"),
|
||||
session_cycle_recent_reverse: keybind("<leader>[", "Cycle to the next recent session"),
|
||||
session_quick_switch_1: keybind("<leader>1", "Switch to session in quick slot 1"),
|
||||
session_quick_switch_2: keybind("<leader>2", "Switch to session in quick slot 2"),
|
||||
session_quick_switch_3: keybind("<leader>3", "Switch to session in quick slot 3"),
|
||||
@@ -273,9 +270,6 @@ export const CommandMap = {
|
||||
session_child_cycle_reverse: "session.child.previous",
|
||||
session_parent: "session.parent",
|
||||
session_pin_toggle: "session.pin.toggle",
|
||||
session_toggle_recent: "session.toggle.recent",
|
||||
session_cycle_recent: "session.cycle_recent",
|
||||
session_cycle_recent_reverse: "session.cycle_recent_reverse",
|
||||
session_quick_switch_1: "session.quick_switch.1",
|
||||
session_quick_switch_2: "session.quick_switch.2",
|
||||
session_quick_switch_3: "session.quick_switch.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { batch, createEffect, createMemo, on } from "solid-js"
|
||||
import { batch, createEffect, createMemo } from "solid-js"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { useTheme } from "@tui/context/theme"
|
||||
import { useRoute } from "@tui/context/route"
|
||||
@@ -8,7 +8,6 @@ import { useEvent } from "@tui/context/event"
|
||||
import { uniqueBy } from "remeda"
|
||||
import path from "path"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { iife } from "@/util/iife"
|
||||
import { useToast } from "../ui/toast"
|
||||
import { useArgs } from "./args"
|
||||
@@ -387,13 +386,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
const [sessionStore, setSessionStore] = createStore<{
|
||||
ready: boolean
|
||||
pinned: string[]
|
||||
dismissedRecent: string[]
|
||||
recentOrder: string[]
|
||||
}>({
|
||||
ready: false,
|
||||
pinned: [],
|
||||
dismissedRecent: [],
|
||||
recentOrder: [],
|
||||
})
|
||||
|
||||
const filePath = path.join(Global.Path.state, "session.json")
|
||||
@@ -409,16 +404,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
state.pending = false
|
||||
void Filesystem.writeJson(filePath, {
|
||||
pinned: sessionStore.pinned,
|
||||
dismissedRecent: sessionStore.dismissedRecent,
|
||||
recentOrder: sessionStore.recentOrder,
|
||||
})
|
||||
}
|
||||
|
||||
Filesystem.readJson(filePath)
|
||||
.then((x: any) => {
|
||||
if (Array.isArray(x.pinned)) setSessionStore("pinned", x.pinned)
|
||||
if (Array.isArray(x.dismissedRecent)) setSessionStore("dismissedRecent", x.dismissedRecent)
|
||||
if (Array.isArray(x.recentOrder)) setSessionStore("recentOrder", x.recentOrder)
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
@@ -428,19 +419,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
|
||||
const route = useRoute()
|
||||
const event = useEvent()
|
||||
let cycling = false
|
||||
|
||||
const slots = createMemo(() => {
|
||||
const rootSessions = sync.data.session.filter((x) => x.parentID === undefined)
|
||||
const existing = new Set(rootSessions.map((x) => x.id))
|
||||
const dismissed = new Set(sessionStore.dismissedRecent)
|
||||
const pins = sessionStore.pinned.filter((id) => existing.has(id))
|
||||
const pinnedSet = new Set(pins)
|
||||
const recent = rootSessions
|
||||
.filter((x) => !pinnedSet.has(x.id) && !dismissed.has(x.id))
|
||||
.toSorted((a, b) => b.time.updated - a.time.updated)
|
||||
.map((x) => x.id)
|
||||
return [...pins, ...recent].slice(0, 9)
|
||||
const existing = new Set(sync.data.session.filter((x) => x.parentID === undefined).map((x) => x.id))
|
||||
return sessionStore.pinned.filter((id) => existing.has(id)).slice(0, 9)
|
||||
})
|
||||
|
||||
function prune(sessionID: string) {
|
||||
@@ -451,18 +433,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
sessionStore.pinned.filter((x) => x !== sessionID),
|
||||
)
|
||||
}
|
||||
if (sessionStore.dismissedRecent.includes(sessionID)) {
|
||||
setSessionStore(
|
||||
"dismissedRecent",
|
||||
sessionStore.dismissedRecent.filter((x) => x !== sessionID),
|
||||
)
|
||||
}
|
||||
if (sessionStore.recentOrder.includes(sessionID)) {
|
||||
setSessionStore(
|
||||
"recentOrder",
|
||||
sessionStore.recentOrder.filter((x) => x !== sessionID),
|
||||
)
|
||||
}
|
||||
save()
|
||||
})
|
||||
}
|
||||
@@ -471,25 +441,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
prune(evt.properties.info.id)
|
||||
})
|
||||
|
||||
if (Flag.OPENCODE_EXPERIMENTAL_SESSION_SWITCHING) {
|
||||
createEffect(
|
||||
on(
|
||||
() => (sessionStore.ready && route.data.type === "session" ? route.data.sessionID : undefined),
|
||||
(sessionID) => {
|
||||
if (!sessionID) return
|
||||
if (cycling) {
|
||||
cycling = false
|
||||
return
|
||||
}
|
||||
const filtered = sessionStore.recentOrder.filter((x) => x !== sessionID)
|
||||
const next = [sessionID, ...filtered].slice(0, 20)
|
||||
setSessionStore("recentOrder", next)
|
||||
save()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
get ready() {
|
||||
return sessionStore.ready
|
||||
@@ -497,19 +448,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
pinned() {
|
||||
return sessionStore.pinned
|
||||
},
|
||||
dismissedRecent() {
|
||||
return sessionStore.dismissedRecent
|
||||
},
|
||||
recentOrder() {
|
||||
return sessionStore.recentOrder
|
||||
},
|
||||
slots,
|
||||
isPinned(sessionID: string) {
|
||||
return sessionStore.pinned.includes(sessionID)
|
||||
},
|
||||
isDismissed(sessionID: string) {
|
||||
return sessionStore.dismissedRecent.includes(sessionID)
|
||||
},
|
||||
togglePin(sessionID: string) {
|
||||
batch(() => {
|
||||
const exists = sessionStore.pinned.includes(sessionID)
|
||||
@@ -520,52 +462,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
save()
|
||||
})
|
||||
},
|
||||
toggleRecent(sessionID: string) {
|
||||
batch(() => {
|
||||
const exists = sessionStore.dismissedRecent.includes(sessionID)
|
||||
const next = exists
|
||||
? sessionStore.dismissedRecent.filter((x) => x !== sessionID)
|
||||
: [sessionID, ...sessionStore.dismissedRecent]
|
||||
setSessionStore("dismissedRecent", next)
|
||||
save()
|
||||
})
|
||||
},
|
||||
quickSwitch(slot: number) {
|
||||
const target = slots()[slot - 1]
|
||||
if (!target) return
|
||||
if (route.data.type === "session" && route.data.sessionID === target) return
|
||||
route.navigate({ type: "session", sessionID: target })
|
||||
},
|
||||
cycleRecent(direction: 1 | -1) {
|
||||
if (route.data.type !== "session") {
|
||||
toast.show({
|
||||
variant: "info",
|
||||
message: "Open a session first to cycle between recent sessions",
|
||||
duration: 3000,
|
||||
})
|
||||
return
|
||||
}
|
||||
const current = route.data.sessionID
|
||||
const order = sessionStore.recentOrder.filter((id) =>
|
||||
sync.data.session.some((s) => s.id === id && s.parentID === undefined),
|
||||
)
|
||||
if (order.length < 2) {
|
||||
toast.show({
|
||||
variant: "info",
|
||||
message: "No other recent sessions to cycle to",
|
||||
duration: 3000,
|
||||
})
|
||||
return
|
||||
}
|
||||
const index = order.indexOf(current)
|
||||
if (index === -1) return
|
||||
const next = index + direction
|
||||
if (next < 0 || next >= order.length) return
|
||||
const target = order[next]
|
||||
if (!target || target === current) return
|
||||
cycling = true
|
||||
route.navigate({ type: "session", sessionID: target })
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -29,8 +29,6 @@ type Shortcuts = {
|
||||
messagesToggleConceal: TipShortcut
|
||||
modelCycleRecent: TipShortcut
|
||||
modelList: TipShortcut
|
||||
sessionCycleRecent: TipShortcut
|
||||
sessionCycleRecentReverse: TipShortcut
|
||||
sessionExport: TipShortcut
|
||||
sessionInterrupt: TipShortcut
|
||||
sessionList: TipShortcut
|
||||
@@ -41,7 +39,6 @@ type Shortcuts = {
|
||||
sessionQuickSwitch9: TipShortcut
|
||||
sessionSidebarToggle: TipShortcut
|
||||
sessionTimeline: TipShortcut
|
||||
sessionToggleRecent: TipShortcut
|
||||
statusView: TipShortcut
|
||||
terminalSuspend: TipShortcut
|
||||
themeList: TipShortcut
|
||||
@@ -121,8 +118,6 @@ export function Tips(props: { api: TuiPluginApi; connected?: boolean }) {
|
||||
messagesToggleConceal: configShortcut(props.api, "session.toggle.conceal"),
|
||||
modelCycleRecent: useCommandShortcut("model.cycle_recent"),
|
||||
modelList: useCommandShortcut("model.list"),
|
||||
sessionCycleRecent: useCommandShortcut("session.cycle_recent"),
|
||||
sessionCycleRecentReverse: useCommandShortcut("session.cycle_recent_reverse"),
|
||||
sessionExport: configShortcut(props.api, "session.export"),
|
||||
sessionInterrupt: configShortcut(props.api, "session.interrupt"),
|
||||
sessionList: useCommandShortcut("session.list"),
|
||||
@@ -133,7 +128,6 @@ export function Tips(props: { api: TuiPluginApi; connected?: boolean }) {
|
||||
sessionQuickSwitch9: useCommandShortcut("session.quick_switch.9"),
|
||||
sessionSidebarToggle: configShortcut(props.api, "session.sidebar.toggle"),
|
||||
sessionTimeline: configShortcut(props.api, "session.timeline"),
|
||||
sessionToggleRecent: configShortcut(props.api, "session.toggle.recent"),
|
||||
statusView: useCommandShortcut("opencode.status"),
|
||||
terminalSuspend: useCommandShortcut("terminal.suspend"),
|
||||
themeList: useCommandShortcut("theme.switch"),
|
||||
@@ -183,14 +177,8 @@ const TIPS: Tip[] = [
|
||||
press(shortcuts.sessionPinToggle(), "in the session list to pin a session so it stays at the top"),
|
||||
(shortcuts) =>
|
||||
shortcuts.sessionQuickSwitch1() && shortcuts.sessionQuickSwitch9()
|
||||
? `Pinned and recent sessions are bound to ${shortcutText(shortcuts.sessionQuickSwitch1())} through ${shortcutText(shortcuts.sessionQuickSwitch9())} for one-press switching`
|
||||
? `Pinned sessions are bound to ${shortcutText(shortcuts.sessionQuickSwitch1())} through ${shortcutText(shortcuts.sessionQuickSwitch9())} for one-press switching`
|
||||
: undefined,
|
||||
(shortcuts) =>
|
||||
shortcuts.sessionCycleRecent() && shortcuts.sessionCycleRecentReverse()
|
||||
? `Press ${shortcutText(shortcuts.sessionCycleRecent())} / ${shortcutText(shortcuts.sessionCycleRecentReverse())} to cycle through recently visited sessions`
|
||||
: undefined,
|
||||
(shortcuts) =>
|
||||
press(shortcuts.sessionToggleRecent(), "in the session list to show or hide a session in the Recent group"),
|
||||
] satisfies Tip[])
|
||||
: []),
|
||||
"Run {highlight}/compact{/highlight} to summarize long sessions near context limits",
|
||||
|
||||
Reference in New Issue
Block a user