OpenProgram Docs

Bookmark Manager Implementation Plan#

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Make saved web bookmarks discoverable and manageable from the persistent right sidebar, with immediate synchronization and split/full-tab opening.

Architecture: Keep openprogram.bookmarks local storage and its existing change event as the sole data source. Add one title-renaming operation, one focused BookmarksPanel, and one right-dock view. The panel uses the split store interface from the desktop-web-split plan instead of creating its own navigation state.

Tech Stack: TypeScript, React 18, Zustand 5, Lucide icons, CSS, Node assertion checks.

Global Constraints#

  • Preserve the toolbar star and new-tab bookmark shortcuts.
  • Add search across title and URL, inline title rename, split open, full-tab open, delete, empty state, and no-results state.
  • Renaming trims the title, preserves order and URL, falls back to URL for an empty title, and emits openprogram:bookmarks-changed only after successful storage.
  • Do not add folders, drag sorting, server synchronization, favicon fetching, import/export, a backend schema, or a dependency.
  • When a session and split layout are available, normal bookmark activation opens beside chat and collapses the right panel; explicit full-tab activation always uses openWebTab.

Task 1: Bookmark rename operation#

Files:

  • Modify: web/lib/bookmarks.ts
  • Modify: web/scripts/check-bookmarks.mjs

Interfaces:

  • Produces: renameBookmark(url: string, title: string): Bookmark[].

  • Step 1: Write failing data checks

Extend check-bookmarks.mjs:

assert.deepEqual(
  bookmarks.renameBookmark(second.url, "  Renamed  "),
  [{ title: "Renamed", url: second.url }],
);
assert.deepEqual(
  bookmarks.renameBookmark(second.url, "   "),
  [{ title: second.url, url: second.url }],
);
assert.deepEqual(
  bookmarks.renameBookmark("https://missing.example/", "Missing"),
  [{ title: second.url, url: second.url }],
);

Also assert exactly one change event per successful rename and none for a missing URL or failed storage write.

  • Step 2: Run and verify RED

Run: cd web && npm run check:bookmarks

Expected: failure because renameBookmark is undefined.

  • Step 3: Implement minimal rename
export function renameBookmark(url: string, title: string): Bookmark[] {
  const bookmarks = readBookmarks();
  const index = bookmarks.findIndex((bookmark) => bookmark.url === url);
  if (index < 0) return bookmarks;
  const next = bookmarks.map((bookmark, i) =>
    i === index ? { ...bookmark, title: title.trim() || bookmark.url } : bookmark,
  );
  return saveBookmarks(next);
}
  • Step 4: Run the bookmark check

Run: cd web && npm run check:bookmarks

Expected: bookmark storage checks passed.

  • Step 5: Commit
git add web/lib/bookmarks.ts web/scripts/check-bookmarks.mjs
git commit -m "feat(bookmarks): support title renaming"

Task 2: Persistent right-sidebar manager#

Files:

  • Create: web/components/right-sidebar/bookmarks-panel.tsx
  • Modify: web/components/right-sidebar/right-sidebar.tsx
  • Modify: web/app/styles/right-dock.css
  • Modify: web/scripts/check-bookmarks.mjs

Interfaces:

  • Consumes: readBookmarks, renameBookmark, removeBookmark, BOOKMARKS_CHANGE_EVENT, openWebTab, openWebTabInSplit, isDesktopSplitLayoutAvailable.

  • Produces: right-dock view id bookmarks.

  • Step 1: Add failing UI structure checks

Require bookmarks-panel.tsx to subscribe to BOOKMARKS_CHANGE_EVENT, filter with both bookmark.title.toLowerCase() and bookmark.url.toLowerCase(), call rename/remove functions, expose Open in full tab, and render distinct empty/no-results text. Require right-sidebar.tsx and right-dock.css to register data-view="bookmarks".

  • Step 2: Run and verify RED

Run: cd web && npm run check:bookmarks

Expected: failure because the manager file/view is absent.

  • Step 3: Implement the panel and right-dock entry

Create a focused client component with bookmarks, query, editingUrl, and draftTitle state. Refresh on the change event. Row title click calls split open only when the active center tab is a session, the desktop bridge exists, and split layout is available; otherwise it calls openWebTab. The explicit full-tab button always calls openWebTab. A successful split open calls setRightDockOpen(false).

const [bookmarks, setBookmarks] = useState<Bookmark[]>(readBookmarks);
const [query, setQuery] = useState("");
const [editingUrl, setEditingUrl] = useState<string | null>(null);
const [draftTitle, setDraftTitle] = useState("");
useEffect(() => {
  const refresh = () => setBookmarks(readBookmarks());
  window.addEventListener(BOOKMARKS_CHANGE_EVENT, refresh);
  return () => window.removeEventListener(BOOKMARKS_CHANGE_EVENT, refresh);
}, []);
const needle = query.trim().toLowerCase();
const filtered = bookmarks.filter((bookmark) =>
  !needle || bookmark.title.toLowerCase().includes(needle)
    || bookmark.url.toLowerCase().includes(needle),
);
function openBesideChat(url: string) {
  const tabs = useCenterTabs.getState();
  const active = tabs.tabs.find((tab) => tab.id === tabs.activeId);
  if (desktopBridge() && active?.kind === "session"
      && isDesktopSplitLayoutAvailable()) {
    tabs.openWebTabInSplit(url);
    useSessionStore.getState().setRightDockOpen(false);
    return;
  }
  tabs.openWebTab(url);
}

function saveRename(url: string) {
  setBookmarks(renameBookmark(url, draftTitle));
  setEditingUrl(null);
}
{bookmarks.length === 0 ? (
  <div className="bookmarks-empty">{text("No bookmarks yet", "还没有书签")}</div>
) : filtered.length === 0 ? (
  <div className="bookmarks-empty">{text("No matching bookmarks", "没有匹配的书签")}</div>
) : filtered.map((bookmark) => (
  <div className="bookmark-row" key={bookmark.url}>
    <button onClick={() => openBesideChat(bookmark.url)}>{bookmark.title}</button>
    <span>{bookmark.url}</span>
    <button onClick={() => useCenterTabs.getState().openWebTab(bookmark.url)}
      aria-label={text("Open in full tab", "在完整标签页中打开")} />
    <button onClick={() => removeBookmark(bookmark.url)}
      aria-label={text("Delete bookmark", "删除书签")} />
  </div>
))}

Add a Bookmark Lucide nav icon and label beside History and Files. Add the exact view selector:

.right-sidebar[data-view="bookmarks"] .right-view[data-view="bookmarks"] {
  display: flex;
}

Rows show title, URL, inline edit/save/cancel controls, full-tab action, and delete action. All icon-only buttons require localized title and aria-label.

  • Step 4: Run focused and aggregate web checks

Run:

cd web
npm run check:bookmarks
npm run check:web-split
npm run check
npm run build

Expected: all exit 0; build reports successful compilation.

  • Step 5: Commit
git add web/components/right-sidebar/bookmarks-panel.tsx web/components/right-sidebar/right-sidebar.tsx web/app/styles/right-dock.css web/scripts/check-bookmarks.mjs
git commit -m "feat(bookmarks): add right-sidebar manager"

Task 3: Live desktop acceptance#

Files:

  • Modify only if a live failure requires a focused fix and matching regression assertion.

Interfaces:

  • Verifies the completed split and bookmark plans together.

  • Step 1: Start the development desktop app

Use the repository's existing development launcher and CDP port. Confirm the renderer loads the current working tree, not the previously packaged app.

  • Step 2: Verify split behavior with a real page

Open a session, open https://example.com/, enter split mode, drag the divider, switch sessions, select the web tab full-width, and return to chat. Confirm the page does not reload and the split ratio restores.

  • Step 3: Verify model control and occlusion

Use the existing app browser tool with engine=app to obtain the right-page target and perform one visible action. Open an attachment preview or Radix dialog and confirm the native page receives zero bounds and does not cover the overlay.

  • Step 4: Verify the bookmark workflow

Add a bookmark from the web toolbar, open Bookmarks from the right rail, search by title and URL, rename it, open it in split and full-tab modes, delete it, and reload the app. Confirm every surface updates immediately and deletion persists.

  • Step 5: Run final automated verification and commit any focused fixes

Run:

cd web && npm run check && npm run build
cd .. && node desktop/scripts/check-webtab-navigation.js
python -m pytest tests/unit/test_webtab_control.py -q
git diff --check

Expected: every command exits 0. If live verification required code changes, first add a failing assertion to the nearest existing check, implement the minimal fix, rerun the covering check, and commit with a scope-specific message.

Last updated · 2026-08-13