Skip to content

Add kebab menus for session/window/pane actions in sidebar#27

Open
guysmoilov wants to merge 3 commits into
mainfrom
claude/implement-kebab-menu-8ev2g
Open

Add kebab menus for session/window/pane actions in sidebar#27
guysmoilov wants to merge 3 commits into
mainfrom
claude/implement-kebab-menu-8ev2g

Conversation

@guysmoilov

@guysmoilov guysmoilov commented Feb 14, 2026

Copy link
Copy Markdown
Member

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

flowchart LR
  A["Session/Window/Pane Row"] -->|Click Kebab Button| B["Context Menu"]
  B -->|Rename/New/Move| C["Send Control Message"]
  B -->|Kill/Respawn| D["Show Confirmation Dialog"]
  D -->|Confirm| C
  C -->|Backend Handler| E["TmuxCliExecutor"]
  E -->|Execute tmux Command| F["Update State"]
Loading

File Walkthrough

Relevant files
Enhancement
server.ts
Add backend handlers for kebab menu operations                     

src/backend/server.ts

  • Added 6 new message handlers for kebab menu actions: rename_session,
    rename_window, kill_session, respawn_pane, break_pane, swap_window
  • Each handler delegates to corresponding deps.tmux methods
  • kill_session handler calls ensureAttachedSession to maintain valid
    session state
+20/-0   
cli-executor.ts
Implement tmux CLI operations for kebab menu actions         

src/backend/tmux/cli-executor.ts

  • Implemented 5 new public methods on TmuxCliExecutor class
  • renameSession: renames session using tmux rename-session command
  • renameWindow: renames window at specific index using rename-window
  • respawnPane: respawns pane with -k flag using respawn-pane
  • breakPane: breaks pane to new window using break-pane
  • swapWindow: swaps window order using swap-window command
+20/-0   
types.ts
Extend TmuxGateway interface with new operations                 

src/backend/tmux/types.ts

  • Extended TmuxGateway interface with 5 new method signatures
  • Added methods: renameSession, renameWindow, respawnPane, breakPane,
    swapWindow
  • All methods follow existing async/Promise pattern
+5/-0     
protocol.ts
Add new control message types for kebab menu operations   

src/backend/types/protocol.ts

  • Extended ControlClientMessage union type with 6 new message variants
  • Added: rename_session, rename_window, kill_session, respawn_pane,
    break_pane, swap_window
  • Each message includes necessary parameters (session, windowIndex,
    paneId, newName, indices)
+7/-1     
app.css
Add styles for kebab menus and confirmation dialogs           

src/frontend/styles/app.css

  • Replaced .drawer-grid with new .drawer-item layout using flexbox for
    row-based menu structure
  • Added .kebab-btn styling for menu toggle buttons (2.2rem square,
    centered icon)
  • Added .kebab-dropdown styling for dropdown menus with grid layout and
    gap
  • Added .confirm-card and .confirm-actions styling for confirmation
    dialogs
  • Styled destructive buttons with danger color scheme
+54/-3   
App.tsx
Implement kebab menu UI and confirmation dialogs in drawer

src/frontend/App.tsx

  • Added state management for openMenu (tracks which menu is open) and
    confirmAction (tracks pending confirmations)
  • Refactored session list to include kebab menu button and dropdown with
    Rename, New Window, Kill Session actions
  • Refactored window list to include kebab menu with Rename, Split
    Horizontal/Vertical, Move Up/Down, Kill Window actions
  • Refactored pane list to include kebab menu with Zoom, Split
    Horizontal/Vertical, Break to Window, Respawn Pane, Kill Pane actions
  • Removed global action buttons at bottom of drawer (Split H/V, Zoom
    Pane, Close Pane, Kill Window)
  • Added confirmation dialog overlay for destructive actions with
    Cancel/Confirm buttons
  • Updated drawer backdrop click handler to close both drawer and menu
+198/-89

Implementation Details

Backend Architecture:

  • Integrated new tmux operations into the WebSocket control flow via runControlMutation handler, each awaiting execution and returning immediately after completion
  • Kill session operation includes automatic reattachment by invoking ensureAttachedSession() to maintain a connected session after termination
  • All new backend methods follow Promise-based async patterns consistent with existing control flow

Frontend UI/UX:

  • Replaced global action buttons with per-entity kebab menus using a flex-based drawer-item layout: each row contains a clickable label (flex: 1) and a fixed-width kebab button (⋮)
  • Menu state management: openMenu tracks { kind, id } where kind is "session", "window", or "pane", and id uniquely identifies the entity (e.g., "session:name", "window:session:index")
  • Each menu item closes the menu upon action and clears openMenu state, providing atomic UI state transitions
  • Session/window renaming uses browser window.prompt() for inline text input, with client-side validation against unchanged names before sending control message
  • Confirmation overlay (confirmAction) stores a destructive action with a label and onConfirm callback, rendered conditionally with Cancel/Confirm buttons; Confirm invokes the callback and closes the overlay

Protocol Extension:

  • Added six new ControlClientMessage variants with scoped payloads: rename_session, rename_window, kill_session, respawn_pane, break_pane, swap_window
  • Payloads include session identifiers, window indices, pane IDs, and new names as needed per operation

Styling:

  • Drawer items use a gap-separated flex layout with the kebab button sized at 2.2rem for adequate touch target area (addressing mobile usability)
  • Kebab dropdown menus use a compact grid with 0.2rem gaps and reduced font size (0.85rem) for contextual actions
  • Destructive actions (kill, respawn) are colored with danger color in both dropdown and confirmation card
  • Confirmation card has a max width constraint (22rem) and right-aligned destructive button in confirm-actions

Mobile Considerations:

  • Kebab buttons have minimum height of 2.2rem to meet touch target standards
  • Menu backdrop click handler now also closes open menus by resetting openMenu state
  • Drawer remains open during menu interaction, only closing when backdrop is tapped or menu action completes

@coderabbitai

coderabbitai Bot commented Feb 14, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This 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

Cohort / File(s) Summary
Backend Control Flow
src/backend/server.ts
Added WebSocket control message handlers for rename_session, rename_window, kill_session, respawn_pane, break_pane, and swap_window. The kill_session handler includes ensureAttachedSession to reattach after killing.
Tmux CLI Methods
src/backend/tmux/cli-executor.ts
Exposed five new public methods on TmuxCliExecutor: renameSession, renameWindow, respawnPane, breakPane, and swapWindow, each wrapping the corresponding tmux CLI command.
Tmux Gateway Interface
src/backend/tmux/types.ts
Extended TmuxGateway interface with the same five new method signatures to support the new CLI executor implementations.
Protocol Types
src/backend/types/protocol.ts
Expanded ControlClientMessage union with six new message variants: rename_session, rename_window, kill_session, respawn_pane, break_pane, and swap_window, each with required payload fields.
Frontend UI Logic
src/frontend/App.tsx
Introduced openMenu and confirmAction state to manage per-item kebab menus and destructive action confirmations. Replaced flat item listings with drawer items featuring kebab buttons. Added menu identifiers and visibility logic for sessions, windows, and panes. Integrated global confirmation overlay for destructive operations.
Frontend Styling
src/frontend/styles/app.css
Replaced .drawer-grid with .drawer-item flex layout. Added styles for .kebab-btn, .kebab-dropdown, .confirm-card, and .confirm-actions. Updated spacing, alignment, and button styling to support the new menu-driven UI.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (6 files):

⚔️ src/backend/server.ts (content)
⚔️ src/backend/tmux/cli-executor.ts (content)
⚔️ src/backend/tmux/types.ts (content)
⚔️ src/backend/types/protocol.ts (content)
⚔️ src/frontend/App.tsx (content)
⚔️ src/frontend/styles/app.css (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed The PR implements all required backend operations (rename session/window, kill session, respawn pane, break pane, swap window) and frontend kebab menus with confirmation for destructive actions, directly addressing linked issue #20 requirements.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing kebab menus and associated tmux operations as specified in issue #20, with no unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/implement-kebab-menu-8ev2g

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-code-review

qodo-code-review Bot commented Feb 14, 2026

Copy link
Copy Markdown

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Command injection risk

Description: User-controlled strings like newName are passed into tmux CLI arguments (e.g.,
rename-session, rename-window) and could enable command injection if runTmux ultimately
executes through a shell or performs unsafe argument joining.
cli-executor.ts [154-172]

Referred Code
public async renameSession(oldName: string, newName: string): Promise<void> {
  await this.runTmux(["rename-session", "-t", oldName, newName]);
}

public async renameWindow(session: string, windowIndex: number, newName: string): Promise<void> {
  await this.runTmux(["rename-window", "-t", `${session}:${windowIndex}`, newName]);
}

public async respawnPane(paneId: string): Promise<void> {
  await this.runTmux(["respawn-pane", "-k", "-t", paneId]);
}

public async breakPane(paneId: string): Promise<void> {
  await this.runTmux(["break-pane", "-t", paneId]);
}

public async swapWindow(session: string, srcIndex: number, dstIndex: number): Promise<void> {
  await this.runTmux(["swap-window", "-s", `${session}:${srcIndex}`, "-t", `${session}:${dstIndex}`]);
}
Ticket Compliance
🟡
🎫 #20
🟢 Add kebab menus in the sidebar for sessions, windows, and panes to expose context-specific
tmux operations.
Session menu supports: rename, new window, kill session.
Window menu supports: rename, split horizontal, split vertical, swap/move window, kill
window.
Pane menu supports: respawn pane, break pane to window, kill pane.
Each sidebar entity (session/window/pane) has a kebab menu.
Actions are scoped correctly and invoke existing backend control messages.
Destructive actions require confirmation.
🔴 Pane menu supports: send keys.
Mobile usability is verified (tap targets, drawer behavior, no accidental triggers).
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Missing audit logs: New critical control actions (e.g., kill_session, respawn_pane, break_pane, swap_window,
renames) are executed without any audit logging of actor/context/outcome, preventing
reconstruction of events.

Referred Code
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;

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Unhandled edge case: The window menu can send split_pane with an empty paneId (windowState.panes[0]?.id ??
"") and there is no validation or user-visible error handling for failed control
operations.

Referred Code
<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>

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Unvalidated tmux args: User-controlled names/indices from client messages (e.g., newName, oldName) are passed
directly as tmux CLI arguments without validation/guarding against empty/invalid values or
option-like inputs (e.g., names starting with -), creating risk of unintended tmux
behavior.

Referred Code
public async renameSession(oldName: string, newName: string): Promise<void> {
  await this.runTmux(["rename-session", "-t", oldName, newName]);
}

public async renameWindow(session: string, windowIndex: number, newName: string): Promise<void> {
  await this.runTmux(["rename-window", "-t", `${session}:${windowIndex}`, newName]);
}

public async respawnPane(paneId: string): Promise<void> {
  await this.runTmux(["respawn-pane", "-k", "-t", paneId]);
}

public async breakPane(paneId: string): Promise<void> {
  await this.runTmux(["break-pane", "-t", paneId]);
}

public async swapWindow(session: string, srcIndex: number, dstIndex: number): Promise<void> {
  await this.runTmux(["swap-window", "-s", `${session}:${srcIndex}`, "-t", `${session}:${dstIndex}`]);
}

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status:
Error surfacing unknown: New handlers invoke tmux operations without visible error translation/sanitization, so it
is unclear whether internal error details could be propagated to clients.

Referred Code
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;

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review

qodo-code-review Bot commented Feb 14, 2026

Copy link
Copy Markdown

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Attach to renamed session

After renaming a session, re-attach the runtime to the new session name and
notify the client to maintain state synchronization.

src/backend/server.ts [215-217]

 case "rename_session":
   await deps.tmux.renameSession(message.session, message.newName);
+  runtime.attachToSession(message.newName);
+  sendJson(socket, { type: "attached", session: message.newName });
   return;
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: This suggestion fixes a critical bug where renaming a session would cause the client and server to lose sync, as the client would still be attached to the old session name.

Medium
Fix incorrect window reordering logic

Fix the "Move Down" window logic to correctly handle non-contiguous window
indices. Sort windows by index before finding the next one to ensure it swaps
with the adjacent window.

src/frontend/App.tsx [737-745]

 {windowState.index < allWindows[allWindows.length - 1].index && (
   <button onClick={() => {
     setOpenMenu(null);
-    const nextWindow = allWindows.find((w) => w.index > windowState.index);
-    if (nextWindow) {
+    const sortedWindows = [...allWindows].sort((a, b) => a.index - b.index);
+    const currentIndexInSorted = sortedWindows.findIndex(w => w.index === windowState.index);
+    if (currentIndexInSorted !== -1 && currentIndexInSorted < sortedWindows.length - 1) {
+      const nextWindow = sortedWindows[currentIndexInSorted + 1];
       sendControl({ type: "swap_window", session: activeSession.name, srcIndex: windowState.index, dstIndex: nextWindow.index });
     }
   }}>Move Down</button>
 )}
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: This suggestion correctly identifies a logic flaw in the "Move Down" functionality where it might not select the immediate next window if indices are not contiguous, and provides a robust fix.

Medium
Use active pane id for split

Modify the "Split Horizontal" action to target the active pane within the
window, not just the first one.

src/frontend/App.tsx [723-726]

 <button onClick={() => {
   setOpenMenu(null);
-  sendControl({ type: "split_pane", paneId: windowState.panes[0]?.id ?? "", orientation: "h" });
+  const targetPane = windowState.panes.find(p => p.active)?.id;
+  if (targetPane) {
+    sendControl({ type: "split_pane", paneId: targetPane, orientation: "h" });
+  }
 }}>Split Horizontal</button>
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: This suggestion correctly identifies that splitting should occur on the active pane, not always the first one, which is a logic error that would lead to unexpected behavior for the user.

Medium
General
Add overlay backdrop style

Add CSS styling for the .overlay class to create a functional backdrop for the
confirmation dialog.

src/frontend/styles/app.css [647-649]

 .confirm-card {
   width: min(85vw, 22rem);
 }
 
+.overlay {
+  position: fixed;
+  inset: 0;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+}
+
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: This suggestion correctly identifies that the .overlay class is unstyled, which would break the confirmation dialog's appearance, and provides the necessary CSS to make it functional.

Low
Replace window.prompt with a custom component

Replace the use of window.prompt() for renaming with a custom, non-blocking
input dialog component. This will improve user experience and maintain UI
consistency.

src/frontend/App.tsx [653-659]

 <button onClick={() => {
   setOpenMenu(null);
+  // Example of how this could be implemented with a custom prompt/input dialog state
+  // setInputAction({
+  //   label: "Rename session",
+  //   initialValue: session.name,
+  //   onConfirm: (newName) => {
+  //     if (newName && newName !== session.name) {
+  //       sendControl({ type: "rename_session", session: session.name, newName });
+  //     }
+  //   }
+  // });
   const newName = window.prompt("Rename session", session.name);
   if (newName && newName !== session.name) {
     sendControl({ type: "rename_session", session: session.name, newName });
   }
 }}>Rename</button>
  • Apply / Chat
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly points out that using window.prompt() is poor UX and inconsistent with the app's UI, proposing a better, custom implementation that would improve quality and consistency.

Low
  • Update

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟡 Minor

Closing 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

📥 Commits

Reviewing files that changed from the base of the PR and between dd8dca5 and 1abcec3.

📒 Files selected for processing (6)
  • src/backend/server.ts
  • src/backend/tmux/cli-executor.ts
  • src/backend/tmux/types.ts
  • src/backend/types/protocol.ts
  • src/frontend/App.tsx
  • src/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.

Comment on lines +166 to +167
public async breakPane(paneId: string): Promise<void> {
await this.runTmux(["break-pane", "-t", paneId]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

tmux break-pane -s vs -t flag source pane documentation

💡 Result:

In tmux break-pane:

  • -s src-pane specifies the source pane to break out (the pane you are moving into its own window). If omitted, tmux uses the current pane. The src-pane is addressed like session:window.pane (for example: break-pane -s Advanced:1.2). [1][2]
  • -t dst-window specifies 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.

Suggested change
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]);
}

Comment thread src/frontend/App.tsx
Comment on lines +631 to 676
{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>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
{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>

Comment thread src/frontend/App.tsx
Comment on lines +688 to +757
? 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>
);
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
? 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>
);
})

@guysmoilov guysmoilov force-pushed the claude/implement-kebab-menu-8ev2g branch from 1abcec3 to 1ee6908 Compare February 14, 2026 21:47
@github-actions

github-actions Bot commented Feb 15, 2026

Copy link
Copy Markdown
Contributor

UI Screenshots

Auto-captured from commit b5df326

Screenshots are available as a build artifact.

Files captured: drawer-open.png, main-view.png

Download the ui-screenshots artifact from the Actions run to view full-resolution images.

claude and others added 3 commits February 18, 2026 00:29
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
@guysmoilov guysmoilov force-pushed the claude/implement-kebab-menu-8ev2g branch from 77558e4 to 9ec9584 Compare February 17, 2026 22:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add sidebar kebab menus for session/window/pane actions

2 participants