Add kebab menus for session/window/pane actions in sidebar#27
Add kebab menus for session/window/pane actions in sidebar#27guysmoilov wants to merge 3 commits into
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis PR adds kebab menu UI controls to the frontend sidebar for sessions, windows, and panes, with backend handlers for renaming, respawning, breaking, swapping, and killing these entities via new tmux CLI methods and WebSocket control messages. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
PR Compliance Guide 🔍Below is a summary of compliance checks for this PR:
Compliance status legend🟢 - Fully Compliant🟡 - Partial Compliant 🔴 - Not Compliant ⚪ - Requires Further Human Verification 🏷️ - Compliance label |
|||||||||||||||||||||||||||||||||||||
PR Code Suggestions ✨Explore these optional code suggestions:
|
||||||||||||||||||
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/frontend/App.tsx (1)
613-623:⚠️ Potential issue | 🟡 MinorClosing the drawer should also clear any open kebab menu.
Backdrop clicks clear
openMenu, but the close button doesn’t—so reopening the drawer can show a stale menu. Mirror the same reset there.🧹 Suggested fix
- <button - className="drawer-close" - onClick={() => setDrawerOpen(false)} + <button + className="drawer-close" + onClick={() => { + setDrawerOpen(false); + setOpenMenu(null); + }}
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
src/backend/server.tssrc/backend/tmux/cli-executor.tssrc/backend/tmux/types.tssrc/backend/types/protocol.tssrc/frontend/App.tsxsrc/frontend/styles/app.css
🔇 Additional comments (7)
src/backend/server.ts (1)
215-234: Control-mutation handlers look consistent with the existing flow.The new cases mirror the established pattern and the post-kill reattachment is handled.
src/frontend/styles/app.css (1)
608-664: Kebab/confirm styling is consistent and cohesive.Spacing, alignment, and destructive styling fit the existing theme system.
src/backend/types/protocol.ts (1)
13-19: Protocol updates align with new control messages.src/backend/tmux/types.ts (1)
24-28: Gateway surface matches the new control operations.src/frontend/App.tsx (3)
65-67: State additions for menus/confirmations are clean and localized.
774-833: Pane-level menu actions look solid.
852-870: Confirmation overlay wiring is clean and minimal.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| public async breakPane(paneId: string): Promise<void> { | ||
| await this.runTmux(["break-pane", "-t", paneId]); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
tmux break-pane -s vs -t flag source pane documentation
💡 Result:
In tmux break-pane:
-s src-panespecifies the source pane to break out (the pane you are moving into its own window). If omitted, tmux uses the current pane. Thesrc-paneis addressed likesession:window.pane(for example:break-pane -s Advanced:1.2). [1][2]-t dst-windowspecifies the destination window (the window that will contain the broken-out pane as its only pane). If omitted, tmux creates a new window (at the next available index) for the pane. [1]
Version note: older tmux versions used -t target-pane (without -s); newer versions split this into -s src-pane and -t dst-window. [2]
Sources:
[1] tmux man page (break-pane ... [-s src-pane] [-t dst-window]) https://linuxcommand.org/lc3_man_pages/tmux1.html
[2] tmux_interface docs quoting tmux manuals across versions https://docs.rs/tmux_interface/latest/tmux_interface/commands/windows_and_panes/break_pane/struct.BreakPane.html
Use -s flag instead of -t to specify the source pane.
In modern tmux break-pane, -s src-pane specifies the source pane to break out, while -t dst-window specifies the destination window (optional; if omitted, a new window is created). The current code passes paneId with -t, which targets the wrong thing. Switch to -s:
public async breakPane(paneId: string): Promise<void> {
- await this.runTmux(["break-pane", "-t", paneId]);
+ await this.runTmux(["break-pane", "-s", paneId]);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public async breakPane(paneId: string): Promise<void> { | |
| await this.runTmux(["break-pane", "-t", paneId]); | |
| public async breakPane(paneId: string): Promise<void> { | |
| await this.runTmux(["break-pane", "-s", paneId]); | |
| } |
| {snapshot.sessions.map((session) => { | ||
| const sessionMenuId = `session:${session.name}`; | ||
| const isMenuOpen = openMenu?.kind === "session" && openMenu.id === sessionMenuId; | ||
| return ( | ||
| <li key={session.name}> | ||
| <div className="drawer-item"> | ||
| <button | ||
| onClick={() => sendControl({ type: "select_session", session: session.name })} | ||
| className={session.name === (attachedSession || activeSession?.name) ? "active" : ""} | ||
| > | ||
| {session.name} {session.attached ? "*" : ""} | ||
| </button> | ||
| <button | ||
| className="kebab-btn" | ||
| onClick={() => setOpenMenu(isMenuOpen ? null : { kind: "session", id: sessionMenuId })} | ||
| aria-label={`Actions for session ${session.name}`} | ||
| > | ||
| ⋮ | ||
| </button> | ||
| </div> | ||
| {isMenuOpen && ( | ||
| <div className="kebab-dropdown"> | ||
| <button onClick={() => { | ||
| setOpenMenu(null); | ||
| const newName = window.prompt("Rename session", session.name); | ||
| if (newName && newName !== session.name) { | ||
| sendControl({ type: "rename_session", session: session.name, newName }); | ||
| } | ||
| }}>Rename</button> | ||
| <button onClick={() => { | ||
| setOpenMenu(null); | ||
| sendControl({ type: "new_window", session: session.name }); | ||
| }}>New Window</button> | ||
| <button className="destructive" onClick={() => { | ||
| setOpenMenu(null); | ||
| setConfirmAction({ | ||
| label: `Kill session "${session.name}"?`, | ||
| onConfirm: () => sendControl({ type: "kill_session", session: session.name }) | ||
| }); | ||
| }}>Kill Session</button> | ||
| </div> | ||
| )} | ||
| </li> | ||
| ); | ||
| })} | ||
| </ul> |
There was a problem hiding this comment.
Renaming the attached session can leave selection highlight stale.
attachedSession isn’t updated after a rename, so the active session indicator can disappear until a reattach. Consider syncing it when renaming the active session.
🔁 Suggested fix
<button onClick={() => {
setOpenMenu(null);
const newName = window.prompt("Rename session", session.name);
if (newName && newName !== session.name) {
sendControl({ type: "rename_session", session: session.name, newName });
+ if (session.name === attachedSession || session.attached) {
+ setAttachedSession(newName);
+ }
}
}}>Rename</button>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {snapshot.sessions.map((session) => { | |
| const sessionMenuId = `session:${session.name}`; | |
| const isMenuOpen = openMenu?.kind === "session" && openMenu.id === sessionMenuId; | |
| return ( | |
| <li key={session.name}> | |
| <div className="drawer-item"> | |
| <button | |
| onClick={() => sendControl({ type: "select_session", session: session.name })} | |
| className={session.name === (attachedSession || activeSession?.name) ? "active" : ""} | |
| > | |
| {session.name} {session.attached ? "*" : ""} | |
| </button> | |
| <button | |
| className="kebab-btn" | |
| onClick={() => setOpenMenu(isMenuOpen ? null : { kind: "session", id: sessionMenuId })} | |
| aria-label={`Actions for session ${session.name}`} | |
| > | |
| ⋮ | |
| </button> | |
| </div> | |
| {isMenuOpen && ( | |
| <div className="kebab-dropdown"> | |
| <button onClick={() => { | |
| setOpenMenu(null); | |
| const newName = window.prompt("Rename session", session.name); | |
| if (newName && newName !== session.name) { | |
| sendControl({ type: "rename_session", session: session.name, newName }); | |
| } | |
| }}>Rename</button> | |
| <button onClick={() => { | |
| setOpenMenu(null); | |
| sendControl({ type: "new_window", session: session.name }); | |
| }}>New Window</button> | |
| <button className="destructive" onClick={() => { | |
| setOpenMenu(null); | |
| setConfirmAction({ | |
| label: `Kill session "${session.name}"?`, | |
| onConfirm: () => sendControl({ type: "kill_session", session: session.name }) | |
| }); | |
| }}>Kill Session</button> | |
| </div> | |
| )} | |
| </li> | |
| ); | |
| })} | |
| </ul> | |
| {snapshot.sessions.map((session) => { | |
| const sessionMenuId = `session:${session.name}`; | |
| const isMenuOpen = openMenu?.kind === "session" && openMenu.id === sessionMenuId; | |
| return ( | |
| <li key={session.name}> | |
| <div className="drawer-item"> | |
| <button | |
| onClick={() => sendControl({ type: "select_session", session: session.name })} | |
| className={session.name === (attachedSession || activeSession?.name) ? "active" : ""} | |
| > | |
| {session.name} {session.attached ? "*" : ""} | |
| </button> | |
| <button | |
| className="kebab-btn" | |
| onClick={() => setOpenMenu(isMenuOpen ? null : { kind: "session", id: sessionMenuId })} | |
| aria-label={`Actions for session ${session.name}`} | |
| > | |
| ⋮ | |
| </button> | |
| </div> | |
| {isMenuOpen && ( | |
| <div className="kebab-dropdown"> | |
| <button onClick={() => { | |
| setOpenMenu(null); | |
| const newName = window.prompt("Rename session", session.name); | |
| if (newName && newName !== session.name) { | |
| sendControl({ type: "rename_session", session: session.name, newName }); | |
| if (session.name === attachedSession || session.attached) { | |
| setAttachedSession(newName); | |
| } | |
| } | |
| }}>Rename</button> | |
| <button onClick={() => { | |
| setOpenMenu(null); | |
| sendControl({ type: "new_window", session: session.name }); | |
| }}>New Window</button> | |
| <button className="destructive" onClick={() => { | |
| setOpenMenu(null); | |
| setConfirmAction({ | |
| label: `Kill session "${session.name}"?`, | |
| onConfirm: () => sendControl({ type: "kill_session", session: session.name }) | |
| }); | |
| }}>Kill Session</button> | |
| </div> | |
| )} | |
| </li> | |
| ); | |
| })} | |
| </ul> |
| ? activeSession.windowStates.map((windowState, _idx, allWindows) => { | ||
| const windowMenuId = `window:${activeSession.name}:${windowState.index}`; | ||
| const isMenuOpen = openMenu?.kind === "window" && openMenu.id === windowMenuId; | ||
| return ( | ||
| <li key={`${activeSession.name}-${windowState.index}`}> | ||
| <div className="drawer-item"> | ||
| <button | ||
| onClick={() => | ||
| sendControl({ | ||
| type: "select_window", | ||
| session: activeSession.name, | ||
| windowIndex: windowState.index | ||
| }) | ||
| } | ||
| className={windowState.active ? "active" : ""} | ||
| > | ||
| {windowState.index}: {windowState.name} {windowState.active ? "*" : ""} | ||
| </button> | ||
| <button | ||
| className="kebab-btn" | ||
| onClick={() => setOpenMenu(isMenuOpen ? null : { kind: "window", id: windowMenuId })} | ||
| aria-label={`Actions for window ${windowState.index}`} | ||
| > | ||
| ⋮ | ||
| </button> | ||
| </div> | ||
| {isMenuOpen && ( | ||
| <div className="kebab-dropdown"> | ||
| <button onClick={() => { | ||
| setOpenMenu(null); | ||
| const newName = window.prompt("Rename window", windowState.name); | ||
| if (newName && newName !== windowState.name) { | ||
| sendControl({ type: "rename_window", session: activeSession.name, windowIndex: windowState.index, newName }); | ||
| } | ||
| }}>Rename</button> | ||
| <button onClick={() => { | ||
| setOpenMenu(null); | ||
| sendControl({ type: "split_pane", paneId: windowState.panes[0]?.id ?? "", orientation: "h" }); | ||
| }}>Split Horizontal</button> | ||
| <button onClick={() => { | ||
| setOpenMenu(null); | ||
| sendControl({ type: "split_pane", paneId: windowState.panes[0]?.id ?? "", orientation: "v" }); | ||
| }}>Split Vertical</button> | ||
| {windowState.index > 0 && ( | ||
| <button onClick={() => { | ||
| setOpenMenu(null); | ||
| sendControl({ type: "swap_window", session: activeSession.name, srcIndex: windowState.index, dstIndex: windowState.index - 1 }); | ||
| }}>Move Up</button> | ||
| )} | ||
| {windowState.index < allWindows[allWindows.length - 1].index && ( | ||
| <button onClick={() => { | ||
| setOpenMenu(null); | ||
| const nextWindow = allWindows.find((w) => w.index > windowState.index); | ||
| if (nextWindow) { | ||
| sendControl({ type: "swap_window", session: activeSession.name, srcIndex: windowState.index, dstIndex: nextWindow.index }); | ||
| } | ||
| }}>Move Down</button> | ||
| )} | ||
| <button className="destructive" onClick={() => { | ||
| setOpenMenu(null); | ||
| setConfirmAction({ | ||
| label: `Kill window ${windowState.index}: ${windowState.name}?`, | ||
| onConfirm: () => sendControl({ type: "kill_window", session: activeSession.name, windowIndex: windowState.index }) | ||
| }); | ||
| }}>Kill Window</button> | ||
| </div> | ||
| )} | ||
| </li> | ||
| ); | ||
| }) |
There was a problem hiding this comment.
Guard split actions against missing panes and prefer the active pane.
Using windowState.panes[0]?.id ?? "" can send an empty pane id and ignores the window’s active pane. Safer to derive a target pane and no-op if missing.
🛡️ Suggested fix
- ? activeSession.windowStates.map((windowState, _idx, allWindows) => {
+ ? activeSession.windowStates.map((windowState, _idx, allWindows) => {
+ const targetPaneId =
+ windowState.panes.find((pane) => pane.active)?.id ?? windowState.panes[0]?.id ?? "";
const windowMenuId = `window:${activeSession.name}:${windowState.index}`;
const isMenuOpen = openMenu?.kind === "window" && openMenu.id === windowMenuId;
return (
<li key={`${activeSession.name}-${windowState.index}`}>
<div className="drawer-item">
@@
<button onClick={() => {
setOpenMenu(null);
- sendControl({ type: "split_pane", paneId: windowState.panes[0]?.id ?? "", orientation: "h" });
+ if (targetPaneId) {
+ sendControl({ type: "split_pane", paneId: targetPaneId, orientation: "h" });
+ }
}}>Split Horizontal</button>
<button onClick={() => {
setOpenMenu(null);
- sendControl({ type: "split_pane", paneId: windowState.panes[0]?.id ?? "", orientation: "v" });
+ if (targetPaneId) {
+ sendControl({ type: "split_pane", paneId: targetPaneId, orientation: "v" });
+ }
}}>Split Vertical</button>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ? activeSession.windowStates.map((windowState, _idx, allWindows) => { | |
| const windowMenuId = `window:${activeSession.name}:${windowState.index}`; | |
| const isMenuOpen = openMenu?.kind === "window" && openMenu.id === windowMenuId; | |
| return ( | |
| <li key={`${activeSession.name}-${windowState.index}`}> | |
| <div className="drawer-item"> | |
| <button | |
| onClick={() => | |
| sendControl({ | |
| type: "select_window", | |
| session: activeSession.name, | |
| windowIndex: windowState.index | |
| }) | |
| } | |
| className={windowState.active ? "active" : ""} | |
| > | |
| {windowState.index}: {windowState.name} {windowState.active ? "*" : ""} | |
| </button> | |
| <button | |
| className="kebab-btn" | |
| onClick={() => setOpenMenu(isMenuOpen ? null : { kind: "window", id: windowMenuId })} | |
| aria-label={`Actions for window ${windowState.index}`} | |
| > | |
| ⋮ | |
| </button> | |
| </div> | |
| {isMenuOpen && ( | |
| <div className="kebab-dropdown"> | |
| <button onClick={() => { | |
| setOpenMenu(null); | |
| const newName = window.prompt("Rename window", windowState.name); | |
| if (newName && newName !== windowState.name) { | |
| sendControl({ type: "rename_window", session: activeSession.name, windowIndex: windowState.index, newName }); | |
| } | |
| }}>Rename</button> | |
| <button onClick={() => { | |
| setOpenMenu(null); | |
| sendControl({ type: "split_pane", paneId: windowState.panes[0]?.id ?? "", orientation: "h" }); | |
| }}>Split Horizontal</button> | |
| <button onClick={() => { | |
| setOpenMenu(null); | |
| sendControl({ type: "split_pane", paneId: windowState.panes[0]?.id ?? "", orientation: "v" }); | |
| }}>Split Vertical</button> | |
| {windowState.index > 0 && ( | |
| <button onClick={() => { | |
| setOpenMenu(null); | |
| sendControl({ type: "swap_window", session: activeSession.name, srcIndex: windowState.index, dstIndex: windowState.index - 1 }); | |
| }}>Move Up</button> | |
| )} | |
| {windowState.index < allWindows[allWindows.length - 1].index && ( | |
| <button onClick={() => { | |
| setOpenMenu(null); | |
| const nextWindow = allWindows.find((w) => w.index > windowState.index); | |
| if (nextWindow) { | |
| sendControl({ type: "swap_window", session: activeSession.name, srcIndex: windowState.index, dstIndex: nextWindow.index }); | |
| } | |
| }}>Move Down</button> | |
| )} | |
| <button className="destructive" onClick={() => { | |
| setOpenMenu(null); | |
| setConfirmAction({ | |
| label: `Kill window ${windowState.index}: ${windowState.name}?`, | |
| onConfirm: () => sendControl({ type: "kill_window", session: activeSession.name, windowIndex: windowState.index }) | |
| }); | |
| }}>Kill Window</button> | |
| </div> | |
| )} | |
| </li> | |
| ); | |
| }) | |
| ? activeSession.windowStates.map((windowState, _idx, allWindows) => { | |
| const targetPaneId = | |
| windowState.panes.find((pane) => pane.active)?.id ?? windowState.panes[0]?.id ?? ""; | |
| const windowMenuId = `window:${activeSession.name}:${windowState.index}`; | |
| const isMenuOpen = openMenu?.kind === "window" && openMenu.id === windowMenuId; | |
| return ( | |
| <li key={`${activeSession.name}-${windowState.index}`}> | |
| <div className="drawer-item"> | |
| <button | |
| onClick={() => | |
| sendControl({ | |
| type: "select_window", | |
| session: activeSession.name, | |
| windowIndex: windowState.index | |
| }) | |
| } | |
| className={windowState.active ? "active" : ""} | |
| > | |
| {windowState.index}: {windowState.name} {windowState.active ? "*" : ""} | |
| </button> | |
| <button | |
| className="kebab-btn" | |
| onClick={() => setOpenMenu(isMenuOpen ? null : { kind: "window", id: windowMenuId })} | |
| aria-label={`Actions for window ${windowState.index}`} | |
| > | |
| ⋮ | |
| </button> | |
| </div> | |
| {isMenuOpen && ( | |
| <div className="kebab-dropdown"> | |
| <button onClick={() => { | |
| setOpenMenu(null); | |
| const newName = window.prompt("Rename window", windowState.name); | |
| if (newName && newName !== windowState.name) { | |
| sendControl({ type: "rename_window", session: activeSession.name, windowIndex: windowState.index, newName }); | |
| } | |
| }}>Rename</button> | |
| <button onClick={() => { | |
| setOpenMenu(null); | |
| if (targetPaneId) { | |
| sendControl({ type: "split_pane", paneId: targetPaneId, orientation: "h" }); | |
| } | |
| }}>Split Horizontal</button> | |
| <button onClick={() => { | |
| setOpenMenu(null); | |
| if (targetPaneId) { | |
| sendControl({ type: "split_pane", paneId: targetPaneId, orientation: "v" }); | |
| } | |
| }}>Split Vertical</button> | |
| {windowState.index > 0 && ( | |
| <button onClick={() => { | |
| setOpenMenu(null); | |
| sendControl({ type: "swap_window", session: activeSession.name, srcIndex: windowState.index, dstIndex: windowState.index - 1 }); | |
| }}>Move Up</button> | |
| )} | |
| {windowState.index < allWindows[allWindows.length - 1].index && ( | |
| <button onClick={() => { | |
| setOpenMenu(null); | |
| const nextWindow = allWindows.find((w) => w.index > windowState.index); | |
| if (nextWindow) { | |
| sendControl({ type: "swap_window", session: activeSession.name, srcIndex: windowState.index, dstIndex: nextWindow.index }); | |
| } | |
| }}>Move Down</button> | |
| )} | |
| <button className="destructive" onClick={() => { | |
| setOpenMenu(null); | |
| setConfirmAction({ | |
| label: `Kill window ${windowState.index}: ${windowState.name}?`, | |
| onConfirm: () => sendControl({ type: "kill_window", session: activeSession.name, windowIndex: windowState.index }) | |
| }); | |
| }}>Kill Window</button> | |
| </div> | |
| )} | |
| </li> | |
| ); | |
| }) |
1abcec3 to
1ee6908
Compare
UI Screenshots
Screenshots are available as a build artifact. Files captured: drawer-open.png, main-view.png Download the |
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
77558e4 to
9ec9584
Compare
User description
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
PR Type
Enhancement
Description
Replace flat action buttons with context-specific kebab menus on each session, window, and pane row
Add new backend operations: rename session/window, kill session, respawn/break pane, swap window order
Implement confirmation dialogs for destructive actions (kill, respawn)
Refactor drawer UI to display per-entity menus instead of global action buttons
Diagram Walkthrough
File Walkthrough
server.ts
Add backend handlers for kebab menu operationssrc/backend/server.ts
rename_session,rename_window,kill_session,respawn_pane,break_pane,swap_windowdeps.tmuxmethodskill_sessionhandler callsensureAttachedSessionto maintain validsession state
cli-executor.ts
Implement tmux CLI operations for kebab menu actionssrc/backend/tmux/cli-executor.ts
TmuxCliExecutorclassrenameSession: renames session using tmuxrename-sessioncommandrenameWindow: renames window at specific index usingrename-windowrespawnPane: respawns pane with-kflag usingrespawn-panebreakPane: breaks pane to new window usingbreak-paneswapWindow: swaps window order usingswap-windowcommandtypes.ts
Extend TmuxGateway interface with new operationssrc/backend/tmux/types.ts
TmuxGatewayinterface with 5 new method signaturesrenameSession,renameWindow,respawnPane,breakPane,swapWindowprotocol.ts
Add new control message types for kebab menu operationssrc/backend/types/protocol.ts
ControlClientMessageunion type with 6 new message variantsrename_session,rename_window,kill_session,respawn_pane,break_pane,swap_windowpaneId, newName, indices)
app.css
Add styles for kebab menus and confirmation dialogssrc/frontend/styles/app.css
.drawer-gridwith new.drawer-itemlayout using flexbox forrow-based menu structure
.kebab-btnstyling for menu toggle buttons (2.2rem square,centered icon)
.kebab-dropdownstyling for dropdown menus with grid layout andgap
.confirm-cardand.confirm-actionsstyling for confirmationdialogs
App.tsx
Implement kebab menu UI and confirmation dialogs in drawersrc/frontend/App.tsx
openMenu(tracks which menu is open) andconfirmAction(tracks pending confirmations)Rename, New Window, Kill Session actions
Horizontal/Vertical, Move Up/Down, Kill Window actions
Horizontal/Vertical, Break to Window, Respawn Pane, Kill Pane actions
Pane, Close Pane, Kill Window)
Cancel/Confirm buttons
Implementation Details
Backend Architecture:
runControlMutationhandler, each awaiting execution and returning immediately after completionensureAttachedSession()to maintain a connected session after terminationFrontend UI/UX:
drawer-itemlayout: each row contains a clickable label (flex: 1) and a fixed-width kebab button (⋮)openMenutracks{ kind, id }where kind is "session", "window", or "pane", and id uniquely identifies the entity (e.g., "session:name", "window:session:index")window.prompt()for inline text input, with client-side validation against unchanged names before sending control messageconfirmAction) stores a destructive action with a label andonConfirmcallback, rendered conditionally with Cancel/Confirm buttons; Confirm invokes the callback and closes the overlayProtocol Extension:
ControlClientMessagevariants with scoped payloads: rename_session, rename_window, kill_session, respawn_pane, break_pane, swap_windowStyling:
Mobile Considerations: