-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat: auto-attach tmux sessions and switch them from remote block context menu #3484
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
zwcf5200
wants to merge
4
commits into
wavetermdev:main
Choose a base branch
from
zwcf5200:feat/tmux-auto-attach
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
8d89add
feat: auto-attach tmux sessions on remote blocks via term:tmux:session
zwcf5200 4981ce7
feat: list and switch tmux sessions from remote block context menu
zwcf5200 3d259ee
fix: address tmux feature review feedback
zwcf5200 8a3115c
fix: preserve tmux session names with leading/trailing whitespace
zwcf5200 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| // Copyright 2026, Command Line Inc. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| import { describe, expect, it, vi } from "vitest"; | ||
|
|
||
| import { applyTmuxSessionChange, toggleTmuxSession } from "./tmux-session"; | ||
|
|
||
| describe("toggleTmuxSession", () => { | ||
| it("associates an unchecked session", () => { | ||
| expect(toggleTmuxSession("", "mactop-3")).toBe("mactop-3"); | ||
| expect(toggleTmuxSession("workspace-8", "mactop-3")).toBe("mactop-3"); | ||
| }); | ||
|
|
||
| it("cancels the association when the checked session is selected again", () => { | ||
| expect(toggleTmuxSession("mactop-3", "mactop-3")).toBe(""); | ||
| }); | ||
| }); | ||
|
|
||
| describe("applyTmuxSessionChange", () => { | ||
| it("does nothing when the selected session is already active", async () => { | ||
| const persistSession = vi.fn(async () => {}); | ||
| const restartController = vi.fn(async () => {}); | ||
|
|
||
| const changed = await applyTmuxSessionChange("mactop-3", "mactop-3", persistSession, restartController); | ||
|
|
||
| expect(changed).toBe(false); | ||
| expect(persistSession).not.toHaveBeenCalled(); | ||
| expect(restartController).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it.each([ | ||
| ["cancel", "mactop-3", ""], | ||
| ["switch", "mactop-3", "workspace-8"], | ||
| ["associate", "", "mactop-3"], | ||
| ])("persists and restarts when attempting to %s", async (_operation, currentSession, nextSession) => { | ||
| const calls: string[] = []; | ||
| const persistSession = vi.fn(async (session: string) => { | ||
| calls.push(`persist:${session}`); | ||
| }); | ||
| const restartController = vi.fn(async () => { | ||
| calls.push("restart"); | ||
| }); | ||
|
|
||
| const changed = await applyTmuxSessionChange(currentSession, nextSession, persistSession, restartController); | ||
|
|
||
| expect(changed).toBe(true); | ||
| expect(calls).toEqual([`persist:${nextSession}`, "restart"]); | ||
| }); | ||
|
|
||
| it("does not restart when persisting the session fails", async () => { | ||
| const persistSession = vi.fn(async () => { | ||
| throw new Error("set meta failed"); | ||
| }); | ||
| const restartController = vi.fn(async () => {}); | ||
|
|
||
| await expect(applyTmuxSessionChange("mactop-3", "", persistSession, restartController)).rejects.toThrow( | ||
| "set meta failed" | ||
| ); | ||
| expect(restartController).not.toHaveBeenCalled(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| // Copyright 2026, Command Line Inc. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| export async function applyTmuxSessionChange( | ||
| currentSession: string, | ||
| nextSession: string, | ||
| persistSession: (session: string) => Promise<void>, | ||
| restartController: () => Promise<void> | ||
| ): Promise<boolean> { | ||
| if (currentSession === nextSession) { | ||
| return false; | ||
| } | ||
| await persistSession(nextSession); | ||
| await restartController(); | ||
| return true; | ||
| } | ||
|
|
||
| export function toggleTmuxSession(currentSession: string, selectedSession: string): string { | ||
| return currentSession === selectedSession ? "" : selectedSession; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| // Copyright 2026, Command Line Inc. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package blockcontroller | ||
|
|
||
| import ( | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/wavetermdev/waveterm/pkg/util/shellutil" | ||
| "github.com/wavetermdev/waveterm/pkg/waveobj" | ||
| ) | ||
|
|
||
| // Requirement: auto-associate remote sessions with tmux. | ||
| // Acceptance criteria coverage: | ||
| // 1. Remote block with term:tmux:session → tmux attach is injected (bash/zsh) | ||
| // 2. Local block / block without the meta key → no injection | ||
| // 3. Session names are quote-escaped to prevent injection | ||
| // 4. Unsupported shells (fish/pwsh) → no injection (syntax would not parse) | ||
|
|
||
| func TestBuildTmuxAttachScript_RemoteConn(t *testing.T) { | ||
| meta := waveobj.MetaMapType{ | ||
| waveobj.MetaKey_TermTmuxSession: "omlx-11335", | ||
| } | ||
| for _, shellType := range []string{shellutil.ShellType_bash, shellutil.ShellType_zsh} { | ||
| script := buildTmuxAttachScript(meta, "aws:co-gpu", shellType) | ||
| if script == "" { | ||
| t.Fatalf("expected tmux attach script for remote conn with shell %q", shellType) | ||
| } | ||
| if !strings.Contains(script, `exec tmux new -A -t 'omlx-11335'`) { | ||
| t.Fatalf("unexpected attach command: %q", script) | ||
| } | ||
| if !strings.Contains(script, `[ -n "$TMUX" ]`) { | ||
| t.Fatalf("missing nested tmux guard: %q", script) | ||
| } | ||
| if !strings.Contains(script, `command -v tmux`) { | ||
| t.Fatalf("missing tmux presence guard: %q", script) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestBuildTmuxAttachScript_UnsupportedShell(t *testing.T) { | ||
| meta := waveobj.MetaMapType{ | ||
| waveobj.MetaKey_TermTmuxSession: "omlx-11335", | ||
| } | ||
| for _, shellType := range []string{shellutil.ShellType_fish, shellutil.ShellType_pwsh, shellutil.ShellType_unknown} { | ||
| if script := buildTmuxAttachScript(meta, "aws:co-gpu", shellType); script != "" { | ||
| t.Fatalf("expected no script for shell %q, got %q", shellType, script) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestBuildTmuxAttachScript_LocalConn(t *testing.T) { | ||
| meta := waveobj.MetaMapType{ | ||
| waveobj.MetaKey_TermTmuxSession: "omlx-11335", | ||
| } | ||
| for _, connName := range []string{"local", "local:whatever", ""} { | ||
| if script := buildTmuxAttachScript(meta, connName, shellutil.ShellType_bash); script != "" { | ||
| t.Fatalf("expected no script for local conn %q, got %q", connName, script) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestBuildTmuxAttachScript_NoMetaKey(t *testing.T) { | ||
| if script := buildTmuxAttachScript(waveobj.MetaMapType{}, "aws:co-gpu", shellutil.ShellType_bash); script != "" { | ||
| t.Fatalf("expected no script when meta key absent, got %q", script) | ||
| } | ||
| if script := buildTmuxAttachScript(waveobj.MetaMapType{waveobj.MetaKey_TermTmuxSession: ""}, "aws:co-gpu", shellutil.ShellType_bash); script != "" { | ||
| t.Fatalf("expected no script when session name empty, got %q", script) | ||
| } | ||
| } | ||
|
|
||
| func TestBuildTmuxAttachScript_EscapesSessionName(t *testing.T) { | ||
| meta := waveobj.MetaMapType{ | ||
| waveobj.MetaKey_TermTmuxSession: `evil"; rm -rf /; echo "`, | ||
| } | ||
| script := buildTmuxAttachScript(meta, "aws:co-gpu", shellutil.ShellType_bash) | ||
| // Session names must be single-quoted so inner double quotes / semicolons cannot break shell structure. | ||
| if !strings.Contains(script, `-t 'evil"; rm -rf /; echo "'`) { | ||
| t.Fatalf("expected single-quoted session name, got: %q", script) | ||
| } | ||
| // Session names containing single quotes are also safe: inner single quotes escape to '\''. | ||
| meta2 := waveobj.MetaMapType{ | ||
| waveobj.MetaKey_TermTmuxSession: `a'b`, | ||
| } | ||
| script2 := buildTmuxAttachScript(meta2, "aws:co-gpu", shellutil.ShellType_bash) | ||
| if !strings.Contains(script2, `-t 'a'\''b'`) { | ||
| t.Fatalf("expected escaped inner single quote, got: %q", script2) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: wavetermdev/waveterm
Length of output: 19120
Normalize null RPC results to an empty list.
When
sendRpcCommandreturnsnull,WshClient.wshRpcCallreturnsnullinstead of a promise. The tmux consumer then callssessions.includes(...)outside its rejection path and can throw. Return[]from this wrapper for null results, or guard the consumer.🤖 Prompt for AI Agents
Source: Linters/SAST tools