From 9875092e5d3b72f72c3d89d499420a5f8624b705 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Feb 2026 16:14:52 +0000 Subject: [PATCH 1/3] Add kebab menus for session/window/pane actions in sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace flat action buttons at the bottom of the drawer with per-entity kebab menus (⋮) on each session, window, and pane row. Each menu exposes context-specific tmux operations scoped to that entity. Destructive actions (kill session/window/pane, respawn pane) require confirmation. New backend operations: rename session, rename window, kill session, respawn pane, break pane to window, swap window order. Closes #20 https://claude.ai/code/session_01UcinJH2bV8mC345tgYTvFa --- src/backend/server.ts | 20 +++ src/backend/tmux/cli-executor.ts | 20 +++ src/backend/tmux/types.ts | 5 + src/backend/types/protocol.ts | 8 +- src/frontend/App.tsx | 287 +++++++++++++++++++++---------- src/frontend/styles/app.css | 57 +++++- 6 files changed, 304 insertions(+), 93 deletions(-) diff --git a/src/backend/server.ts b/src/backend/server.ts index ec9e7a2..1024878 100644 --- a/src/backend/server.ts +++ b/src/backend/server.ts @@ -216,6 +216,26 @@ export const createTmuxMobileServer = ( case "send_compose": runtime.write(`${message.text}\r`); return; + case "rename_session": + await deps.tmux.renameSession(message.session, message.newName); + return; + case "rename_window": + await deps.tmux.renameWindow(message.session, message.windowIndex, message.newName); + return; + case "kill_session": { + await deps.tmux.killSession(message.session); + await ensureAttachedSession(socket); + return; + } + case "respawn_pane": + await deps.tmux.respawnPane(message.paneId); + return; + case "break_pane": + await deps.tmux.breakPane(message.paneId); + return; + case "swap_window": + await deps.tmux.swapWindow(message.session, message.srcIndex, message.dstIndex); + return; case "auth": return; default: { diff --git a/src/backend/tmux/cli-executor.ts b/src/backend/tmux/cli-executor.ts index 4b0772a..d168d2c 100644 --- a/src/backend/tmux/cli-executor.ts +++ b/src/backend/tmux/cli-executor.ts @@ -150,4 +150,24 @@ export class TmuxCliExecutor implements TmuxGateway { public async capturePane(paneId: string, lines: number): Promise { return this.runTmux(["capture-pane", "-t", paneId, "-p", "-S", `-${lines}`]); } + + public async renameSession(oldName: string, newName: string): Promise { + await this.runTmux(["rename-session", "-t", oldName, newName]); + } + + public async renameWindow(session: string, windowIndex: number, newName: string): Promise { + await this.runTmux(["rename-window", "-t", `${session}:${windowIndex}`, newName]); + } + + public async respawnPane(paneId: string): Promise { + await this.runTmux(["respawn-pane", "-k", "-t", paneId]); + } + + public async breakPane(paneId: string): Promise { + await this.runTmux(["break-pane", "-t", paneId]); + } + + public async swapWindow(session: string, srcIndex: number, dstIndex: number): Promise { + await this.runTmux(["swap-window", "-s", `${session}:${srcIndex}`, "-t", `${session}:${dstIndex}`]); + } } diff --git a/src/backend/tmux/types.ts b/src/backend/tmux/types.ts index f6224c5..04cf7ce 100644 --- a/src/backend/tmux/types.ts +++ b/src/backend/tmux/types.ts @@ -21,6 +21,11 @@ export interface TmuxGateway { selectPane(paneId: string): Promise; zoomPane(paneId: string): Promise; capturePane(paneId: string, lines: number): Promise; + renameSession(oldName: string, newName: string): Promise; + renameWindow(session: string, windowIndex: number, newName: string): Promise; + respawnPane(paneId: string): Promise; + breakPane(paneId: string): Promise; + swapWindow(session: string, srcIndex: number, dstIndex: number): Promise; } export const buildSnapshot = async ( diff --git a/src/backend/types/protocol.ts b/src/backend/types/protocol.ts index 93a599d..84be17c 100644 --- a/src/backend/types/protocol.ts +++ b/src/backend/types/protocol.ts @@ -10,7 +10,13 @@ export type ControlClientMessage = | { type: "kill_pane"; paneId: string } | { type: "zoom_pane"; paneId: string } | { type: "capture_scrollback"; paneId: string; lines?: number } - | { type: "send_compose"; text: string }; + | { type: "send_compose"; text: string } + | { type: "rename_session"; session: string; newName: string } + | { type: "rename_window"; session: string; windowIndex: number; newName: string } + | { type: "kill_session"; session: string } + | { type: "respawn_pane"; paneId: string } + | { type: "break_pane"; paneId: string } + | { type: "swap_window"; session: string; srcIndex: number; dstIndex: number }; export interface TmuxSessionSummary { name: string; diff --git a/src/frontend/App.tsx b/src/frontend/App.tsx index 52bf5a7..ef0c23d 100644 --- a/src/frontend/App.tsx +++ b/src/frontend/App.tsx @@ -62,6 +62,9 @@ export const App = () => { const [composeEnabled, setComposeEnabled] = useState(true); const [composeText, setComposeText] = useState(""); + const [openMenu, setOpenMenu] = useState<{ kind: "session" | "window" | "pane"; id: string } | null>(null); + const [confirmAction, setConfirmAction] = useState<{ label: string; onConfirm: () => void } | null>(null); + const [scrollbackVisible, setScrollbackVisible] = useState(false); const [scrollbackText, setScrollbackText] = useState(""); const [scrollbackLines, setScrollbackLines] = useState(1000); @@ -610,7 +613,7 @@ export const App = () => { {drawerOpen && (
setDrawerOpen(false)} + onClick={() => { setDrawerOpen(false); setOpenMenu(null); }} data-testid="drawer-backdrop" >