Skip to content

feat: support Windows right-click context menu to open directory#3280

Open
cannebergeee wants to merge 1 commit intowavetermdev:mainfrom
cannebergeee:feature/right-click-context-menu
Open

feat: support Windows right-click context menu to open directory#3280
cannebergeee wants to merge 1 commit intowavetermdev:mainfrom
cannebergeee:feature/right-click-context-menu

Conversation

@cannebergeee
Copy link
Copy Markdown

Summary

  • Passes workingDirectory from Electron's second-instance event to createNewWaveWindow and ultimately sets "cmd:cwd" metadata on the new workspace, so shells spawn in the right-clicked directory.
  • Adds .reg files for installing/removing the Windows context menu entry.

Problem

Currently, right-clicking a folder in Windows Explorer and selecting "Open in Wave Terminal" opens Wave, but always starts in the home directory — the path from the context menu is silently ignored.

The root cause: emain.ts second-instance handler receives workingDirectory but throws it away:

electronApp.on("second-instance", (_event, argv, workingDirectory) => {
    fireAndForget(createNewWaveWindow); // ← workingDirectory ignored
});

Solution

Three small changes in the Electron main process:

  1. emain.ts — pass workingDirectory to createNewWaveWindow
  2. emain-window.tscreateNewWaveWindow(cwd?)createBrowserWindow → after workspace creation, call ObjectService.UpdateObjectMeta with {"cmd:cwd": cwd} before the default tab initializes its shell
  3. emain-window.ts — add cwd?: string to WindowOpts

The Go backend already reads "cmd:cwd" from workspace metadata in shellexec.go:

if cmdOpts.Cwd != "" {
    ecmd.Dir = cmdOpts.Cwd
}

Testing

  • TypeScript: tsc --noEmit — zero new errors in emain/
  • Build: electron-vite build — passes (SSR + preload + frontend)

Registry files included

  • wave-context-menu.reg — installs "Open in Wave Terminal" in Directory\Background, Directory, and Drive shell keys
  • wave-context-menu-remove.reg — removes the entries

Related

…ave Terminal

Pass workingDirectory from Electron second-instance event through
createNewWaveWindow > createBrowserWindow, and set cmd:cwd metadata
on new workspaces so shells spawn in the right-clicked directory.

Also add .reg files for installing/removing the context menu entry.
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 4, 2026

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 4, 2026

Walkthrough

The changes extend WindowOpts with an optional cwd?: string field. createNewWaveWindow is updated to accept a cwd parameter and log it. When a window is created with cwd provided, workspace metadata is updated with "cmd:cwd". The Electron second-instance event handler now captures workingDirectory and passes it to createNewWaveWindow. Two Windows registry scripts are added: wave-context-menu.reg registers context menu entries for opening Wave Terminal from directory backgrounds, folders, and drives, and wave-context-menu-remove.reg removes these entries.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding Windows right-click context menu support to open directories in Wave Terminal.
Description check ✅ Passed The description thoroughly explains the problem, solution, changes made, testing performed, and related information—all directly relevant to the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
emain/emain-window.ts (1)

725-745: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

cwd is silently dropped when recreating an existing window.

UpdateObjectMeta is nested inside if (!waveWindow), so it only runs when a brand-new window is created (waveWindow == null). The "recreate first window" code path in createNewWaveWindow (Line 866) calls createBrowserWindow(existingWindowData, fullConfig, { cwd }) with a non-null waveWindow, so the if (!waveWindow) block is entirely skipped and cwd is never persisted.

This hits the edge case where Wave is running but has no visible windows (all windows closed, app still alive), then the user invokes the context menu. Move the call to after the window data is fully resolved so it applies in all paths, including the workspace-recovery fallback at Line 736:

🐛 Proposed fix
     if (!waveWindow) {
         console.log("createBrowserWindow: no waveWindow");
         waveWindow = await WindowService.CreateWindow(null, "");
-        if (opts.cwd && waveWindow?.workspaceid) {
-            await ObjectService.UpdateObjectMeta(`workspace:${waveWindow.workspaceid}`, { "cmd:cwd": opts.cwd } as MetaType);
-        }
     }
     let workspace = await WorkspaceService.GetWorkspace(waveWindow.workspaceid);
     if (!workspace) {
         console.log("createBrowserWindow: no workspace, creating new window");
         await WindowService.CloseWindow(waveWindow.oid, true);
         waveWindow = await WindowService.CreateWindow(null, "");
         workspace = await WorkspaceService.GetWorkspace(waveWindow.workspaceid);
     }
+    if (opts.cwd && waveWindow?.workspaceid) {
+        await ObjectService.UpdateObjectMeta(`workspace:${waveWindow.workspaceid}`, { "cmd:cwd": opts.cwd } as MetaType);
+    }
     console.log("createBrowserWindow", waveWindow.oid, workspace.oid, workspace);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@emain/emain-window.ts` around lines 725 - 745, The cwd meta is only set when
waveWindow is null, so recreate paths skip persisting opts.cwd; move the
ObjectService.UpdateObjectMeta call so it runs after the window and workspace
are fully resolved (i.e., after you obtain/possibly recreate waveWindow and
fetch workspace via WorkspaceService.GetWorkspace), using
waveWindow?.workspaceid and opts.cwd to update `workspace:<id>` in all code
paths; update inside createBrowserWindow (around where waveWindow and workspace
are finalized, before constructing WaveBrowserWindow and returning) so both
CreateWindow and the workspace-recovery fallback persist cwd.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@emain/emain.ts`:
- Around line 389-393: The second-instance handler currently uses
workingDirectory which is only reliable for Directory\\Background; instead, in
the electronApp.on("second-instance", ...) callback scan the argv array for the
first non-flag absolute path (skip entries that start with "-" or are relative)
and use that path as cwd when calling createNewWaveWindow; if none found, fall
back to the provided workingDirectory/undefined. Update the handler around
electronApp.on("second-instance", ...) to perform this argv scan and then call
fireAndForget(() => createNewWaveWindow(resolvedCwd)) so Directory and Drive
launch targets open the correct folder.

In `@wave-context-menu.reg`:
- Around line 14-33: The .reg file hardcodes a developer path
(C:\\Users\\azurr\\AppData\\Local\\Programs\\waveterm\\Wave.exe) in all Icon and
command values so it will fail for other users; replace the static .reg with a
PowerShell installer that constructs the executable path from $env:LOCALAPPDATA
(e.g. $env:LOCALAPPDATA\Programs\waveterm\Wave.exe), writes entries under
HKCU:\SOFTWARE\Classes for the keys Directory\Background\shell\WaveTerminal,
Directory\shell\WaveTerminal and Drive\shell\WaveTerminal (setting the default
value, Icon and the \command default), and ensure the command default uses the
correct shell variable (%V vs %1) for Directory vs Folder/Drive as noted so the
context-menu opens the intended target without requiring elevation.

---

Outside diff comments:
In `@emain/emain-window.ts`:
- Around line 725-745: The cwd meta is only set when waveWindow is null, so
recreate paths skip persisting opts.cwd; move the ObjectService.UpdateObjectMeta
call so it runs after the window and workspace are fully resolved (i.e., after
you obtain/possibly recreate waveWindow and fetch workspace via
WorkspaceService.GetWorkspace), using waveWindow?.workspaceid and opts.cwd to
update `workspace:<id>` in all code paths; update inside createBrowserWindow
(around where waveWindow and workspace are finalized, before constructing
WaveBrowserWindow and returning) so both CreateWindow and the workspace-recovery
fallback persist cwd.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9b017f9f-8be0-434f-93e4-a751a49f5b7b

📥 Commits

Reviewing files that changed from the base of the PR and between 2e25ea1 and 480e0c7.

📒 Files selected for processing (4)
  • emain/emain-window.ts
  • emain/emain.ts
  • wave-context-menu-remove.reg
  • wave-context-menu.reg

Comment thread emain/emain.ts
Comment on lines 389 to 393
electronApp.on("second-instance", (_event, argv, workingDirectory) => {
console.log("second-instance event, argv:", argv, "workingDirectory:", workingDirectory);
fireAndForget(createNewWaveWindow);
const cwd = workingDirectory || undefined;
fireAndForget(() => createNewWaveWindow(cwd));
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Electron second-instance workingDirectory vs argv for Windows Explorer right-click Directory context menu

💡 Result:

Electron single-instance relaunch: use the second-instance event’s argv (and workingDirectory) rather than process.argv. 1) What Electron passes to the primary instance - When a second process starts and the first instance called app.requestSingleInstanceLock, Electron emits: - second-instance with parameters (event, argv, workingDirectory, additionalData). - argv is an array of the second instance’s command-line arguments, and workingDirectory is the second instance’s current working directory. - additionalData is JSON/object data you passed to app.requestSingleInstanceLock(additionalData); Electron documentation notes that argv may not be exactly the same as what you launched with (order may change; additional args may be appended). - Electron also notes Chromium may add extra arguments (e.g. --original-process-start-time). So, for your Windows Explorer right-click Directory context menu, the value you care about (the selected directory path) should be read from the argv array provided to the second-instance handler, and/or (more robustly) you should move that data into additionalData. 2) workingDirectory vs argv (which one to use) - workingDirectory: - It is the “current working directory” of the second launch. - This is generally not the selected directory path you get from Explorer context menus. - argv: - Contains the command-line arguments that Explorer (via your registered command) passed to your Electron executable for the second launch. - This is what you should parse to obtain the selected folder path. 3) Important caveats on Windows/Chromium argument handling - Electron/Chromium may manipulate the second-instance argv: - The documentation explicitly warns that argv “will not be exactly the same list of arguments as those passed to the second instance”; order can change and additional args can be appended. - Chromium command-line parsing can also split/move values such as switches vs positional arguments (documented in electron/electron issues). This can make “exact argv reproduction” unreliable. Practical implication for your question: - Don’t rely on workingDirectory to provide the right-click target. - Don’t rely on argv matching process.argv exactly. 4) Recommended robust pattern - In main process, register a single-instance lock and in second-instance: - Parse the directory path from the argv array you receive. - Focus/notify your existing BrowserWindow. - If you need strict data fidelity (exact path, including tricky quoting), prefer passing your payload through additionalData (Electron supports an object passed via app.requestSingleInstanceLock(additionalData) and received in the second-instance handler as additionalData). Code skeleton: const { app, BrowserWindow } = require('electron'); let win; const gotTheLock = app.requestSingleInstanceLock(/* optional initial data /); if (!gotTheLock) app.quit; else { app.on('second-instance', (event, argv, workingDirectory /, additionalData */) => { // argv: array of second instance’s command-line args // workingDirectory: cwd of second instance // Example: find your custom flag and read the following value // const path = extractFrom(argv); if (win) { if (win.isMinimized) win.restore; win.focus; } // send the extracted folder path to renderer / update tabs }); app.whenReady.then( => { win = new BrowserWindow({}); // load your app }); } 5) Bottom line answer to the “workingDirectory vs argv” part - Use argv for the Explorer right-click “Directory” target path you pass via your registry “command” line. - workingDirectory is only the second process’s current working directory and usually won’t be the selected directory. - Treat argv as parsed-by-Chromium and not guaranteed to equal your original process.argv; if you need exactness, use additionalData.

Citations:


workingDirectory is only correct for Directory\Background; the Directory and Drive entries will silently open the wrong folder.

workingDirectory is the second instance's current working directory, not necessarily the folder that was right-clicked. For Directory\Background, Windows Explorer sets the process CWD to the browsed folder, so workingDirectory is correct. However, for Directory and Drive, the target path is passed as a command-line argument (%1 in the registry command) and the process CWD is elsewhere — workingDirectory will silently resolve to the wrong location.

The robust approach is to scan argv for the first non-flag, absolute-path argument and fall back to workingDirectory. Note that Electron's argv may not exactly match what was passed to the second instance — order may change and Chromium flags (e.g., --original-process-start-time) may be appended, which is why scanning for a non-flag absolute path (not relying on a fixed position) is necessary.

Proposed fix
  electronApp.on("second-instance", (_event, argv, workingDirectory) => {
      console.log("second-instance event, argv:", argv, "workingDirectory:", workingDirectory);
-     const cwd = workingDirectory || undefined;
+     // For Directory/Drive context menu, the target path is passed as an explicit
+     // %1/%V argument in argv. For Directory\Background, workingDirectory is the folder.
+     // Electron may inject Chromium flags into argv, so scan for a non-flag absolute path.
+     const pathArg = argv?.find(
+         (arg) => !arg.startsWith("-") && /^[a-zA-Z]:[\\\/]/.test(arg)
+     );
+     const cwd = pathArg || workingDirectory || undefined;
      fireAndForget(() => createNewWaveWindow(cwd));
  });
📝 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
electronApp.on("second-instance", (_event, argv, workingDirectory) => {
console.log("second-instance event, argv:", argv, "workingDirectory:", workingDirectory);
fireAndForget(createNewWaveWindow);
const cwd = workingDirectory || undefined;
fireAndForget(() => createNewWaveWindow(cwd));
});
electronApp.on("second-instance", (_event, argv, workingDirectory) => {
console.log("second-instance event, argv:", argv, "workingDirectory:", workingDirectory);
// For Directory/Drive context menu, the target path is passed as an explicit
// %1/%V argument in argv. For Directory\Background, workingDirectory is the folder.
// Electron may inject Chromium flags into argv, so scan for a non-flag absolute path.
const pathArg = argv?.find(
(arg) => !arg.startsWith("-") && /^[a-zA-Z]:[\\\/]/.test(arg)
);
const cwd = pathArg || workingDirectory || undefined;
fireAndForget(() => createNewWaveWindow(cwd));
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@emain/emain.ts` around lines 389 - 393, The second-instance handler currently
uses workingDirectory which is only reliable for Directory\\Background; instead,
in the electronApp.on("second-instance", ...) callback scan the argv array for
the first non-flag absolute path (skip entries that start with "-" or are
relative) and use that path as cwd when calling createNewWaveWindow; if none
found, fall back to the provided workingDirectory/undefined. Update the handler
around electronApp.on("second-instance", ...) to perform this argv scan and then
call fireAndForget(() => createNewWaveWindow(resolvedCwd)) so Directory and
Drive launch targets open the correct folder.

Comment thread wave-context-menu.reg
Comment on lines +14 to +33
"Icon"="C:\\Users\\azurr\\AppData\\Local\\Programs\\waveterm\\Wave.exe"

[HKEY_CLASSES_ROOT\Directory\Background\shell\WaveTerminal\command]
@="\"C:\\Users\\azurr\\AppData\\Local\\Programs\\waveterm\\Wave.exe\" \"%V\""

; --- Folder itself (right-click on folder) ---
[HKEY_CLASSES_ROOT\Directory\shell\WaveTerminal]
@="Open in Wave Terminal"
"Icon"="C:\\Users\\azurr\\AppData\\Local\\Programs\\waveterm\\Wave.exe"

[HKEY_CLASSES_ROOT\Directory\shell\WaveTerminal\command]
@="\"C:\\Users\\azurr\\AppData\\Local\\Programs\\waveterm\\Wave.exe\" \"%1\""

; --- Drive root (right-click on drive in This PC) ---
[HKEY_CLASSES_ROOT\Drive\shell\WaveTerminal]
@="Open in Wave Terminal"
"Icon"="C:\\Users\\azurr\\AppData\\Local\\Programs\\waveterm\\Wave.exe"

[HKEY_CLASSES_ROOT\Drive\shell\WaveTerminal\command]
@="\"C:\\Users\\azurr\\AppData\\Local\\Programs\\waveterm\\Wave.exe\" \"%1\""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Hardcoded developer path makes this file non-functional for any other user.

All six path entries reference C:\\Users\\azurr\\AppData\\Local\\Programs\\waveterm\\Wave.exe — a personal installation that will not exist on any other machine. Merging this as-is means the feature ships completely broken for every user who tries to apply it.

Since .reg files don't expand %LOCALAPPDATA% in plain REG_SZ values, the recommended fix is to replace this static .reg file with a PowerShell script that builds the paths dynamically:

$wavePath = "$env:LOCALAPPDATA\Programs\waveterm\Wave.exe"

@(
    "HKCU:\SOFTWARE\Classes\Directory\Background\shell\WaveTerminal",
    "HKCU:\SOFTWARE\Classes\Directory\shell\WaveTerminal",
    "HKCU:\SOFTWARE\Classes\Drive\shell\WaveTerminal"
) | ForEach-Object {
    New-Item -Path "$_\command" -Force | Out-Null
    Set-ItemProperty -Path $_ -Name "(default)" -Value "Open in Wave Terminal"
    Set-ItemProperty -Path $_ -Name "Icon"       -Value $wavePath
    Set-ItemProperty -Path "$_\command" -Name "(default)" -Value "`"$wavePath`" `"%V`""
}

Using HKCU:\SOFTWARE\Classes\... rather than HKEY_CLASSES_ROOT\... also avoids the administrator-elevation requirement.

Note: for the Directory and Drive cases you'll also want to validate which shell variable (%V vs %1) correctly delivers the target path (see the comment on emain.ts).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wave-context-menu.reg` around lines 14 - 33, The .reg file hardcodes a
developer path (C:\\Users\\azurr\\AppData\\Local\\Programs\\waveterm\\Wave.exe)
in all Icon and command values so it will fail for other users; replace the
static .reg with a PowerShell installer that constructs the executable path from
$env:LOCALAPPDATA (e.g. $env:LOCALAPPDATA\Programs\waveterm\Wave.exe), writes
entries under HKCU:\SOFTWARE\Classes for the keys
Directory\Background\shell\WaveTerminal, Directory\shell\WaveTerminal and
Drive\shell\WaveTerminal (setting the default value, Icon and the \command
default), and ensure the command default uses the correct shell variable (%V vs
%1) for Directory vs Folder/Drive as noted so the context-menu opens the
intended target without requiring elevation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants