Skip to content

File explorer bookmarks (folder / file / document-position)#3443

Open
dandacompany wants to merge 12 commits into
wavetermdev:mainfrom
dandacompany:file-explorer-bookmarks
Open

File explorer bookmarks (folder / file / document-position)#3443
dandacompany wants to merge 12 commits into
wavetermdev:mainfrom
dandacompany:file-explorer-bookmarks

Conversation

@dandacompany

Copy link
Copy Markdown

What

Adds a bookmarks feature to the file preview/explorer: save and quickly jump to folders, files, or a specific position inside a document.

  • Folder / file bookmarks — jump straight to a directory or open a file.
  • Document-position bookmarks — remember a spot inside a document: a Markdown heading anchor, or a line number in a code/text file, and restore it when the bookmark is opened.
  • Header star (★) dropdown on the preview block: lists bookmarks (click to open), plus Add current location (context-aware: folder / file / doc-position) and Edit bookmarks….
  • Add to Bookmarks entry in the directory right-click menu.
  • Edit modal: rename, reorder, delete.

Bookmarks are stored globally in a new filebookmarks.json config collection, following the existing WebBookmark / map + display:order convention. This also replaces the hard-coded BOOKMARKS array in preview-model.tsx (the // TODO drive this using config) with config-driven defaults.

Why

The preview file explorer had a hard-coded folder-shortcut list and no way for users to save their own locations. This puts frequently-used folders, files, and document positions one click away.

How

  • Backend (pkg/wconfig, pkg/wshrpc): FileBookmark type + filebookmarks.json collection; RPCs SetFileBookmarkCommand / DeleteFileBookmarkCommand.
  • Frontend: BookmarksModel singleton + derived fileBookmarksAtom; PreviewModel gains openBookmark / addCurrentLocationBookmark / showBookmarkMenu and a star button in endIconButtons; document-position capture/restore via an imperative MarkdownHandle (heading slug) and the Monaco editor line; an edit modal registered in the modal registry.
  • Document-position uses the stable Markdown heading slug (rehype-slug) / Monaco line number rather than a pixel scroll offset, so it survives edits and window resizing.

Also included is a small related fix: preserve Markdown preview scroll position across refresh. The preview was defining ScrollableMarkdown/NonScrollableMarkdown inside render, which remounted the OverlayScrollbars container every render and reset scroll to the top; this inlines them and restores the viewport scroll after content updates.

Testing

  • Go unit tests for the config helpers (pkg/wconfig).
  • Vitest for the pure logic: BookmarksModel ordering, pickCurrentAnchor, and the menu builder.
  • Manual: add / open / edit bookmarks for folders, files, and document positions on macOS, including cross-file heading/line restore.

Notes

  • TS bindings were regenerated with task generate (no manual edits to generated files).
  • Happy to sign the CLA and to split the scroll-position fix out or adjust scope per maintainer feedback.

dandacompany and others added 12 commits July 23, 2026 10:52
Inline the markdown content so OverlayScrollbars no longer remounts every
render, track viewport scrollTop via the scroll event, and restore it in a
layout effect after content updates so refreshing an edited document keeps
the reader's position instead of jumping to the top.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the preview mock's FullConfigType literal after the new required
filebookmarks field (and pre-existing version/buildtime gaps) so the mock
type-checks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The change adds persisted file bookmarks with default configuration, Go configuration helpers, RPC handlers, generated clients, and frontend RPC wrappers. The frontend adds sorted bookmark state, CRUD/reorder operations, context menus, an edit modal, directory actions, and preview integration. Markdown and editor previews capture and restore document locations, while Markdown gains imperative anchor scrolling, current-anchor detection, and scroll preservation. Unit tests cover bookmark ordering, menu helpers, configuration updates, and anchor selection.

Estimated code review effort: 4 (Complex) | ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% 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 file explorer bookmarks for folders, files, and document positions.
Description check ✅ Passed The description matches the PR and covers the bookmark feature, config storage, UI changes, backend RPCs, tests, and scroll preservation.
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

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Checkov (3.3.8)
pkg/wconfig/defaultconfig/filebookmarks.json

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'

🔧 Biome (2.5.3)
frontend/types/gotypes.d.ts

File contains syntax errors that prevent linting: Line 1898: Expected a property, or a signature but instead found '#'.; Line 1898: Expected an expression, or an assignment but instead found ':'.; Line 1898: Expected an expression but instead found ']'.; Line 2211: Expected a statement but instead found '}'.


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: 3

🤖 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/store/bookmarksmodel.ts`:
- Around line 79-95: Update reorder in the bookmark model to persist the entire
ordering through a single transactional or bulk reorder RPC instead of issuing
one SetFileBookmarkCommand per bookmark. Preserve the orderedKeys mapping and
display:order values, and ensure a failed operation leaves all bookmark orders
unchanged.

In `@frontend/app/view/preview/preview-model.tsx`:
- Around line 604-614: Update openBookmark so cross-connection bookmark opens
use the same goHistory navigation path as same-connection opens, while still
applying the required connection and file metadata updates. Ensure the forward
history is reset or updated consistently so both bookmark-opening branches
participate in back/forward navigation.

In `@pkg/wconfig/settingsconfig.go`:
- Around line 938-956: Serialize the full read-modify-write transaction in
SetFileBookmarkConfigValue and DeleteFileBookmarkConfigValue, rather than
locking only through WriteWaveHomeConfigFile. Add or reuse an atomic
fileBookmark update helper that holds the shared lock across
ReadWaveHomeConfigFile, UpsertFileBookmarkInMap or RemoveFileBookmarkInMap, and
the write, then route both functions through it.
🪄 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 Plus

Run ID: 87be89a8-9a5f-4cdd-a13e-614371a65814

📥 Commits

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

📒 Files selected for processing (22)
  • frontend/app/element/markdown-anchor.test.ts
  • frontend/app/element/markdown-anchor.ts
  • frontend/app/element/markdown.tsx
  • frontend/app/modals/modalregistry.tsx
  • frontend/app/store/bookmarksmodel.test.ts
  • frontend/app/store/bookmarksmodel.ts
  • frontend/app/store/wshclientapi.ts
  • frontend/app/view/preview/bookmarks-edit-modal.tsx
  • frontend/app/view/preview/bookmarks-menu.test.ts
  • frontend/app/view/preview/bookmarks-menu.ts
  • frontend/app/view/preview/preview-directory.tsx
  • frontend/app/view/preview/preview-edit.tsx
  • frontend/app/view/preview/preview-markdown.tsx
  • frontend/app/view/preview/preview-model.tsx
  • frontend/preview/mock/defaultconfig.ts
  • frontend/types/gotypes.d.ts
  • pkg/wconfig/defaultconfig/filebookmarks.json
  • pkg/wconfig/filebookmarks_test.go
  • pkg/wconfig/settingsconfig.go
  • pkg/wshrpc/wshclient/wshclient.go
  • pkg/wshrpc/wshrpctypes.go
  • pkg/wshrpc/wshserver/wshserver.go

Comment on lines +79 to +95
async reorder(orderedKeys: string[]): Promise<void> {
const byKey = new Map(this.getBookmarks().map((b) => [b.key, b]));
for (let i = 0; i < orderedKeys.length; i++) {
const b = byKey.get(orderedKeys[i]);
if (b == null) {
continue;
}
const newOrder = i + 1;
if ((b["display:order"] ?? 0) == newOrder) {
continue;
}
const { key: _omit, ...rest } = b;
await RpcApi.SetFileBookmarkCommand(TabRpcClient, {
key: b.key,
bookmark: { ...rest, "display:order": newOrder },
});
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make reorder persistence atomic.

This loop issues one full bookmark update per item. If a later RPC fails, earlier order changes remain persisted, leaving a partially reordered list. Use a transactional/bulk reorder operation rather than independent SetFileBookmarkCommand calls.

🤖 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/store/bookmarksmodel.ts` around lines 79 - 95, Update reorder in
the bookmark model to persist the entire ordering through a single transactional
or bulk reorder RPC instead of issuing one SetFileBookmarkCommand per bookmark.
Preserve the orderedKeys mapping and display:order values, and ensure a failed
operation leaves all bookmark orders unchanged.

Comment on lines +604 to +614
async openBookmark(bm: BookmarkEntry) {
const currentConn = globalStore.get(this.blockAtom)?.meta?.connection ?? "";
const bmConn = bm.connection ?? "";
if (bmConn != currentConn) {
const blockOref = WOS.makeORef("block", this.blockId);
await this.env.services.object.UpdateObjectMeta(blockOref, { connection: bmConn, file: bm.path });
globalStore.set(this.fileContentSaved, null);
globalStore.set(this.newFileContent, null);
} else {
await this.goHistory(bm.path);
}

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

🏁 Script executed:

#!/bin/bash
# Inspect goHistory/goHistoryBack/goHistoryForward implementations to see what meta fields they manage.
rg -n -A 20 'function goHistory\b' --type=ts
rg -n -A 20 'function goHistoryBack\b' --type=ts

Repository: wavetermdev/waveterm

Length of output: 158


🏁 Script executed:

#!/bin/bash
set -u

echo "Tracked files matching preview-model:"
git ls-files | rg 'preview-model\.tsx$' || true

echo
echo "Search history-related identifiers:"
rg -n 'goHistory|history|goHistoryBack|goHistoryForward|Cmd.*Arrow|ArrowLeft|ArrowRight|OpenBookmark|openBookmark' --type ts --type tsx .

Repository: wavetermdev/waveterm

Length of output: 309


🏁 Script executed:

#!/bin/bash
set -u

echo "Locate preview-model.tsx:"
git ls-files | rg 'frontend/app/view/preview/preview-model\.tsx$' || true

echo
echo "Search history-related identifiers:"
rg -n 'goHistory|history|goHistoryBack|goHistoryForward|Cmd.*Arrow|ArrowLeft|ArrowRight|OpenBookmark|openBookmark' frontend/app/view/preview/preview-model.tsx frontend/app --glob '*.ts' --glob '*.tsx' || true

echo
echo "Show surrounding preview-model section:"
sed -n '560,640p' frontend/app/view/preview/preview-model.tsx

Repository: wavetermdev/waveterm

Length of output: 9658


🏁 Script executed:

#!/bin/bash
set -u

echo "Find historyutil source:"
git ls-files | rg 'historyutil\.(ts|tsx)$' || true

echo
echo "historyutil implementation:"
sed -n '1,260p' frontend/app/util/historyutil.ts

echo
echo "Preview model top and relevant history helpers:"
sed -n '1,80p' frontend/app/view/preview/preview-model.tsx
sed -n '860,915p' frontend/app/view/preview/preview-model.tsx

Repository: wavetermdev/waveterm

Length of output: 5222


🏁 Script executed:

#!/bin/bash
set -u

echo "Locate historyutil:"
git ls-files | rg '(^|/)historyutil\.(ts|tsx)$' || true

echo
FOUND=$(git ls-files | rg 'historyutil\.(ts|tsx)$' | head -n 1)
if [ -n "$FOUND" ]; then
  echo "historyutil implementation:"
  sed -n '1,260p' "$FOUND"

  echo
  echo "bookmark connection/path metadata usages:"
  rg -n 'bookmark(type|type)?|connection|bookmarks|OpenBookmark|openBookmark|Cmd:ArrowLeft|Cmd:ArrowRight' frontend --glob '*.ts' --glob '*.tsx' || true
fi

Repository: wavetermdev/waveterm

Length of output: 29625


Treat cross-connection bookmark opens the same way as normal navigation

Same-connection bookmark opens are pushed onto /forward history by goHistory, but cross-connection opens update block meta with only { connection, file }. Update the connection/path via the same goHistory path (or reset history:forward explicitly) so back/forward navigation doesn’t ignore cross-connection bookmark opens.

🤖 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 604 - 614, Update
openBookmark so cross-connection bookmark opens use the same goHistory
navigation path as same-connection opens, while still applying the required
connection and file metadata updates. Ensure the forward history is reset or
updated consistently so both bookmark-opening branches participate in
back/forward navigation.

Comment on lines +938 to +956
func SetFileBookmarkConfigValue(key string, bookmark FileBookmark) error {
m, cerrs := ReadWaveHomeConfigFile(FileBookmarksFile)
if len(cerrs) > 0 {
return fmt.Errorf("error reading config file: %v", cerrs[0])
}
m, err := UpsertFileBookmarkInMap(m, key, bookmark)
if err != nil {
return err
}
return WriteWaveHomeConfigFile(FileBookmarksFile, m)
}

func DeleteFileBookmarkConfigValue(key string) error {
m, cerrs := ReadWaveHomeConfigFile(FileBookmarksFile)
if len(cerrs) > 0 {
return fmt.Errorf("error reading config file: %v", cerrs[0])
}
m = RemoveFileBookmarkInMap(m, key)
return WriteWaveHomeConfigFile(FileBookmarksFile, m)

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether bookmark persistence has transaction-level serialization.
rg -n -C 8 'func (ReadWaveHomeConfigFile|WriteWaveHomeConfigFile|SetFileBookmarkConfigValue|DeleteFileBookmarkConfigValue)' pkg/wconfig/settingsconfig.go
rg -n -C 5 'SetFileBookmarkCommand|DeleteFileBookmarkCommand' pkg/wshrpc/wshserver/wshserver.go
rg -n -i 'mutex|rwmutex|flock|lock' pkg/wconfig/settingsconfig.go

Repository: wavetermdev/waveterm

Length of output: 3628


🏁 Script executed:

#!/bin/bash
# Inspect the full read/write/filebookmarks helpers and call graph/lock types.
set -eu
sed -n '1,80p' pkg/wconfig/settingsconfig.go
sed -n '520,615p' pkg/wconfig/settingsconfig.go
sed -n '900,960p' pkg/wconfig/settingsconfig.go
rg -n 'type .*File.*Bookmark|var .*Bookmark|func .*FileBookmark|DeleteFileBookmark|SetFileBookmark|UpsertFileBookmarkInMap|RemoveFileBookmarkInMap|FileBookmarksFile' pkg

Repository: wavetermdev/waveterm

Length of output: 10888


Serialize the full fileBookmark RMW transaction.

WriteWaveHomeConfigFile locks only the write step, while SetFileBookmarkConfigValue/DeleteFileBookmarkConfigValue read, mutate, and then write filebookmarks.json separately. Concurrent add/reorder/delete RPCs can interleave and overwrite each other; hold a shared lock/read-modify-write guard around the entire operation with an atomic update helper.

🤖 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/wconfig/settingsconfig.go` around lines 938 - 956, Serialize the full
read-modify-write transaction in SetFileBookmarkConfigValue and
DeleteFileBookmarkConfigValue, rather than locking only through
WriteWaveHomeConfigFile. Add or reuse an atomic fileBookmark update helper that
holds the shared lock across ReadWaveHomeConfigFile, UpsertFileBookmarkInMap or
RemoveFileBookmarkInMap, and the write, then route both functions through it.

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.

1 participant