From 58fcad77a8a2e4abcbe985c395e7192801003857 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Sun, 20 Sep 2026 19:15:37 +0000 Subject: [PATCH] feat(tui): copy session ID from tab menu (#50181) Co-authored-by: kitlangton <7587245+kitlangton@users.noreply.github.com> --- packages/tui/src/component/session-tabs.tsx | 14 +++++++++++++- .../test/component/session-tabs-status.test.tsx | 10 ++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/tui/src/component/session-tabs.tsx b/packages/tui/src/component/session-tabs.tsx index cd555a6f8ca..d96b007627d 100644 --- a/packages/tui/src/component/session-tabs.tsx +++ b/packages/tui/src/component/session-tabs.tsx @@ -23,6 +23,7 @@ import { Portal, useRenderer, useTerminalDimensions } from "@opentui/solid" import { useConfig } from "../config" import { useSessionTabs } from "../context/session-tabs" import { useData } from "../context/data" +import { useClipboard } from "../context/clipboard" import { useTheme } from "../context/theme" import { adaptiveSessionTabLayout, @@ -44,6 +45,7 @@ import { SESSION_SIDEBAR_WIDTH, SESSION_TABS_COMPACT_BREAKPOINT } from "../ui/la import { projectName } from "../util/project" import { marqueeCycleWidth, marqueeOverflows, marqueeTextParts } from "../util/marquee" import { useDialog } from "../ui/dialog" +import { useToast } from "../ui/toast" import { DialogSessionRename } from "./dialog-session-rename" import { Keymap } from "../context/keymap" import { registerOpencodeSpinner } from "./register-spinner" @@ -75,7 +77,7 @@ const FADE_WIDTH = 4 const ADD_TAB_WIDTH = 3 const MARQUEE_DELAY = 600 const MARQUEE_INTERVAL = 80 -const CONTEXT_MENU_WIDTH = 16 +const CONTEXT_MENU_WIDTH = 20 const MIDDLE_MOUSE_BUTTON = 1 const RIGHT_MOUSE_BUTTON = 2 const MOUSE_CLOSE_HOLD_MS = 5_000 @@ -370,6 +372,8 @@ function TabContextMenu(props: { state: TabContextMenuState; tabs: SessionTabsCo const background = () => theme.background.raised.base const actionHovered = () => theme.background.raised.high const dialog = useDialog() + const clipboard = useClipboard() + const toast = useToast() onCleanup(Keymap.use().mode.push("menu")) Keymap.createLayer(() => ({ mode: "menu", @@ -387,6 +391,14 @@ function TabContextMenu(props: { state: TabContextMenuState; tabs: SessionTabsCo run: () => props.tabs.rename ? props.tabs.rename(sessionID) : DialogSessionRename.show(dialog, sessionID, title), }, + { + title: "Copy session ID", + run: () => + void clipboard + .write(sessionID) + .then(() => toast.show({ message: "Session ID copied to clipboard", variant: "info" })) + .catch(toast.error), + }, { title: "Close", run: () => props.tabs.close(sessionID) }, ] : []), diff --git a/packages/tui/test/component/session-tabs-status.test.tsx b/packages/tui/test/component/session-tabs-status.test.tsx index 0099353703f..f9ccda102d0 100644 --- a/packages/tui/test/component/session-tabs-status.test.tsx +++ b/packages/tui/test/component/session-tabs-status.test.tsx @@ -34,6 +34,7 @@ for (const orientation of ["horizontal", "vertical"] as const) { const [active, setActive] = createSignal("second") const [newTab, setNewTab] = createSignal(false) const settings: Info = { tabs: { enabled: true } } + const copied: string[] = [] let config!: ReturnType let theme!: ReturnType function Colors() { @@ -61,7 +62,10 @@ for (const orientation of ["horizontal", "vertical"] as const) { } satisfies SessionTabsController const app = await testRender( () => ( - + undefined, write: async (text) => void copied.push(text) }} + > frame.includes("Rename")) expect(app.captureCharFrame().split("\n")[row + 1]!.indexOf("Rename")).toBe(column + 1) + expect(app.captureCharFrame()).toContain("Copy session ID") expect(app.captureCharFrame()).toContain("Close") expect(app.captureCharFrame()).not.toContain("Keep open") expect(active()).toBe("second") - app.mockInput.pressKey("c", { ctrl: true }) + await app.mockMouse.click(column + 1, row + 2) + expect(copied).toEqual(["first"]) await app.waitForFrame((frame) => !frame.includes("Rename")) setNewTab(true)