Skip to content

Feat/files widget active cwd tree#3399

Open
tungnguyentu wants to merge 4 commits into
wavetermdev:mainfrom
tungnguyentu:feat/files-widget-active-cwd-tree
Open

Feat/files widget active cwd tree#3399
tungnguyentu wants to merge 4 commits into
wavetermdev:mainfrom
tungnguyentu:feat/files-widget-active-cwd-tree

Conversation

@tungnguyentu

Copy link
Copy Markdown

No description provided.

…and switch to tree-mode

U1: When the Files widget is clicked, resolve the focused terminal's cwd
via GetFocusedBlockDataCommand and pass it as the preview's file target.
Add preview:treemode flag so the directory preview switches to tree
navigation. Remove hardcoded 'file: ~' from defwidget@files config so
the path is resolved at runtime.

U2: Add DirectoryTableOrTree component that conditionally renders a
TreeView when preview:treemode is set, preserving the existing flat
table for all other directory preview entry points. Includes a sticky
root-path indicator, lazy-loading via FileListStreamCommand, and
file-open actions through the existing model.handleOpenFile path.
…ense loading and directory tree preview issues
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds file-search RPC types and handlers, directory tree preview support, a search view, preview and widget wiring for search/tree modes, editor line positioning from metadata, and build-time compatibility aliases and shims.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.24% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive No pull request description was provided, so there is no meaningful summary to evaluate. Add a short description of the main behavior changes, especially cwd resolution and preview tree mode.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title references the files widget and cwd/tree behavior, which matches the main changes in the PR.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 7

🧹 Nitpick comments (9)
frontend/app/workspace/widgets.test.tsx (1)

5-61: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

These tests don't exercise the production logic.

Each case constructs a literal object and asserts its own properties (e.g. Line 10-11 checks eligibleMeta.view === "preview"), without ever invoking handleWidgetSelect or the cwd-resolution path it claims to validate. They will pass regardless of any regression in widgets.tsx, giving false confidence.

Consider importing handleWidgetSelect, injecting a mocked WidgetsEnv (a stubbed GetFocusedBlockDataCommand and a spy createBlock), and asserting the resulting meta passed to createBlock for the term/non-term/missing-cwd branches.

🤖 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 `@frontend/app/workspace/widgets.test.tsx` around lines 5 - 61, The widget
cwd-resolution tests are only checking literal object properties and never
exercise the real production path in handleWidgetSelect, so they won’t catch
regressions. Replace these assertions with tests that import handleWidgetSelect
from widgets.tsx and drive it through a mocked WidgetsEnv, stubbing
GetFocusedBlockDataCommand and spying on createBlock. Verify the meta passed to
createBlock for the preview, term, non-term, and missing-cwd branches, including
preview:treemode and connection handling.
frontend/app/view/preview/preview-directory.tsx (3)

1052-1118: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Icon-color logic is duplicated.

getIconColorForTree is a near-verbatim copy of getIconColor (lines 140–204), which is itself duplicated again in frontend/app/view/search/search.tsx (lines 114–156). Three independent copies of the same extension→color map will drift. Extract a single shared helper that takes (label, isDirectory, mimeColor?) and reuse it across the table, tree, and search views.

🤖 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 `@frontend/app/view/preview/preview-directory.tsx` around lines 1052 - 1118,
The icon-color mapping logic in getIconColorForTree is duplicated across
multiple view components and will drift over time. Extract a shared helper that
accepts the node label, directory flag, and optional mime color, then reuse it
from getIconColorForTree, getIconColor, and the search view’s color logic so all
extension-to-color rules live in one place.

1120-1170: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consolidate the three identical "switch to search" handlers; guard against double dispatch.

directoryKeyDownHandler (1120–1139) and both branches of handleTreeKeyDown (1143–1167) contain the exact same SetMetaCommand({view:"search", file, connection}) body. Additionally, both model.directoryKeyDownHandler (registered at 1172–1177) and the onKeyDown={handleTreeKeyDown} prop on TreeView (1247) respond to / and Cmd/Ctrl+F, so a single keypress can fire SetMetaCommand twice. Extract one helper (shared with preview-model.tsx) and route the key through a single path to avoid the redundant RPC.

🤖 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 `@frontend/app/view/preview/preview-directory.tsx` around lines 1120 - 1170,
The “switch to search” logic is duplicated in directoryKeyDownHandler and
handleTreeKeyDown, and both paths can fire on the same / or Cmd/Ctrl+F press,
causing SetMetaCommand to be sent twice. Extract the shared search-transition
behavior into one helper used by directoryKeyDownHandler, handleTreeKeyDown, and
the corresponding preview-model.tsx logic, then make only one keydown path
handle these shortcuts so the TreeView onKeyDown and
model.directoryKeyDownHandler do not both dispatch the same RPC.

709-718: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated search-switch block.

This is the same SetMetaCommand search transition implemented in keyDownHandler/endIconButtons of preview-model.tsx and twice more in DirectoryTreePreview. Centralize it (e.g., a model.openSearchView() method) so the oref/meta contract is defined once.

🤖 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 `@frontend/app/view/preview/preview-directory.tsx` around lines 709 - 718, The
search-view transition is duplicated across DirectoryTreePreview and
preview-model.tsx, so centralize the SetMetaCommand contract in one place. Add a
single helper on PreviewModel, such as model.openSearchView(), that reads
metaFilePath/connectionImmediate and sends the search meta update with the block
oref. Replace the repeated SetMetaCommand calls in keyDownHandler,
endIconButtons, and DirectoryTreePreview with that helper so the oref/meta shape
is defined once.
frontend/app/view/preview/preview-directory.test.tsx (1)

3-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

These tests don't exercise the component.

Each test asserts a property on a hand-written object literal (e.g., blockMeta["preview:treemode"]).toBe(true)) rather than rendering DirectoryTableOrTree and verifying it selects DirectoryTreePreview vs DirectoryPreview. As written they pass even if the component's branch logic regresses. Consider rendering the component with a mocked model and asserting which subtree mounts.

🤖 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 `@frontend/app/view/preview/preview-directory.test.tsx` around lines 3 - 19,
The tests in DirectoryPreview treemode flag only assert values on a manually
created blockMeta object and do not exercise DirectoryTableOrTree branch logic.
Update these cases to render the actual component path (DirectoryTableOrTree and
its related preview subtree) with a mocked model/block meta, then assert that
DirectoryTreePreview is mounted when preview:treemode is true and that
DirectoryPreview is used when the flag is absent or false.
frontend/app/view/preview/preview-model.tsx (2)

889-907: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Same search-switch block as the directory handlers.

This duplicates the SetMetaCommand transition found in DirectoryPreview/DirectoryTreePreview. Worth folding into a single openSearchView() to keep the meta shape consistent. No correctness issue here.

🤖 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 `@frontend/app/view/preview/preview-model.tsx` around lines 889 - 907, The
search-view transition in keyDownHandler duplicates the same SetMetaCommand
payload used by DirectoryPreview and DirectoryTreePreview, so extract that
shared logic into a single openSearchView() helper and reuse it here. Keep the
meta object shape identical across the directory handlers and this preview path,
and update keyDownHandler to call the shared method instead of building the
command inline.

710-759: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Target-block selection picks the last match and duplicates handleLineClick.

walkNodes keeps overwriting targetBlockId for every non-tree preview block, so with more than one preview pane it deterministically picks the last one visited rather than, say, the nearest/most-recent. If a single target is expected, break on first match or define an explicit selection rule.

This whole walk-and-reuse-or-split routine is also duplicated almost line-for-line in frontend/app/view/search/search.tsx handleLineClick (294–341). Extract a shared helper (e.g., openFileInPreviewTarget(filePath, connection, extraMeta?)) so both tree-open and search-open stay in sync.

🤖 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 `@frontend/app/view/preview/preview-model.tsx` around lines 710 - 759, The
tree-mode open logic in PreviewModel currently overwrites targetBlockId for
every matching preview block, so it always ends up using the last match instead
of a deliberate target. Update the walkNodes-based selection to stop at the
first valid non-tree preview block or apply an explicit selection rule, and keep
the fallback createBlockSplitVertically path unchanged. Also extract this
reuse-or-split flow from PreviewModel into a shared helper used by search.tsx
handleLineClick so both entry points stay consistent.
frontend/app/view/search/search.tsx (2)

405-438: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Result rows are non-interactive for keyboard users.

The folder/file rows and per-line match rows are <div>s with onClick only (406, 430). They aren't focusable or activatable via keyboard and expose no role. Use <button> (or add role, tabIndex={0}, and an onKeyDown for Enter/Space) so search navigation is keyboard-accessible.

🤖 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 `@frontend/app/view/search/search.tsx` around lines 405 - 438, The search tree
rows in renderNode are click-only divs, so folder/file entries and match rows in
search.tsx cannot be reached or activated by keyboard. Update the clickable row
containers around toggleExpand(node.id) and handleLineClick(...) to use semantic
button behavior or add proper accessibility props (role, tabIndex, and keyboard
handlers for Enter/Space) so users can focus and activate them without a mouse.

22-37: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Use the generated RPC types here instead of re-declaring them locally. These interfaces match frontend/types/gotypes.d.ts today, but the duplicate copy can drift when the backend contract changes.

🤖 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 `@frontend/app/view/search/search.tsx` around lines 22 - 37, Replace the
locally re-declared search result/request interfaces in search.tsx with the
generated RPC types from frontend/types/gotypes.d.ts. Update the
CommandFileSearchData, FileSearchMatch, and FileSearchResult usages to import
and reference the generated symbols so the search flow stays aligned with the
backend contract and avoids drift.
🤖 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 `@electron.vite.config.ts`:
- Around line 15-24: The Mermaid import rewrite in streamdownMermaidFix is
building an invalid /@fs path on Windows because MERMAID_CORE uses native
separators from path.resolve. Update the transform replacement so the generated
import uses a POSIX-style absolute path with a leading slash (for example by
normalizing separators before interpolating), and keep the logic centered in
MERMAID_CORE and streamdownMermaidFix so the rewritten import resolves correctly
across platforms.

In `@frontend/app/block/block.tsx`:
- Around line 276-291: Move the view-model transition out of render in
BlockInner/SubBlockInner: the current conditional that calls makeViewModel,
registerBlockComponentModel, and dispose on the previous instance during render
should be relocated to a commit-phase effect so rendering stays pure. Update the
lifecycle around viewModel, registerBlockComponentModel,
unregisterBlockComponentModel, and the useEffect cleanup so creation,
registration, and disposal happen exactly once per committed instance, avoiding
duplicate dispose calls under StrictMode or aborted renders.

In `@frontend/app/treeview/treeview.tsx`:
- Around line 283-289: The TreeView state is being reset because
defaultExpandedIds receives a new array reference on every parent render, which
causes expandedIds and nodesById to be reinitialized and collapse loaded
branches. Update the parent to memoize the [dirPath] array before passing it
into TreeView, and keep the TreeView effect/useMemo logic in treeview.tsx keyed
on stable defaultExpandedIds so it only resets when the actual value changes.

In `@frontend/app/view/search/search.tsx`:
- Around line 233-270: The search flow in handleSearch is vulnerable to
stale-result races because multiple async FileSearchCommand calls can overlap
and update state out of order. Add request sequencing or cancellation inside
handleSearch and the useEffect debounce path so only the latest
query/matchCase/useRegex invocation is allowed to call setResults and
setExpanded. Use the existing handleSearch callback and its callers from the
debounced effect and the Match-Case/Regex toggles to compare the current request
before applying RPC results.
- Line 350: The regex in search.tsx is over-escaping the forward slash, which
triggers the no-useless-escape lint warning. Update the escaping pattern used in
the query.replace call within the search logic to remove the unnecessary escape
for “/”, while keeping the rest of the character-class escaping intact. Use the
existing escaped variable in the same search-related code path to verify the
fix.

In `@pkg/wshrpc/wshremote/wshremote_file.go`:
- Around line 667-720: RemoteFileSearchCommand accepts ctx but the
filepath.WalkDir traversal never checks it, so cancellation and RPC timeouts are
ignored. Update the walk callback in ServerImpl.RemoteFileSearchCommand to
consult ctx.Err() at the start of each visit and return that error to stop the
walk immediately. Keep the fix localized around the WalkDir callback so
long-running searches abort cleanly when the caller cancels.
- Around line 771-802: The search loop in wshremote_file.go uses bufio.Scanner
without increasing its token buffer, so long lines can terminate scanning early
and miss later matches. Update the scanning logic around the scanner.Scan() loop
to call scanner.Buffer(...) before iteration with a larger max token size, and
make sure scanner.Err() is checked after the loop so scan failures are surfaced.
If this code is expected to handle very long lines routinely, consider replacing
the Scanner-based logic in this file search path with bufio.Reader instead.

---

Nitpick comments:
In `@frontend/app/view/preview/preview-directory.test.tsx`:
- Around line 3-19: The tests in DirectoryPreview treemode flag only assert
values on a manually created blockMeta object and do not exercise
DirectoryTableOrTree branch logic. Update these cases to render the actual
component path (DirectoryTableOrTree and its related preview subtree) with a
mocked model/block meta, then assert that DirectoryTreePreview is mounted when
preview:treemode is true and that DirectoryPreview is used when the flag is
absent or false.

In `@frontend/app/view/preview/preview-directory.tsx`:
- Around line 1052-1118: The icon-color mapping logic in getIconColorForTree is
duplicated across multiple view components and will drift over time. Extract a
shared helper that accepts the node label, directory flag, and optional mime
color, then reuse it from getIconColorForTree, getIconColor, and the search
view’s color logic so all extension-to-color rules live in one place.
- Around line 1120-1170: The “switch to search” logic is duplicated in
directoryKeyDownHandler and handleTreeKeyDown, and both paths can fire on the
same / or Cmd/Ctrl+F press, causing SetMetaCommand to be sent twice. Extract the
shared search-transition behavior into one helper used by
directoryKeyDownHandler, handleTreeKeyDown, and the corresponding
preview-model.tsx logic, then make only one keydown path handle these shortcuts
so the TreeView onKeyDown and model.directoryKeyDownHandler do not both dispatch
the same RPC.
- Around line 709-718: The search-view transition is duplicated across
DirectoryTreePreview and preview-model.tsx, so centralize the SetMetaCommand
contract in one place. Add a single helper on PreviewModel, such as
model.openSearchView(), that reads metaFilePath/connectionImmediate and sends
the search meta update with the block oref. Replace the repeated SetMetaCommand
calls in keyDownHandler, endIconButtons, and DirectoryTreePreview with that
helper so the oref/meta shape is defined once.

In `@frontend/app/view/preview/preview-model.tsx`:
- Around line 889-907: The search-view transition in keyDownHandler duplicates
the same SetMetaCommand payload used by DirectoryPreview and
DirectoryTreePreview, so extract that shared logic into a single
openSearchView() helper and reuse it here. Keep the meta object shape identical
across the directory handlers and this preview path, and update keyDownHandler
to call the shared method instead of building the command inline.
- Around line 710-759: The tree-mode open logic in PreviewModel currently
overwrites targetBlockId for every matching preview block, so it always ends up
using the last match instead of a deliberate target. Update the walkNodes-based
selection to stop at the first valid non-tree preview block or apply an explicit
selection rule, and keep the fallback createBlockSplitVertically path unchanged.
Also extract this reuse-or-split flow from PreviewModel into a shared helper
used by search.tsx handleLineClick so both entry points stay consistent.

In `@frontend/app/view/search/search.tsx`:
- Around line 405-438: The search tree rows in renderNode are click-only divs,
so folder/file entries and match rows in search.tsx cannot be reached or
activated by keyboard. Update the clickable row containers around
toggleExpand(node.id) and handleLineClick(...) to use semantic button behavior
or add proper accessibility props (role, tabIndex, and keyboard handlers for
Enter/Space) so users can focus and activate them without a mouse.
- Around line 22-37: Replace the locally re-declared search result/request
interfaces in search.tsx with the generated RPC types from
frontend/types/gotypes.d.ts. Update the CommandFileSearchData, FileSearchMatch,
and FileSearchResult usages to import and reference the generated symbols so the
search flow stays aligned with the backend contract and avoids drift.

In `@frontend/app/workspace/widgets.test.tsx`:
- Around line 5-61: The widget cwd-resolution tests are only checking literal
object properties and never exercise the real production path in
handleWidgetSelect, so they won’t catch regressions. Replace these assertions
with tests that import handleWidgetSelect from widgets.tsx and drive it through
a mocked WidgetsEnv, stubbing GetFocusedBlockDataCommand and spying on
createBlock. Verify the meta passed to createBlock for the preview, term,
non-term, and missing-cwd branches, including preview:treemode and connection
handling.
🪄 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: bddbefbe-42ef-4ac7-89e1-e411ff592bb0

📥 Commits

Reviewing files that changed from the base of the PR and between c99022c and fc5bdf5.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (27)
  • electron.vite.config.ts
  • frontend/app/block/block.tsx
  • frontend/app/block/blockregistry.ts
  • frontend/app/block/blockutil.tsx
  • frontend/app/element/markdown.tsx
  • frontend/app/store/keymodel.ts
  • frontend/app/store/wshclientapi.ts
  • frontend/app/treeview/treeview.tsx
  • frontend/app/view/preview/preview-directory.test.tsx
  • frontend/app/view/preview/preview-directory.tsx
  • frontend/app/view/preview/preview-edit.tsx
  • frontend/app/view/preview/preview-model.tsx
  • frontend/app/view/preview/preview.tsx
  • frontend/app/view/search/search.tsx
  • frontend/app/workspace/widgets.test.tsx
  • frontend/app/workspace/widgets.tsx
  • frontend/extend-compat.ts
  • frontend/preview/previews/widgets.preview.tsx
  • frontend/style-to-js-compat.ts
  • frontend/types/gotypes.d.ts
  • frontend/util/waveutil.ts
  • pkg/remote/fileshare/wshfs/wshfs.go
  • pkg/wconfig/defaultconfig/widgets.json
  • pkg/wshrpc/wshclient/wshclient.go
  • pkg/wshrpc/wshremote/wshremote_file.go
  • pkg/wshrpc/wshrpctypes_file.go
  • pkg/wshrpc/wshserver/wshserver.go

Comment thread electron.vite.config.ts Outdated
Comment on lines +15 to +24
const MERMAID_CORE = path.resolve(__dirname, "node_modules/mermaid/dist/mermaid.core.mjs");

// for debugging
// target is like -- path.resolve(__dirname, "frontend/app/workspace/workspace-layout-model.ts");
function whoImportsTarget(target: string) {
function streamdownMermaidFix() {
return {
name: "who-imports-target",
buildEnd() {
// Build reverse graph: child -> [importers...]
const parents = new Map<string, string[]>();
for (const id of (this as any).getModuleIds()) {
const info = (this as any).getModuleInfo(id);
if (!info) continue;
for (const child of [...info.importedIds, ...info.dynamicallyImportedIds]) {
const arr = parents.get(child) ?? [];
arr.push(id);
parents.set(child, arr);
}
}

// Walk upward from TARGET and print paths to entries
const entries = [...parents.keys()].filter((id) => {
const m = (this as any).getModuleInfo(id);
return m?.isEntry;
});

const seen = new Set<string>();
const stack: string[] = [];
const dfs = (node: string) => {
if (seen.has(node)) return;
seen.add(node);
stack.push(node);
const ps = parents.get(node) || [];
if (ps.length === 0) {
// hit a root (likely main entry or plugin virtual)
console.log("\nImporter chain:");
stack
.slice()
.reverse()
.forEach((s) => console.log(" ↳", s));
} else {
for (const p of ps) dfs(p);
}
stack.pop();
};

if (!parents.has(target)) {
console.log(`[who-imports] TARGET not in MAIN graph: ${target}`);
} else {
dfs(target);
}
},
async resolveId(id: any, importer: any) {
const r = await (this as any).resolve(id, importer, { skipSelf: true });
if (r?.id === target) {
console.log(`[resolve] ${importer} -> ${id} -> ${r.id}`);
}
return null;
name: "streamdown-mermaid-fix",
enforce: "pre" as const,
transform(code: string, id: string) {
if (!id.includes("node_modules/streamdown")) return null;
if (!code.includes("import('mermaid')")) return null;
return code.replaceAll("import('mermaid')", `import('/@fs${MERMAID_CORE}')`);

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

/@fs URL is broken on Windows. path.resolve returns OS-native separators, so on Windows MERMAID_CORE is e.g. C:\...\mermaid.core.mjs. Interpolating it as import('/@fs${MERMAID_CORE}') yields /@fsC:\... — Vite's /@fs prefix expects a POSIX-style absolute path (/@fs/C:/...), so the rewritten import will fail to resolve on Windows. Since this is an Electron app targeting Windows too, normalize the separators and ensure a leading slash.

🔧 Proposed fix
-const MERMAID_CORE = path.resolve(__dirname, "node_modules/mermaid/dist/mermaid.core.mjs");
+const MERMAID_CORE = path.resolve(__dirname, "node_modules/mermaid/dist/mermaid.core.mjs").replace(/\\/g, "/");
@@
-            return code.replaceAll("import('mermaid')", `import('/@fs${MERMAID_CORE}')`);
+            const fsPath = MERMAID_CORE.startsWith("/") ? MERMAID_CORE : `/${MERMAID_CORE}`;
+            return code.replaceAll("import('mermaid')", `import('/@fs${fsPath}')`);
📝 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
const MERMAID_CORE = path.resolve(__dirname, "node_modules/mermaid/dist/mermaid.core.mjs");
// for debugging
// target is like -- path.resolve(__dirname, "frontend/app/workspace/workspace-layout-model.ts");
function whoImportsTarget(target: string) {
function streamdownMermaidFix() {
return {
name: "who-imports-target",
buildEnd() {
// Build reverse graph: child -> [importers...]
const parents = new Map<string, string[]>();
for (const id of (this as any).getModuleIds()) {
const info = (this as any).getModuleInfo(id);
if (!info) continue;
for (const child of [...info.importedIds, ...info.dynamicallyImportedIds]) {
const arr = parents.get(child) ?? [];
arr.push(id);
parents.set(child, arr);
}
}
// Walk upward from TARGET and print paths to entries
const entries = [...parents.keys()].filter((id) => {
const m = (this as any).getModuleInfo(id);
return m?.isEntry;
});
const seen = new Set<string>();
const stack: string[] = [];
const dfs = (node: string) => {
if (seen.has(node)) return;
seen.add(node);
stack.push(node);
const ps = parents.get(node) || [];
if (ps.length === 0) {
// hit a root (likely main entry or plugin virtual)
console.log("\nImporter chain:");
stack
.slice()
.reverse()
.forEach((s) => console.log(" ↳", s));
} else {
for (const p of ps) dfs(p);
}
stack.pop();
};
if (!parents.has(target)) {
console.log(`[who-imports] TARGET not in MAIN graph: ${target}`);
} else {
dfs(target);
}
},
async resolveId(id: any, importer: any) {
const r = await (this as any).resolve(id, importer, { skipSelf: true });
if (r?.id === target) {
console.log(`[resolve] ${importer} -> ${id} -> ${r.id}`);
}
return null;
name: "streamdown-mermaid-fix",
enforce: "pre" as const,
transform(code: string, id: string) {
if (!id.includes("node_modules/streamdown")) return null;
if (!code.includes("import('mermaid')")) return null;
return code.replaceAll("import('mermaid')", `import('/@fs${MERMAID_CORE}')`);
const MERMAID_CORE = path.resolve(__dirname, "node_modules/mermaid/dist/mermaid.core.mjs").replace(/\\/g, "/");
function streamdownMermaidFix() {
return {
name: "streamdown-mermaid-fix",
enforce: "pre" as const,
transform(code: string, id: string) {
if (!id.includes("node_modules/streamdown")) return null;
if (!code.includes("import('mermaid')")) return null;
const fsPath = MERMAID_CORE.startsWith("/") ? MERMAID_CORE : `/${MERMAID_CORE}`;
return code.replaceAll("import('mermaid')", `import('/@fs${fsPath}')`);
🤖 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 `@electron.vite.config.ts` around lines 15 - 24, The Mermaid import rewrite in
streamdownMermaidFix is building an invalid /@fs path on Windows because
MERMAID_CORE uses native separators from path.resolve. Update the transform
replacement so the generated import uses a POSIX-style absolute path with a
leading slash (for example by normalizing separators before interpolating), and
keep the logic centered in MERMAID_CORE and streamdownMermaidFix so the
rewritten import resolves correctly across platforms.

Comment on lines +276 to +291
if (viewModel == null || viewModel.viewType !== props.viewType) {
if (viewModel != null) {
viewModel.dispose?.();
}
viewModel = makeViewModel(props.nodeModel.blockId, props.viewType, props.nodeModel, tabModel, waveEnv);
registerBlockComponentModel(props.nodeModel.blockId, { viewModel });
}
useEffect(() => {
return () => {
unregisterBlockComponentModel(props.nodeModel.blockId);
const currentBcm = getBlockComponentModel(props.nodeModel.blockId);
if (currentBcm?.viewModel === viewModel) {
unregisterBlockComponentModel(props.nodeModel.blockId);
}
viewModel?.dispose?.();
};
}, []);
}, [viewModel]);

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.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect dispose() implementations to confirm double-dispose tolerance.
rg -nP -C3 '\bdispose\s*\(' frontend/app/view --type=ts -g '*model*'

Repository: wavetermdev/waveterm

Length of output: 1112


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== block.tsx around reported lines ==\n'
sed -n '250,360p' frontend/app/block/block.tsx

printf '\n== vdom-model.tsx dispose implementation ==\n'
sed -n '180,210p' frontend/app/view/vdom/vdom-model.tsx

printf '\n== term-model.ts dispose implementation ==\n'
sed -n '580,620p' frontend/app/view/term/term-model.ts

Repository: wavetermdev/waveterm

Length of output: 6698


Keep view-model creation/disposal out of render.

BlockInner and SubBlockInner both dispose the previous model, create a new one, and register it during render. That breaks render purity and can trigger extra create/dispose cycles under StrictMode or aborted concurrent renders. The previous effect cleanup then disposes the old instance again, since dispose() here has no repeat-call guard.

Move this transition into an effect or other commit-phase lifecycle so each model is created, registered, and torn down exactly once.

🤖 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 `@frontend/app/block/block.tsx` around lines 276 - 291, Move the view-model
transition out of render in BlockInner/SubBlockInner: the current conditional
that calls makeViewModel, registerBlockComponentModel, and dispose on the
previous instance during render should be relocated to a commit-phase effect so
rendering stays pure. Update the lifecycle around viewModel,
registerBlockComponentModel, unregisterBlockComponentModel, and the useEffect
cleanup so creation, registration, and disposal happen exactly once per
committed instance, avoiding duplicate dispose calls under StrictMode or aborted
renders.

Comment on lines +283 to 289
setExpandedIds(new Set(defaultExpandedIds ?? []));
}, [initialNodes, defaultExpandedIds]);

const visibleRows = useMemo(() => buildVisibleRows(nodesById, rootIds, expandedIds), [nodesById, rootIds, expandedIds]);
const idToIndex = useMemo(
() => new Map(visibleRows.map((row, index) => [row.id, index])),
[visibleRows]
const visibleRows = useMemo(
() => buildVisibleRows(nodesById, rootIds, expandedIds, filterText),
[nodesById, rootIds, expandedIds, filterText]
);

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether defaultExpandedIds / initialNodes are memoized at call sites.
rg -nP -C3 '(defaultExpandedIds|initialNodes)\s*=\s*\{' frontend/app/view/preview/preview-directory.tsx
rg -nP -C3 '\b(initialNodes|useMemo)\b' frontend/app/view/preview/preview-directory.tsx

Repository: wavetermdev/waveterm

Length of output: 2410


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether the TreeView effect resets only on prop identity changes and
# whether loadChildren is gated by missing children or expansion state.
rg -n -C4 'useEffect|setExpandedIds|buildVisibleRows|loadChildren|childrenLoaded|hasChildren|expandedIds' frontend/app/treeview/treeview.tsx

# Check if the preview-directory tree props are otherwise memoized or if parent rerenders are expected.
rg -n -C4 'defaultExpandedIds|rootIds|initialNodes|useMemo|useState|useEffect|set.*State' frontend/app/view/preview/preview-directory.tsx

Repository: wavetermdev/waveterm

Length of output: 12775


Memoize defaultExpandedIds before passing it to TreeView. The inline [dirPath] array changes on every parent render, so TreeView keeps resetting expandedIds/nodesById, collapsing loaded branches and forcing them to load again on the next expand.

🤖 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 `@frontend/app/treeview/treeview.tsx` around lines 283 - 289, The TreeView
state is being reset because defaultExpandedIds receives a new array reference
on every parent render, which causes expandedIds and nodesById to be
reinitialized and collapse loaded branches. Update the parent to memoize the
[dirPath] array before passing it into TreeView, and keep the TreeView
effect/useMemo logic in treeview.tsx keyed on stable defaultExpandedIds so it
only resets when the actual value changes.

Comment on lines +233 to +270
const handleSearch = React.useCallback(async (q: string, mc: boolean, rx: boolean) => {
if (!q) {
setResults([]);
setErrorMsg(null);
return;
}
setLoading(true);
setErrorMsg(null);
try {
const formattedPath = formatRemoteUri(searchDir, connName);
const data: CommandFileSearchData = {
path: formattedPath,
query: q,
ignorecase: !mc,
regex: rx,
};
const response = await model.env.rpc.FileSearchCommand(TabRpcClient, data);
setResults(response ?? []);
setExpanded({});
} catch (e: any) {
console.error("File search failed", e);
setErrorMsg(e?.message ?? String(e));
} finally {
setLoading(false);
}
}, [connName, searchDir]);

React.useEffect(() => {
if (!query) {
setResults([]);
setErrorMsg(null);
return;
}
const timer = setTimeout(() => {
handleSearch(query, matchCase, useRegex);
}, 300);
return () => clearTimeout(timer);
}, [query, matchCase, useRegex, handleSearch]);

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Search results can be applied out of order (stale-result race).

handleSearch is async with no sequencing/cancellation. The debounced effect (266–268) and the direct calls from the Match-Case/Regex toggles (484, 497) can run concurrently; whichever RPC resolves last wins, so an older query's results can overwrite a newer one's. Track the latest request (e.g., an incrementing request id or AbortController) and ignore responses that aren't current before calling setResults.

🤖 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 `@frontend/app/view/search/search.tsx` around lines 233 - 270, The search flow
in handleSearch is vulnerable to stale-result races because multiple async
FileSearchCommand calls can overlap and update state out of order. Add request
sequencing or cancellation inside handleSearch and the useEffect debounce path
so only the latest query/matchCase/useRegex invocation is allowed to call
setResults and setExpanded. Use the existing handleSearch callback and its
callers from the debounced effect and the Match-Case/Regex toggles to compare
the current request before applying RPC results.

if (useRegex) {
regex = new RegExp(query, matchCase ? "g" : "gi");
} else {
const escaped = query.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the unnecessary escape.

Inside the character class the / does not need escaping; ESLint flags \/ as no-useless-escape.

🔧 Proposed fix
-                const escaped = query.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
+                const escaped = query.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
📝 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
const escaped = query.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
const escaped = query.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
🧰 Tools
🪛 ESLint

[error] 350-350: Unnecessary escape character: /.

(no-useless-escape)

🤖 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 `@frontend/app/view/search/search.tsx` at line 350, The regex in search.tsx is
over-escaping the forward slash, which triggers the no-useless-escape lint
warning. Update the escaping pattern used in the query.replace call within the
search logic to remove the unnecessary escape for “/”, while keeping the rest of
the character-class escaping intact. Use the existing escaped variable in the
same search-related code path to verify the fix.

Source: Linters/SAST tools

Comment on lines +667 to +720
func (impl *ServerImpl) RemoteFileSearchCommand(ctx context.Context, data wshrpc.CommandFileSearchData) ([]*wshrpc.FileSearchResult, error) {
searchPath, err := wavebase.ExpandHomeDir(data.Path)
if err != nil {
return nil, fmt.Errorf("invalid search path %q: %w", data.Path, err)
}
searchPath = filepath.Clean(searchPath)

log.Printf("RemoteFileSearchCommand: path=%q query=%q regex=%v ignorecase=%v resolved_path=%q", data.Path, data.Query, data.Regex, data.IgnoreCase, searchPath)

if data.Query == "" {
return nil, nil
}

var re *regexp.Regexp
if data.Regex {
regexQuery := data.Query
if data.IgnoreCase {
regexQuery = "(?i)" + regexQuery
}
var err error
re, err = regexp.Compile(regexQuery)
if err != nil {
return nil, fmt.Errorf("invalid regex: %w", err)
}
}

queryLower := strings.ToLower(data.Query)
var results []*wshrpc.FileSearchResult
totalMatches := 0
const maxMatches = 2000
const maxFileSize = 5 * 1024 * 1024 // 5MB
const maxLineLen = 1000

excludedDirs := map[string]bool{
".git": true,
"node_modules": true,
".gemini": true,
".roo": true,
".task": true,
".vscode": true,
".zed": true,
"dist": true,
"build": true,
"tmp": true,
"__pycache__": true,
".venv": true,
"venv": true,
"env": true,
".pytest_cache": true,
"vendor": true,
".idea": true,
}

err = filepath.WalkDir(searchPath, func(path string, d fs.DirEntry, walkErr error) error {

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Honor ctx cancellation during the directory walk.

ctx is accepted but never consulted. filepath.WalkDir over a large tree can run for a long time, and there is no way to abort it if the caller cancels or the RPC times out — the server keeps walking and reading files. Check ctx.Err() at the top of the walk callback (and abort the walk) so cancellation/timeouts are respected.

🛡️ Suggested cancellation check inside the walk callback
 	err = filepath.WalkDir(searchPath, func(path string, d fs.DirEntry, walkErr error) error {
+		if err := ctx.Err(); err != nil {
+			return err
+		}
 		if walkErr != nil {
📝 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
func (impl *ServerImpl) RemoteFileSearchCommand(ctx context.Context, data wshrpc.CommandFileSearchData) ([]*wshrpc.FileSearchResult, error) {
searchPath, err := wavebase.ExpandHomeDir(data.Path)
if err != nil {
return nil, fmt.Errorf("invalid search path %q: %w", data.Path, err)
}
searchPath = filepath.Clean(searchPath)
log.Printf("RemoteFileSearchCommand: path=%q query=%q regex=%v ignorecase=%v resolved_path=%q", data.Path, data.Query, data.Regex, data.IgnoreCase, searchPath)
if data.Query == "" {
return nil, nil
}
var re *regexp.Regexp
if data.Regex {
regexQuery := data.Query
if data.IgnoreCase {
regexQuery = "(?i)" + regexQuery
}
var err error
re, err = regexp.Compile(regexQuery)
if err != nil {
return nil, fmt.Errorf("invalid regex: %w", err)
}
}
queryLower := strings.ToLower(data.Query)
var results []*wshrpc.FileSearchResult
totalMatches := 0
const maxMatches = 2000
const maxFileSize = 5 * 1024 * 1024 // 5MB
const maxLineLen = 1000
excludedDirs := map[string]bool{
".git": true,
"node_modules": true,
".gemini": true,
".roo": true,
".task": true,
".vscode": true,
".zed": true,
"dist": true,
"build": true,
"tmp": true,
"__pycache__": true,
".venv": true,
"venv": true,
"env": true,
".pytest_cache": true,
"vendor": true,
".idea": true,
}
err = filepath.WalkDir(searchPath, func(path string, d fs.DirEntry, walkErr error) error {
err = filepath.WalkDir(searchPath, func(path string, d fs.DirEntry, walkErr error) error {
if err := ctx.Err(); err != nil {
return err
}
if walkErr != nil {
🤖 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 `@pkg/wshrpc/wshremote/wshremote_file.go` around lines 667 - 720,
RemoteFileSearchCommand accepts ctx but the filepath.WalkDir traversal never
checks it, so cancellation and RPC timeouts are ignored. Update the walk
callback in ServerImpl.RemoteFileSearchCommand to consult ctx.Err() at the start
of each visit and return that error to stop the walk immediately. Keep the fix
localized around the WalkDir callback so long-running searches abort cleanly
when the caller cancels.

Comment on lines +771 to +802
scanner := bufio.NewScanner(f)
lineNum := 0
for scanner.Scan() {
lineNum++
line := scanner.Text()
matched := false

if data.Regex {
matched = re.MatchString(line)
} else {
if data.IgnoreCase {
matched = strings.Contains(strings.ToLower(line), queryLower)
} else {
matched = strings.Contains(line, data.Query)
}
}

if matched {
displayLine := line
if len(displayLine) > maxLineLen {
displayLine = displayLine[:maxLineLen] + "..."
}
matches = append(matches, wshrpc.FileSearchMatch{
LineNum: lineNum,
Line: displayLine,
})
totalMatches++
if totalMatches >= maxMatches {
break
}
}
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

bufio.Scanner default MaxScanTokenSize token too long error

💡 Result:

The "bufio.Scanner: token too long" error occurs because the bufio.Scanner has a default maximum token size of 64 KB (defined by the constant MaxScanTokenSize) [1][2][3]. When the scanner encounters a token (such as a line when using the default ScanLines split function) that exceeds this limit, it stops scanning and returns this error [4][5]. To handle tokens larger than 64 KB, you can increase the maximum token size using the Scanner.Buffer method [2][6]. How to increase the limit: You must call the Buffer method before calling Scan [1][4]. The method takes two arguments: an initial byte slice (which can be nil or an empty slice) and the new maximum capacity [2][7][6]. Example: scanner:= bufio.NewScanner(file) // Increase the maximum token size to 1 MB const maxCapacity = 1024 * 1024 buf:= make([]byte, maxCapacity) scanner.Buffer(buf, maxCapacity) for scanner.Scan { // Process tokens... } Important considerations: 1. Timing: Buffer must be called before the first call to Scan; otherwise, it will panic [1][4]. 2. Memory: Increasing the buffer size will cause the scanner to allocate a larger buffer in memory, which may be significant if you set a very large limit [4][5]. 3. Alternative: If you are processing very large files or tokens of unknown, potentially unbounded size, the official Go documentation recommends using bufio.Reader instead of bufio.Scanner [7][5]. bufio.Reader provides more control over reading and error handling, making it more robust for streaming large or unpredictable data [7][5].

Citations:


Handle scanner token limits in this search loop
bufio.Scanner stops at 64 KB tokens, so a single long line can end the scan early and drop the rest of the file from results. Add scanner.Buffer(...) before scanning and check scanner.Err() after the loop, or switch to bufio.Reader if long lines are expected.

🤖 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 `@pkg/wshrpc/wshremote/wshremote_file.go` around lines 771 - 802, The search
loop in wshremote_file.go uses bufio.Scanner without increasing its token
buffer, so long lines can terminate scanning early and miss later matches.
Update the scanning logic around the scanner.Scan() loop to call
scanner.Buffer(...) before iteration with a larger max token size, and make sure
scanner.Err() is checked after the loop so scan failures are surfaced. If this
code is expected to handle very long lines routinely, consider replacing the
Scanner-based logic in this file search path with bufio.Reader instead.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 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 `@frontend/app/view/term/osc-handlers.ts`:
- Line 241: The path parsing in the `osc-handlers` flow is incorrectly
converting `+` to a space before decoding, which breaks valid `file:` paths like
`cmd:cwd` values containing literal plus signs. Update the `pathPart` handling
in the relevant `decodeURIComponent(url.pathname...)` logic to preserve `+`
as-is and rely on percent-decoding only, so `cmd:cwd` stores the exact directory
and the preview/search widgets open the correct location.
🪄 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: 96f7c64a-ad2c-4cd2-959e-57227a16f2b5

📥 Commits

Reviewing files that changed from the base of the PR and between baf4b59 and 13ff29e.

📒 Files selected for processing (1)
  • frontend/app/view/term/osc-handlers.ts

return true;
}
pathPart = decodeURIComponent(url.pathname);
pathPart = decodeURIComponent(url.pathname.replace(/\+/g, " "));

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

sed -n '220,255p' frontend/app/view/term/osc-handlers.ts
node - <<'JS'
const cases = [
  'file:///tmp/C++',
  'file:///tmp/a+b',
  'file:///tmp/a%2Bb',
  'file:///tmp/a%20b',
  'file://localhost/tmp/a+b',
];
for (const u of cases) {
  const url = new URL(u);
  console.log('\n' + u);
  console.log('pathname:', url.pathname);
  console.log('decodeURIComponent(pathname):', decodeURIComponent(url.pathname));
  console.log('replace+decode:', decodeURIComponent(url.pathname.replace(/\+/g, ' ')));
}
JS

Repository: wavetermdev/waveterm

Length of output: 1970


🏁 Script executed:

rg -n "cmd:cwd|OSC 7|handleOsc7Command|preview:treemode" frontend/app -S

Repository: wavetermdev/waveterm

Length of output: 4818


Don't treat + as a space in file: paths. + is a literal path character here, so this will mangle valid cwd values like /tmp/C++ or /tmp/a+b before they’re stored in cmd:cwd, causing the preview/search widgets to open the wrong directory. Keep percent-decoding only and handle any legacy +-as-space producer at the source.

🤖 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 `@frontend/app/view/term/osc-handlers.ts` at line 241, The path parsing in the
`osc-handlers` flow is incorrectly converting `+` to a space before decoding,
which breaks valid `file:` paths like `cmd:cwd` values containing literal plus
signs. Update the `pathPart` handling in the relevant
`decodeURIComponent(url.pathname...)` logic to preserve `+` as-is and rely on
percent-decoding only, so `cmd:cwd` stores the exact directory and the
preview/search widgets open the correct location.

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