소스 검색

feat(tui): enable pinned session switching (#27780)

Shoubhit Dash 3 달 전
부모
커밋
f33b4455a1

+ 0 - 1
packages/core/src/flag/flag.ts

@@ -44,7 +44,6 @@ export const Flag = {
 
   OPENCODE_WORKSPACE_ID: process.env["OPENCODE_WORKSPACE_ID"],
   OPENCODE_EXPERIMENTAL_WORKSPACES: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_WORKSPACES"),
-  OPENCODE_EXPERIMENTAL_SESSION_SWITCHING: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_SESSION_SWITCHING"),
 
   // Evaluated at access time (not module load) because tests, the CLI, and
   // external tooling set these env vars at runtime.

+ 10 - 17
packages/opencode/src/cli/cmd/tui/app.tsx

@@ -479,17 +479,15 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
           dialog.clear()
         },
       },
-      ...(Flag.OPENCODE_EXPERIMENTAL_SESSION_SWITCHING
-        ? 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)
-            },
-          }))
-        : []),
+      ...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",
         title: "Switch model",
@@ -804,12 +802,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
 
   useBindings(() => ({
     enabled: command.matcher,
-    bindings: tuiConfig.keybinds.gather(
-      "app",
-      Flag.OPENCODE_EXPERIMENTAL_SESSION_SWITCHING
-        ? appBindingCommands
-        : appBindingCommands.filter((c) => !c.startsWith("session.quick_switch")),
-    ),
+    bindings: tuiConfig.keybinds.gather("app", appBindingCommands),
   }))
 
   useBindings(() => ({

+ 29 - 16
packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx

@@ -31,6 +31,8 @@ export function DialogSessionList() {
   const [toDelete, setToDelete] = createSignal<string>()
   const [search, setSearch] = createDebouncedSignal("", 150)
   const deleteHint = useCommandShortcut("session.delete")
+  const quickSwitch1 = useCommandShortcut("session.quick_switch.1")
+  const quickSwitch9 = useCommandShortcut("session.quick_switch.9")
 
   const [searchResults, { refetch }] = createResource(
     () => ({ query: search(), filter: sync.session.query() }),
@@ -130,8 +132,18 @@ export function DialogSessionList() {
 
   const [browseOrder] = createSignal<string[]>(orderByRecency(sync.data.session))
 
+  const quickSwitchHint = createMemo(() => {
+    const first = quickSwitch1()
+    const last = quickSwitch9()
+    if (!first || !last) return undefined
+    return quickSwitchRange(first, last)
+  })
+  const quickSwitchFooterHints = createMemo(() => {
+    const hint = quickSwitchHint()
+    return hint && local.session.slots().length > 0 ? [{ title: "switch", label: hint }] : []
+  })
+
   const options = createMemo(() => {
-    const enabled = Flag.OPENCODE_EXPERIMENTAL_SESSION_SWITCHING
     const today = new Date().toDateString()
     const sessionMap = new Map(
       sessions()
@@ -142,11 +154,9 @@ export function DialogSessionList() {
     const searchResult = searchResults()
     const displayOrder = searchResult ? orderByRecency(searchResult) : browseOrder()
 
-    const pinned = enabled ? local.session.pinned().filter((id) => sessionMap.has(id)) : []
+    const pinned = 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 slotByID = new Map<string, number>(local.session.slots().map((id, i) => [id, i + 1]))
 
     function buildOption(id: string, category: string) {
       const x = sessionMap.get(id)
@@ -224,17 +234,13 @@ export function DialogSessionList() {
         dialog.clear()
       }}
       actions={[
-        ...(Flag.OPENCODE_EXPERIMENTAL_SESSION_SWITCHING
-          ? [
-              {
-                command: "session.pin.toggle",
-                title: "pin/unpin",
-                onTrigger: (option: { value: string }) => {
-                  local.session.togglePin(option.value)
-                },
-              },
-            ]
-          : []),
+        {
+          command: "session.pin.toggle",
+          title: "pin/unpin",
+          onTrigger: (option: { value: string }) => {
+            local.session.togglePin(option.value)
+          },
+        },
         {
           command: "session.delete",
           title: "delete",
@@ -291,6 +297,13 @@ export function DialogSessionList() {
           },
         },
       ]}
+      footerHints={quickSwitchFooterHints()}
     />
   )
 }
+
+function quickSwitchRange(first: string, last: string) {
+  const prefix = first.slice(0, -1)
+  if (first.endsWith("1") && last === `${prefix}9`) return `${prefix}1-9`
+  return `${first} through ${last}`
+}

+ 6 - 12
packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx

@@ -1,7 +1,6 @@
 import type { TuiPluginApi } from "@opencode-ai/plugin/tui"
 import { createMemo, For, type Accessor } from "solid-js"
 import { DEFAULT_THEMES, useTheme } from "@tui/context/theme"
-import { Flag } from "@opencode-ai/core/flag/flag"
 import { useCommandShortcut } from "../../keymap"
 
 const themeCount = Object.keys(DEFAULT_THEMES).length
@@ -170,17 +169,12 @@ const TIPS: Tip[] = [
   (shortcuts) => `Use ${commandText("/models", shortcuts.modelList())} to see and switch between available AI models`,
   (shortcuts) => `Use ${commandText("/themes", shortcuts.themeList())} to switch between ${themeCount} built-in themes`,
   (shortcuts) => `Use ${commandText("/new", shortcuts.sessionNew())} to start a fresh conversation session`,
-  (shortcuts) => `Use ${commandText("/sessions", shortcuts.sessionList())} to list and continue previous conversations`,
-  ...(Flag.OPENCODE_EXPERIMENTAL_SESSION_SWITCHING
-    ? ([
-        (shortcuts) =>
-          press(shortcuts.sessionPinToggle(), "in the session list to pin a session so it stays at the top"),
-        (shortcuts) =>
-          shortcuts.sessionQuickSwitch1() && shortcuts.sessionQuickSwitch9()
-            ? `Pinned sessions are bound to ${shortcutText(shortcuts.sessionQuickSwitch1())} through ${shortcutText(shortcuts.sessionQuickSwitch9())} for one-press switching`
-            : undefined,
-      ] satisfies Tip[])
-    : []),
+  (shortcuts) => `Use ${commandText("/sessions", shortcuts.sessionList())} to list, pin, and continue sessions`,
+  (shortcuts) => press(shortcuts.sessionPinToggle(), "in the session list to pin a session so it stays at the top"),
+  (shortcuts) =>
+    shortcuts.sessionQuickSwitch1() && shortcuts.sessionQuickSwitch9()
+      ? `Pinned sessions are assigned quick slots; use ${shortcutText(shortcuts.sessionQuickSwitch1())} through ${shortcutText(shortcuts.sessionQuickSwitch9())} to switch`
+      : undefined,
   "Run {highlight}/compact{/highlight} to summarize long sessions near context limits",
   (shortcuts) => `Use ${commandText("/export", shortcuts.sessionExport())} to save the conversation as Markdown`,
   (shortcuts) => press(shortcuts.messagesCopy(), "to copy the assistant's last message to clipboard"),

+ 9 - 3
packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx

@@ -38,6 +38,11 @@ export interface DialogSelectProps<T> {
     disabled?: boolean
     onTrigger: (option: DialogSelectOption<T>) => void
   }[]
+  footerHints?: {
+    title: string
+    label: string
+    side?: "left" | "right"
+  }[]
   bindings?: readonly Binding<Renderable, KeyEvent>[]
   current?: T
 }
@@ -334,11 +339,12 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
   }
   props.ref?.(ref)
 
-  const visibleActions = createMemo(() =>
-    actions()
+  const visibleActions = createMemo(() => [
+    ...actions()
       .map((item) => ({ ...item, label: actionLabels().get(item.command) ?? "" }))
       .filter((item) => !item.disabled && item.label),
-  )
+    ...(props.footerHints ?? []),
+  ])
   const left = createMemo(() => visibleActions().filter((item) => item.side !== "right"))
   const right = createMemo(() => visibleActions().filter((item) => item.side === "right"))