Getting it into your agent
One page per mod, every tool's command on it. A separate URL per tool would split the same page into five that compete with each other.
npx agentmods add agents/windviki/vbookmarks/modulesgit clone --depth 1 https://github.com/windviki/vBookmarksWhat it costs to keep this loaded
Counted locally with the o200k_base tokenizer, which is exact for GPT models; Claude uses its own tokenizer and its counts differ. Treat this as one consistent yardstick across the catalogue rather than a bill. Prices are per million input tokens.
| Model | Per session | Once invoked |
|---|---|---|
| Fable 5 | $0.00000 | $0.41115 |
| Opus 5 | $0.00000 | $0.20558 |
| Sonnet 5 | $0.00000 | $0.08223 |
| Haiku 4.5 | $0.00000 | $0.04111 |
Grade A, and why
modules scanned grade A with 0 findings against 26 rules in 11 categories — prompt injection, anti-refusal, data exfiltration, privilege escalation, supply chain, agent snooping, system-prompt leakage, SSRF and excessive agency — measured yesterday.
A static scan of the body, not an audit. Every finding is printed with the line that produced it so you can judge whether it matters here. A mod is markdown that instructs an agent; that is exactly why what it instructs is worth reading.
Nothing flagged
None of the 26 patterns this scan looks for appear in this file: no shell pipes, no recursive deletes, no credential paths, no hidden text, no instruction-override or anti-refusal phrasing, no agent-config snooping. That is not a guarantee, it is the absence of the things that are checkable.
How it starts
The opening of the file, as written. The whole thing — 94 lines — stays where its author put it; the contents beside it link to each section on GitHub.
Module Reference (per-module detail)
Extracted verbatim from
AGENTS.md— the detailed half of its "Repository Layout" section.AGENTS.mdkeeps the orientation summary; consult this file whenever a task touches a specific module. Keep both in sync: when a module row changes, update it HERE (this is the canonical detail layer) and touchAGENTS.mdonly if the grouping/summary changed.
Repository Layout (detail)
Runtime code is grouped by kind: first-party JS in src/, extension pages in pages/, styles in css/, vendored third-party code in vendor/, images in assets/ (this is also the layout of the shipped extension — page references use root-absolute paths like /src/neat.js). manifest.json and _locales/ must stay at the extension root (Chrome requirement).
| File(s) | Role |
|---|---|
manifest.json |
MV3 manifest (minimum_chrome_version 114): src/background.js service worker (module), pages/popup.html action popup, pages/sidepanel.html side panel page (opt-in via the openInSidePanel setting; side_panel.default_path must not contain a query string — Chrome rejects it at install), pages/options.html options page, omnibox keyword *, permissions bookmarks, tabs, favicon, storage, scripting, sidePanel, contextMenus, tabGroups, alarms, clipboardWrite, proxy (install-time — Chrome refuses proxy as optional; inert unless a dead-scan proxy server is configured, see src/dead-proxy.js), host permissions <all_urls>, optional permission history (requested at runtime inside a user gesture — see src/view-stats.js) |
src/background.js |
Service worker (ES module). Omnibox search (debounced 250 ms chrome.bookmarks.search, suggestion rendering, sync-status glyphs; ranking/highlight helpers imported from ./search-core.js) plus side panel management: the behavior derivation lives in src/panel-behavior.js (see its row; minimum_chrome_version is 114, so chrome.sidePanel/storage.session need no feature detection), and the open-side-panel command (Alt+Shift+B) stamps sidePanelIsOpen + sidePanelHeartbeat in storage.session and opens the panel. The open-command-palette command (Ctrl/Cmd+Shift+K, P2) sets a storage.session pendingPaletteOpen flag and opens the popup via chrome.action.openPopup (Chrome 127+), falling back to a ?palette=1 popup window. The quick-add-bookmark command (Alt+Shift+S, final polish) bookmarks the active tab straight into quickAddFolderId (default '1'). Also owns the vbm-quick-add page context menu (Phase 3, issue #30): created on install and at every SW startup (remove-then-create for idempotence; the remove→create chain is serialized so overlapping cycles — startup × onInstalled, or a storage flip mid-cycle — collapse into one create instead of raising a duplicate-id lastError), click saves the page into quickAddFolderId (default '1'); the entry is gated by the quickAddContextMenu setting (issue #49, default on — a chrome.storage.onChanged listener reads changes.quickAddContextMenu.newValue and adds/removes the menu live, no SW restart needed; the setting lives in the sync area since the 2026-08 storage audit, so the SW reads chrome.storage.sync with a local fallback for not-yet-migrated profiles — the local→sync migration runs page-side, so the first SW start after an upgrade must still honor the pre-migration local value — and the listener accepts both areas). Also starts the sync engine (P3.6): createSyncEngine().start() at top level so every SW cold start re-hooks the listeners. v4 task-2 slice E adds the visit-stats SW collector (createVisitStatsCollector().start() from src/visit-stats-sw.js): chrome.tabs.onUpdated URL navigations matched exactly against a bookmark-URL index bump the same visitStats dataset the popup writes — deduped against popup-initiated opens via a 10 s vbmPopupOpens marker in chrome.storage.session. Also the dead-scan proxy sweep (dead-proxy.js): after the cold-start resume check decides no live run was resumed, when no vbmProxySession storage.session marker exists, a best-effort chrome.proxy.settings.clear sweeps marker-PAC residue a crashed popup could leave behind (namespace-guarded — chrome.proxy is available because proxy is an install-time permission, Chrome refuses it as optional; the PAC only proxies marker-tagged probe URLs, so residue is benign). v4 task-4 #16 adds the dead-scan runner (createDeadScanRunner() from src/dead-scan-sw.js, started top level — pages message it, it publishes the vbmDeadScan live blob and resumes mid-run scans after an SW cold start; see its row) and #11 hardened the omnibox onInputEntered fallback (a fast Enter ahead of the 250 ms debounce no longer pushes the raw query into tabs.update — explicit URLs open directly, anything else falls back to a bookmarks.search + rankBookmarks top hit). The SW also restores the custom action icon on every cold start (issue #52 — chrome.action.setIcon is session-scoped): top level + onStartup/onInstalled read the stored customIcon 19×19 RGBA JSON and rebuild the ImageData via OffscreenCanvas, so a browser restart no longer falls back to the manifest icon until the popup is opened. The SW glue is unit-tested by tests/background.test.js (one import-time chrome double speaking both callback and promise storage styles) |
src/panel-behavior.js |
Side-panel action behavior (round-6 extraction; v4 task-3 #19 liveness fix): derives chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick }) at every SW startup and on storage changes from openInSidePanel (option on → always toggle mode) OR a LIVE panel (option off → toggle mode so the next icon click closes a panel opened via Chrome's native entries). Liveness = the storage.session sidePanelIsOpen marker plus a fresh sidePanelHeartbeat — the panel page beats every PANEL_HEARTBEAT_MS (20 s, src/popup.js), PANEL_STALE_MS (90 s) is the grace window; a bare marker is crash/session-restore residue and gets removed on read, fixing "the icon keeps toggling the side panel with the option off". 4.0.1 adds the event-driven death signal: the panel page holds a vbm-panel runtime port; when Chrome destroys it (an action-toggle close — pagehide is not guaranteed, and the page's own async reset can be dropped mid-teardown; sidePanel.onClosed does NOT exist in the API) the port disconnects and the SW immediately restores popup mode + clears the stale marker when the option is off — so the next icon click opens the popup instead of re-toggling the panel. The gated liveness alarm runs at 20 s (< the 30 s SW idle timeout) so the SW stays alive to receive the disconnect and as a no-port safety net. Unit-tested by tests/panel-behavior.test.js |
src/search-core.js |
Pure search helpers shared by src/background.js and the vitest suites: rankBookmarks, xmlEncode, matcher (no chrome.* references). 4.0.5: rankBookmarks delegates its ranking to src/fuzzy-core.js, so the omnibox and the popup run ONE fzf-style implementation — only the candidate sets differ (the omnibox ranks the chrome.bookmarks.search word-match results, sliced to 6; the popup ranks the whole-tree flat index) |
src/escape.js |
Shared HTML escaping (4.0.5 consolidation), pure ESM: htmlspecialchars — the single source of truth for what used to be nine verbatim module-private copies (tree-render / dialogs / palette / search / view-recent / view-stats / view-dupes / view-dead / palette-commands all import it now). Escapes & (chain head), <, >, ". An earlier revision left & unescaped for idempotence, but a full caller audit (all 9 importers + every highlightTitlePositions upstream) proved no double-feed path survives — every input is raw data (bookmark titles/URLs, user input, _m() messages, settings), and highlightTitlePositions escapes char-by-char from the raw title — so & was added (completeness over idempotence); dialogs.js's widont inserts AFTER escaping and is unaffected |
src/separators.js |
Separator logic (P1 first module extracted from src/neat.js), pure ESM with zero chrome.*/DOM references: StringList, isBlank, SeparatorManager (storage mirror injected via constructor — directly unit-tested by tests/separators.test.js) |
src/dialogs.js |
Popup dialogs (P1 module): alert/confirm/edit/new-folder/sort dialogs, the 4.0.1 tab-group title/color + existing-group picker dialogs, the 4.0.8 VersionDialog (the palette /version command's metadata card: a <dl> of extension/announce/browser/OS/channel/language/UA rows, a copy-as-JSON button with clipboard-API → execCommand fallback, a palette-style Esc footer bar), the 4.1.0 CopyMoveDialog (the tab-groups view's copy-vs-move choice for grouping already-grouped tabs) + BookmarkFolderPickDialog (its add-to-bookmark-folder picker) + #cover/Escape close handling + global error alert. initDialogs(ctx) wires DOM after parse (ctx.onSort = folder reindex); pure widont exported. The returned object gains activeEl() (final polish) — the open dialog's element, backing keyboard.js's modal Tab trap; the ten dialog divs carry aria-modal="true". Unit-tested by tests/dialogs.test.js (DOM stub + real-module import); 2026-08: BookmarkFolderPickDialog.open accepts excludeIds (the folder move/copy cycle guard — banned rows and chips hide while the pin/recent rosters survive unpruned, and a later open without the option resets it) |
src/actions.js |
Popup action table (P1 module): all 14 bookmark/folder actions (open in tab/window/incognito, open-all as a color-coded tab group (P3.4 — since 5df7631 the open+group pipeline runs in the service worker, src/tab-groups-sw.js, so a closing popup can no longer drop the pending group; plus an "…and set name/color" dialog and an open-into-existing-group variant, sharing src/tab-group-utils.js), bulk-open with 10-item confirm threshold, edit via dialogs, delete via ctx.undo.capture + toast instead of ConfirmDialog, focus handoff, add-bookmark/folder/separator mutations (adding into a collapsed folder expands it and re-renders its children so the new node is immediately visible), copy title+URL via navigator.clipboard.writeText (the clipboardWrite permission; execCommand('copy') fallback — the old execCommand-only path was silently rejected in async callbacks), replace-URL, bookmarklet __VBM_CURRENT_TAB_URL__ substitution). initActions(ctx) receives store/dialogs/search/separatorManager/undo/HTML builders incl. generateHTML; tested by tests/actions.test.js (DOM stub + chrome API doubles) — the velvet-staging clipboard family lives here too: setClipBookmark/cancelClipBookmark/hasClipBookmark/hasCutClipboard (the session single-item clipboard), pasteClipBookmarkInto (folder-end paste), pasteClipBookmarkAfter (2026-08: next-sibling paste — a same-parent cut corrects the index for Chrome's remove-then-insert move semantics; an already-next-sibling cut is a clearing no-op), copyMoveBookmarkTo and copyMoveFolderTo (2026-08: the folder-row picker — passes its own subtree as excludeIds, move is a plain bookmarks.move, copy recursively clones via the private copyFolderTree, separators included as plain URL nodes) |
src/context-menu.js |
Popup context menus (P1 module): bookmark/folder/separator right-click menus, item wiring to actions/dialogs, switchBookmarkMenu visibility rules, private currentContext. v4 task-3 adds the slim view-row menus and the positional-item rule: stats-view history rows without a bookmark id get the hist-row menu (open×3 via the row href + bookmark-it via the row's own ☆ button, #10), dupes GROUP HEADS get their own menu (apply-dedup with a live keeper/doomed-count label + expand/collapse, #16), and out-of-tree views hide the position-dependent items (add-*-before/after etc. via POSITIONAL_IDS — invisible tree structure makes them meaningless, #11). initContextMenu(ctx) returns { clearMenu(e?), closeMenu(), switchBookmarkMenu(disable), bookmarkMenu, folderMenu, separatorMenu, searchHistoryMenu, histRowMenu, dupesGroupMenu, tabRowMenu, tabGroupMenu, tabClosedMenu, tabClosedTabMenu (the four 4.1.0 tab-groups menus), folderTabGroupSubmenu, folderSortSubmenu, bookmarkTabGroupSubmenu, openSubmenuFor, closeSubmenu, toggleSubmenuFor, submenuOpen } (the last ten may be absent in minimal test setups — consumers null-check; closeMenu is the keyboard layer's cancel path) — 4.0.1 focus law: every menu-item dispatch closes the menu FIRST and refocuses the owning row (same-id replacement / list container fallbacks via refocusOwner) before the action runs; the open-time positioning clamps the menu to the popup viewport (issue #48: a menu taller than the space below the search bar — the 19-entry folder menu at Windows 150% scaling / page zoom ≥ ~90% — used to make menu.focus() scroll the document, and that scroll fired the scroll-dismiss listeners, closing the menu the instant it opened; now the menu gets max-height + internal overflow-y: auto, chrome-adjusted for its padding/border, so it always fits and focus() never scrolls the page; the real-browser gate is scripts/harness/verify-menu-overflow.js). issue #48 follow-up: the tab-group (folder + bookmark menus) and sort (folder menu) blocks can collapse into single "Tab groups ▸" / "Sort ▸" entries (settings collapseTabGroupMenu default off, collapseSortMenu default on) whose three items live in body-level sibling <menu class="submenu"> flyouts (sub--prefixed ids, normalized to the parent-menu ids at dispatch; positionMenu gained an entry flyout mode — anchored to the entry, flipped on horizontal overflow, same #48 clamp, and stacked below the entry when neither side fits; at extreme zoom/resolution the menu's max-width is capped to the VIEWPORT (the popup body can outgrow the window) and both axes clamp to window.innerWidth; 4.1.0 audit: the clamps are scroll-offset aware ([scrollX, scrollX+innerWidth] in document coordinates — the popup body can sit pre-scrolled a couple of px at extreme zoom from startup focus churn, and a viewport-relative clamp parked the menu that far outside the visible band, the menu-extreme rect.l=-2 failures), and menus focus with preventScroll: true since a viewport-clamped menu never needs a focus scroll; the folder tab-group entry/submenu reads disabled when the folder has no bookmark children; hover/click/→/Enter open, ←/Esc close, two-level document Esc; the real-browser gates are scripts/harness/verify-menu-collapse.js and the DPR×zoom×size sweep scripts/harness/verify-menu-extreme.js); initialized first in src/neat.js because search needs switchBookmarkMenu at init. 4.0.4 (a42efea + 8eb9e97) adds the folder menu's content-dependent greying via applyContentDisabled: an async getChildren read (guarded — a deleted/ghost folder calls back with undefined + lastError) toggles disabled on the open*/tab-group entries when the folder has no URL children and on the sort entries when it has no children at all, so an empty folder's menu offers only the add-type entries; the same no-URL rule greys the collapsed tab-group entry + its submenu. 4.0.5: the link-folder branch (folder rows in search results / the palette) goes through applyContentDisabled too, and hideAllMenus resets the content-disabled state (OPEN/SORT content ids + the collapsed tab-group entry) via clearMenu on every open, so a greyed state never leaks across menus or branches. 4.0.8 adds the view-tab menu: right-clicking a .view-tab (or ContextMenu/Shift+F10 on it) opens #view-tab-context-menu (Hide/Disable — labels + availability from ctx.viewMenu.prepare(viewId), view-manager's viewMenuState; dispatch to hideViewTab/disableView), and the branch marks the tab .active like a row so the document Esc layer dismisses the menu instead of falling through to window.close(). The return list gains viewTabMenu. 4.1.0 adds the tab-groups view's four menus — tab row, group head, closed group, closed tab — wired to the same dispatch/focus law. 2026-08 clipboard/menu fix batch: collapseAddFolderMenu is wired in neat.js (it never was — the class sat permanently on) and now folds the BOOKMARK menu's add-folder pair too (bookmark-add-collapse + bookmark-add-submenu; the folder flyout entry must stay free of inline display:none — CSS alone decides); setPositionalItems writes '' (not 'block') for the visible case so the collapse CSS can still win; the bookmark menu gains paste-here-bookmark (tree-only, clipboard-gated → actions.pasteClipBookmarkAfter, insert as the target's next sibling) and the folder menu gains folder-copy-move-to (root-disabled like folder-edit → actions.copyMoveFolderTo). Parked menus pin top:-999px (base CSS; parkMenu() clears the inline top on every hide) — a never-opened menu's static-position box is zoom-exempt and full-height, and one parked below the fold fed documentElement.scrollHeight (the B2 scrollbar-gate regression diag-b2-overflow.js isolates). Unit-tested by tests/context-menu.test.js 2026-08-26: the staging-group menu leads with the folder menu's open family verbatim (open-all / open-as-tab-group — titled by the group name / new window / incognito, dispatching ctx.staging.groupUrls() through the shared actions; greyed + separator hidden on an empty group), and the tab-row menu gains tab-row-new-window → tabGroupsMenu.moveTabToNewWindow |
src/keyboard.js |
Popup keyboard layer (P1 module): tree type-ahead buffer, treeKeyDown/treeKeyUp (arrow/Home/End/PageUp/PageDown/F2/Delete nav on $tree + search results), menu contextKeyDown (4.0.1: all SEVEN menus bound — the separator menu included, its lone entry was the keyboard-unreachable bug — ↑/↓ wrap on every platform, the macOS no-wrap exception deleted, Home/End first/last enabled item, →/Enter/Space execute + refocus the owning row, ←/Esc cancel + refocus, RTL mirrored, confirm on the bare container or a disabled item is a no-op; the toolbar rung ←/→ walk now wraps at the edges), and the document-level Escape (close dialogs / quit search) + Ctrl/Cmd+F handlers. issue #48 follow-up: contextKeyDown gains submenu branches — the three <menu class="submenu"> flyouts are bound too; on a has-submenu entry → opens the flyout and steps into its first walkable item while ←/Enter/Esc close it first then cancel the menu; inside a flyout ← closes only the flyout and refocuses its entry; the document-capture Esc is two-level (first Esc closes the flyout, second closes the menu). v4 task-3 #7 adds the document-level Tab/Shift+Tab zone cycler tabCycle (search input → visible header buttons → active view tab → the active view's .vbm-toolbar controls → the list's .focus/first row, backwards with Shift; yields to the palette/menus, and while a dialog is open traps Tab inside the dialog's own controls via dialogs.activeEl() — final polish) — list rows carry roving tabindex="-1" so each zone is a single Tab stop. 4.0.5: while the undo toast bar (#undo-toast) is visible its button joins the Tab ring as the last stop — a transient bar fixed to the bottom edge; the hidden attribute is the visibility signal and the 8 s auto-hide drops the stop again (never an arrow rung — the arrow chain stays stable either way). 4.0.8: the transient banners (#donation card, the local #whats-new strip, the remote #announce) join the Tab ring between the header row and the tab strip while visible — the announce banner's dismiss × is keyboard-reachable this way; hidden banners contribute no stop. The announce banner also joined the Esc banner rung (dismissed through its own ×, so the mark-seen once-semantics stay in announce.js). And tabCycle's row stop (view-manager's focusDefault carries the same guard) excludes a .focus marker parked inside a .vbm-dropdown-list listbox — a hidden option, not a row, so it is never a Tab stop (the 5421968 toolbar-↓ regression lesson, same defense). initKeyboard(ctx) receives tree/search/actions/menus/dialogs/body/os/rtl (all already initialized at its call site) and returns { treeKeyDown, treeKeyUp, contextKeyDown, tabCycle }. 4.0.8: the view-tab menu is bound like the other seven and joins the Tab-trap menuContainers; 4.1.0 binds the tab-groups view's four menus (tab row / group head / closed group / closed tab — twelve menus total) the same way, and its row walk skips the non-focusable window section rows; the view-tab strip answers ContextMenu/Shift+F10 with the same dispatch. The undo-toast Tab stop is skipped while the toast button is hidden (a buttonless toastAction hint contributes no stop). No neatools inside. Unit-tested by tests/keyboard.test.js |
src/dnd.js |
Popup drag & drop (P1 module): tree mousedown drag start (rejects root folders / data-virtual recent entries), document mousemove drop-target tracking with #bookmark-clone ghost + #drop-overlay insertion line, edge auto-scroll, mouseup drop → chrome.bookmarks.move with cross-storage guard (canMoveBetweenStorage blocks synced↔local moves and explains via an on-demand #notice-toast — the old alert() destroyed the popup window). initDnd(ctx) receives tree/store/rtl/resetSeparator; returns { isDragging(), consumeNoOpen() } — the click handler and zoom read drag state through these. No neatools inside. 2026-08-28: #bookmark-clone and #drop-overlay are position: fixed (css/neat.css) — as absolutely-positioned body children the clone swinging out at the popup edge EXTENDED the document's scrollable overflow (repro diag/diag-edge-drag.js: body scrollWidth 320→406 mid-drag), which in the real popup (viewport == body width) made the whole view horizontally scrollable: a wheel/trackpad pan mid-drag shifted the view left with a blank strip on the right. Fixed boxes never contribute to scrollable overflow; the JS coordinate math is unchanged (body sits at the viewport origin and never scrolls, so viewport coords == the old body-absolute coords, including the /zoomLevel conversions). Unit-tested by tests/dnd.test.js |
src/icons.js |
Inline SVG icon constants (P4 — retired the folder.png / document-code.png bitmaps): FOLDER_ICON, DOCUMENT_CODE_ICON, plus CHEVRON_ICON for folder twisties, VIEW_ICONS (v4 task-2: one 16px icon per view for the tab strip) and DEFAULT_BOOKMARK_ICON (4.0.2 — the no-favicon globe swapped in by src/favicon-fallback.js). Line-style per docs/现代化演进总方案.md (16px grid, 1.5px stroke, stroke="currentColor"), colored by CSS .vbm-icon-* rules; consumed as HTML strings by tree-render/search/palette row templates |
src/favicon-fallback.js |
Default-favicon fallback (4.0.2): Chrome's _favicon placeholder for favicon-less pages is a flat-gray bitmap that nearly vanishes on dark/ink (the 4.0.1 CSS brightness lift was partial). At init the module fetches the icon for a .invalid URL (always the placeholder) and fingerprints its pixels (hashPixels, FNV-1a over RGBA); a capture-phase load delegation on the document swaps any favicon <img> with the identical fingerprint for the currentColor DEFAULT_BOOKMARK_ICON. Verdicts cached per src; inert when _favicon/canvas unavailable. 4.0.5 adds the favicon contrast-invert service: from the SAME getImageData buffer the placeholder check already samples, contrastStats derives the fraction of opaque pixels on the dark extreme / the light extreme / the opaque coverage, and needsContrast flips a monochrome icon that would vanish on the theme's background — the verdict reads extreme-tone SHARES, not mean luminance+saturation (a mean is fooled by plate-style icons: x.com's white-X-on-black-plate averages "very dark", and flipping it would turn the elegant self-inverting design into a glaring white plate). On a dark background the flip needs dark > 0.55 with light < 0.05; on a light background light > 0.60 with dark < 0.15 AND colored < 0.30 — the colored guard (the fraction of opaque pixels with sat > 38) keeps a colorful logo like the Chrome WebStore devconsole icon (colored ≈ 0.42) from being inverted into a black-card-with-complementary-colors wreck on a light theme, while yabook's pure-white glyph (colored 0) still flips; mid-tone and two-tone icons fall between the guards and never flip. 4.1.0: the dark branch gained a STRICTER colored guard (< 0.10) — the flip's invert+hue-rotate halves a vivid mark's chroma (measured netflix red 190→90, a washed pastel users reported as a "weird filter"), while the saturated original reads fine on dark by chroma contrast alone; the guard stops netflix's dark-red N and the old youtube .ico's red plate, keeps pure-black marks (github/thepaper, colored 0) flipping, and is deliberately stricter than the light branch's 0.30 (wrongly flipping a colorful mark is a glaring defect; wrongly not flipping a dark mark is merely dim). Retuned against a 60-icon matrix (the original 14 + tmp/favicon-lab/icons2's 46 common sites — netflix/youtube/figma/tiktok/x/bilibili/zhihu/taobao/jd/weibo/… , favicon.im-fetched) with a visual comparison sheet (tmp/favicon-lab/sheet-2.py). Also 4.1.0: the default-icon SVG template is parsed once and cloned per swap (a 1371-row re-render used to re-parse the same markup per placeholder row, ~40 ms). The flip itself is CSS invert(1) hue-rotate(180deg) (.favicon-contrast-invert in neat.css) — a lightness-only flip that preserves hue and saturation, so the netflix red keeps its hue. An auto theme OS-level switch re-decides via a matchMedia('(prefers-color-scheme: dark)') change listener (cheap, no re-sampling), and the options page's faviconContrast switch (default on) reaches an already-open panel through a direct chrome.storage.onChanged listener in neat.js — the store mirror has no onChanged forwarding. 4.0.6 favicon enrichment hook: ctx.onPlaceholder(img) → boolean lets the enricher (src/favicon-enrich.js) handle a placeholder first (cached-icon hot-swap returns true; else enqueue + false → default SVG as before) on both placeholder branches; the API gains sampleIcon (the internal fingerprint, for the enricher's contrast registration of injected data-URL icons); reapplyContrast also covers img.favicon-enriched. Unit-tested by tests/favicon-fallback.test.js |
src/favicon-enrich.js |
Missing-favicon enrichment (4.0.6, docs/plan-4.0.8/favicon-补全设计.md): fetches real icons for hosts Chrome's _favicon has no cache for. Discovery chain L1-L4 — L1 GET https://<host>/favicon.ico → L2 parse the bookmarked page's <link rel=icon> (attribute-level regex; at most 5 scored candidates; data: hrefs pass straight through with base64 or percent-encoded decoding, and a malformed one skips just that candidate) → L3 proxy relay (the dead-scan's marker-PAC session, when live, retries L1/L2 via addProxyMarker; each breaker-tripped aggregator gets one proxied retry) → L4 the built-in aggregator list (favicon.run → icon.horse → DuckDuckGo) as the FINAL means (default-on sub-switch, per-provider breaker 6h + automatic failover — each provider's quirks are normalized behind a consistent url(host) + interpret(res, networkOk) → 'icon'/'no-icon'/'unreachable' interface; a clean no-icon fails over to the next provider, an unreachable one trips that provider for 6h; icon.horse answers unknown hosts with a 200 + image/png letter-avatar tile (deterministic per first letter — live-verified 2026-08-19, refuting the original clean-404 assumption), so an icon-horse 'icon' verdict that passes validation is re-checked against a per-letter reference probe (placeholderProbeUrl → <L>-vbmref.invalid, fingerprinted with favicon-fallback's sampleIcon FNV-1a — identical dimensions + hash = avatar → verdict flips to no-icon and failover continues; the avatar never reaches the 30d success cache; one probe fetch per letter per session, any probe trouble fails OPEN and never trips the breaker). Each layer's output passes a four-step validation (res.ok + ≤200KB + Content-Type/magic sniff + Image decode). Cache is per-host keys vbmFavicon:<host> = data URL + one index vbmFaviconIdx {v, down, hosts} (v3; per-provider breaker table, legacy v1 indexes migrate on hydrate keeping hosts, dropping only the stale breaker window), with a dynamic byte budget = (chrome.storage.local quota − bytes used by other features) × 0.8 (floored, capped at the real free space), halving eviction (cut the oldest half) when persisted bytes exceed it, >96KB icons session-only, quota-error emergency eviction (reuses the same halving), index-rebuild/self-heal on corruption/drift. Renders block only on an in-memory Map read; ≤6 concurrent fetches, per-host dedup, failed markers back off 24h → 3d → 7d then give up permanently (the f failure count climbs per retry, failedState derives gaveUp/retryAt; 4.0.8 收敛策略 — success entries NEVER expire: a cached icon is served until budget eviction or a cache clear removes it, so no periodic re-fetch storm and no endless retry of impossible hosts; retries only trigger from placeholder renders, never a background poll). setEnabled(false) aborts every in-flight item, and an aborted run never stamps the failed marker (4.0.8 收尾: the discover layers swallow the abort into a null, which used to be misread as "host has no icon"). The options-page storage.onChanged clear path now also drops individual hosts when only their vbmFavicon:* data keys are removed, not just on index removal (4.0.8 低优先级收尾). Wired by src/neat.js via a lazy ctx.onPlaceholder; live switches faviconEnrich (default on) / faviconEnrichAgg (default on). The hot swap fades the new <img> in and adds a 0.38 s .favicon-pop animation only when the row is in viewport (inViewport) — both silenced under prefers-reduced-motion. 4.0.8: every success entry records WHERE the icon came from — index host entries carry `src: 'direct' |
src/tree-render.js |
Tree HTML + data helpers (P1 module): buildRowTooltip (issues #62/#64: the ONE full-info tooltip builder every view bakes at render — title / URL / labeled Path (canonical root-first) / labeled Added (toLocaleString) / append line, all segments escaped internally, future metadata appends labeled lines), formatPath (canonical root-first ' / ' join — tooltips ALWAYS use it) and formatPathLabel (issue #64: the FULL chain NEAREST-parent-first A < B < C, NO depth cap — overflow falls to the same CSS ellipsis the canonical order uses, which then eats the distant ancestors — the meta-line form the reverseItemPath option selects; buildPathMap/buildTreeSnapshot carry BOTH as the parallel paths/pathLabels maps; generateHTML/nodeHtml/generateTreeBlocks thread the canonical map so TREE rows' tooltips resolve their path O(1) from the snapshot, and meta.tooltipOnlyPath (tree rows + tabgroups tab rows) feeds the tooltip while suppressing every label slot — the tree IS the hierarchy, its rows never grow path labels / a second line), getFaviconUrl, highlightTitlePositions (<mark> wrapping), row builders generateBookmarkHTML/generateFolderHTML/generateSeparatorHTML (sync indicators, localized (Local)/(Synced) dual-storage root suffixes via syncSuffixLocal/syncSuffixSynced, unsynced-subtree marking on syncing === false rows for the highlightUnsynced dimming, separator color via pure colorHex), recursive generateHTML (open-state from ctx getters, lazy getChildren expand, (Empty) rows), and pure tree helpers generateNodeTrees/buildTreeSnapshot/getParentPath/findFolderByType/getEffectiveSubTree/isRootFolder (dual-storage aware). 4.1.0 P1-1: buildTreeSnapshot(tree, subTree) is the single-walk snapshot — one full-tree traversal produces { html, nodeTrees, bookmarkIds, paths, ids } (paths/ids cover the FULL tree for list-row labels + visitStats.prune; html/nodeTrees/bookmarkIds follow the display subtree tree-view selected), so generateTree no longer repeats generateNodeTrees + addBookmarkParents + buildPathMap. initTreeRender(ctx) receives store/separatorManager/getOpens/getRememberState (getters, read at call time); neat.js feeds the builders into the search/actions ctx. No neatools inside. 4.0.5: the untitled-bookmark display-name fallback (protocol-stripped URL) is escaped through htmlspecialchars like its tooltip twin — a pre-v4.0 gap. Unit-tested by tests/tree-render.test.js 2026-08-26: the tree-row 暂存 plane also stands down when the staging VIEW is disabled (stagingRelayOn requires showRecentBookmarks on — the disabled-view contract of the staging master switch) 2026-08-28 perf 任务①: the row-tail icons ride the document-level ICON_SPRITE_SHEET (icons.js — 18 symbols, generated by scripts/icons/gen-sprite.mjs from the inline exports, byte-identical, contract-tested); neat.js injects the sheet at startup and ensureIconSheet stays as the test-environment fallback; the sprite recipe extends to the list views' row/head buttons (tabgroups rowIcons ×5, group/closed/window heads, dupes head+rows, staging rows/heads/bucket, stats/dead/search rows via staging-relay's optional icons arg — flipStageBtn included) |
src/tree-view.js |
Tree view layer (P1 module): owns the tree DOM — generateTree (+ startup getTree bootstrap, focusID/scrollTop restore — issue #58: the focusID restore (refocus + blueFade reveal highlight) is gated by the remember-state flag like the scroll/folder restore (4.1.1 分层记忆: scroll → rememberScroll, opens → rememberOpens (tree-render), focusID/focusSpot → rememberHighlight, query → rememberSearchQuery), so the remember-prev-state option turns the whole "where I was" restore off; revealFolder/revealInTree force remember-state on so explicit reveals keep working; legacy local-separator migration; 4.1.1 full-info round: the lazy folder-expand renders rows OUTSIDE generateTree and passes the LAST snapshot's canonical path map (lastPathsMap) so its rows carry the Path tooltip line too; issue #64: the focusID restore also stands down under ctx.getFocusSearchOnOpen, dropping the stale focusID eagerly), tree events (scroll persist, focus tracking, click expand/collapse with lazy children + closeUnusedFolders, middle-click focus), and bookmarkHandler (click/auxclick open semantics shared by every list view — tree/search results/recent/stats/dead/dupes rows all bind it; final polish: the search results pane also binds auxclick, closing a legacy middle-click gap; v4 task-2 slice D fires the optional ctx.onOpenBookmark(id, url) hook from its bookmark branch, the single page-side visit-stats collection point). The v4 "recently added" in-tree section is gone (slice B moved it to src/view-recent.js). initTreeView(ctx) receives store/tree/separatorManager/SeparatorManager/treeRender/search/actions/dnd/refreshSyncIndicators/open-state getters+setters/click-mode flags/views/onTreeGenerated (rebuilds the view path map + dead-mark overlays + visit-stats prune on every tree build)/toastAction (v4 task-3 #14: the undo bar's generic action toast); returns { generateTree, revealFolder, revealInTree, bookmarkHandler } (issues #62/#64 full-info round: the H2 adaptive-tooltip machinery — adaptBookmarkTooltip/adaptBookmarkTooltips + the delegated mouseover/focusin pass, and resize.js's drag-end re-measure — is RETIRED; every tree row bakes its complete tooltip at render time via tree-render's buildRowTooltip: 标题/URL/路径/添加时间). opens/rememberState stay in neat.js, shared via ctx. v4 task-3 #14: the onlyShowBMBar subtree filter gained a session-only showAllOverride — revealInTree on a target with no nodeTrees entry (outside the bar) toasts a hint with a "show all and reveal" action that regenerates over the full tree and completes the reveal, never rewriting the setting. No neatools inside. Unit-tested by tests/tree-view.test.js 2026-08-27 real-data perf round: tree rows join the 4.1.0 content-visibility list (cv:auto + contain-intrinsic-size auto 1.67em — the row-height formula itself), killing the offscreen style/layout/paint that dominated the maintainer's 5161-URL cold open (trace: UpdateLayoutTree 2715→831ms, Layout 1561→177ms, Paint 1307→108ms); the old 'geometry-sensitive' exclusion was re-audited item by item (tooltips are H2 per-row on demand, dead ×/sync dots are in-row absolutes, reveal scrollTo+focus auto-renders the target) — verify-keyboard/scrollbars/menu all green, 5005-row scrolled visual probe passes 2026-08-28 perf 任务④: CHUNKED first paint for big shallow renders — buildTreeSnapshot now also returns top-level blocks (nodeHtml factored out of generateHTML; html === wrapper + blocks.join, one code path); generateTree streams the first ~3 viewports synchronously then per-rAF (scroll/focusin/dragstart flush synchronously; a remembered deep scrollTop OR a focusID memory stays on the one-shot swap so every restore path is instant; jsdom doubles fall back to the sync swap) |
src/sync-ui.js |
Sync indicator wiring (P1 module): subscribes to SyncManager's syncStatusChanged window event and rebuilds the .sync-indicator badges on tree/search-result rows (updateBookmarkSyncStatus, refreshSyncIndicators). initSyncUi({ store }) runs the wiring on init (DOMContentLoaded-aware) and keeps the legacy window.neat.refreshSyncIndicators surface. No neatools inside. Unit-tested by tests/sync-ui.test.js |
src/view-manager.js |
View system core (v4 task-2 slice A, docs/plan-4.0.0/v4task-2.md §3): the seven-view registry (tree/search structural + tabgroups/recent/stats/dead/dupes registered by their modules — 4.1.0 added tabgroups; registration order = tab order), the #view-tabs strip (16px inline-SVG icons from VIEW_ICONS, localized labels, count badges gated by showTabBadges since v4 task-3 #18, showViewTabs master toggle, ←/→ cyclic tab move with dir-aware RTL, strip Home/End view-scoped — first/last row of the CURRENT view via focusEdgeRow, never a view switch, no rows → focus stays on the tab), the single-active-view state machine (activate(id, { preset }) = deactivate → display switch → activate → persist activeView/viewState; the optional preset is forwarded to the view's activate — v4 task-4 #6 view-preset custom commands drive the dupes strategy/scope and the dead scan-start through it); the sliding .tab-indicator (placeIndicator) is clamped to the strip — offsetLeft/offsetWidth round independently, so on a fractional-DPR layout the last tab's indicator could overshoot the strip by 1px, and that absolutely-positioned 1px overflow propagated through the unclipped #view-tabs→#container→body chain into the document scroll width, which Chrome uses to size the popup window (4.0.8 report: the popup widened 1px only while the dupes tab — the strip's last — was active; the clamp floors at 0 and skips itself when clientWidth is absent, keeping unit stubs unchanged); startup restores the stored view when rememberView is on — default since v4 task-3 #6, panel always restores, an unregistered stored id waits in pendingRestore until its module registers), per-view focus/scroll memory (v4 task-3 #7: viewState migrated from a bare scrollTop number to { scroll, focus } with old-number compat — leaving a view records scrollTop + the .focus row id, a focusin marker keeps mouse-clicked rows trackable, and entering re-marks the row via a 100 ms×20 watchdog that survives async view re-renders), the shared bookmark-id → parent-path map (buildPathMap on every tree rebuild backs every list view's §3.6 path labels via pathOf/showItemPath), the aria-live viewSwitchAnnounce, and the Esc layering (palette → dialog → search → back to tree). View-jump keys (v4 task-4 #10): Alt+1…9 over the visible views is the portable binding (Edge reserves Ctrl+1…8 for browser-tab switching); the legacy Ctrl/Cmd+N twin still fires where the browser lets it through, never inside inputs, and Ctrl+Alt (AltGr) combinations are excluded. 4.0.5: the remembered-row restore resolves its focus target through src/list-focus.js's rowFocusTarget contract (the row's anchor/span, or the tabindex row container) instead of a firstElementChild heuristic — a button-led row (the dupes member rows lead with the keeper radio) made the old restore focus a tabindex-less <li>, a silent no-op — and viewState (per-view scroll + remembered row) is now gated by the same remember-state option as focusSpot and the tree restore (the path-map API also exposes pathsReady() — issue #64's boot-order heal contract — and pathLabelOf(id) / dateAddedOf(id), the meta-line path form (canonical by default, nearest-first label map under the reverseItemPath option, default off; tooltips stay canonical either way) and the id→dateAdded map (the Added tooltip line's source for staging/visit-stats rows whose own models lack it); issue #63: the built-in TREE registration carries persistScroll too — a view round-trip no longer wipes its scrollTop via the container display:none; issue #64: restoreFocusSpot stands down under ctx.getFocusSearchOnOpen): off means never written on view switches and never restored on activate, and a stale stored key from a remember-on session is dropped once at startup. initViewManager(ctx) returns { register, attach, activate, activeId, activeDef, isActive, views, lists, listOf, onEscapeActive, escapeToTree, focusTop, focusActive, focusEdgeRow, buildPathMap, pathOf, updateBadges, showItemPath, isAvailable, availableViews, viewMenuState, hideViewTab, disableView }. 4.0.8: view hide/disable — the tab strip's right-click menu (Hide/Disable, ctx.viewMenu) and the options page's per-view Enabled/Disabled state are driven from here. The legacy show*View keys mean HIDDEN (tab + Alt+N gone, palette command stays, entering a hidden view toasts the Esc/palette way back via ctx.toastAction — an intentionally buttonless toast); the new disable*View keys (disableRecentView/disableStatsView/disableDeadView/disableDupesView/4.1.0's disableTabGroupsView, store.KNOWN_KEYS members) mean fully forbidden (activate refuses, palette rows drop). Tree/search can't be hidden — when only they remain, their Hide item collapses the whole strip (showViewTabs off + body.no-view-tabs). Hiding the ACTIVE view keeps it active; disabling it switches to the first visible view; Alt+1…9 indexes the visible set so numbering compacts. A chrome.storage.onChanged listener + store.adopt live-syncs an open popup/panel with options-page writes. 4.0.8: any real view switch dismisses the transient toast (undo bar / hidden-view hint) via ctx.dismissToast — and because the manager's first activation runs inside initViewManager, neat.js initializes initUndo BEFORE initViewManager and passes toastAction/dismissToast as plain values (2026-08 audit: the key originally sat on initTreeView's ctx where nothing reads it, and the first lazy-getter fix threw a TDZ ReferenceError on the first view switch — caught by the Docker smoke gate, invisible to the unit suites). Unit-tested by tests/view-manager.test.js |
src/list-focus.js |
Row-focus park/restore shared by the four list views (recent/stats/dead/dupes) and the search view's history area (4.0.5 consolidation of their verbatim copies — the 4.0.1 focus law's mechanism): parkRowFocus/unparkRowFocus (a render's innerHTML swap replaces every row, so the focused row is parked before the swap and restored after — by row id when it carries one, else by its index among the list's <li>s, clamped on restore so a vanished row lands on the row that took its place; an emptied list falls back to the container itself, or to the caller's emptyFocus — the parameter that absorbed search.js's history-area "no rows left → focus back to the search box" copy), rowFocusTarget (the row focus-target contract — querySelector('a, span'), or the row container itself when it carries tabindex; never a firstElementChild heuristic, so a button-led row still resolves to its anchor; shared by view-manager.js's remembered-row restores), and the toolbar focus trio TOOLBAR_CONTROLS_SEL + parkToolbarFocus/restoreToolbarFocus (the stats/dead/dupes toolbars re-render together with the rows — a focused control is parked as its first class token + index among same-class controls, the same key view-manager's focusSpot uses, so a re-render that adds or removes a different kind of button does not drift the restore the way a bare position index would; the selector includes .risk-banner controls so dead/dupes' banner rides along). Unit-tested by tests/list-focus.test.js |
src/list-chunks.js |
Chunked list painting (4.1.0 perf, driven by the diag-41x-perf.js findings): paintListChunked(list, { head, pieces, first, chunk, onHead, onChunk, onSettled }) — the heavy list views (tab-groups, dupes) used to pay 1-2 MB of row markup in ONE innerHTML assignment (~470 ms parse for 2508 dupes rows, ~1.7 s parse+layout for a 1371-row tab-groups refresh). The helper paints the head (toolbar + the closed <ul>) SYNCHRONOUSLY, discovers that <ul> in the fresh DOM, appends the first rows INTO it, then streams the remaining pieces inside it in requestAnimationFrame batches — pieces NEVER concatenate after the closed </ul> (in a real parser the <li>s would strand as siblings of the list, invisible to the keyboard row walks and the #x-list ul li CSS rules — the D1 keyboard-gate regression the Docker tier caught, invisible to the string-concat unit doubles) — first content lands in the first frame, the total work is unchanged. Degrades to a single synchronous innerHTML when rAF is absent (unit-test doubles, node), the list lacks querySelector/insertAdjacentHTML, or the list fits in the first chunk; a head without a <ul> repaints everything synchronously (no callback has fired yet). The scheduler is resolved per CALL (not import time). The returned handle's cancel() drops pending batches — every render cancels its predecessor (the views hold a paintHandle). Callback contract: onHead right after the head paint (toolbar focus restore — the toolbar lives in the head), onChunk(list, from, end) after each appended batch WITH the slice bounds (the views gate piece-indexed focus retries so no per-batch whole-list scans run), onSettled once the last piece is in (clamped-index row restore is only safe with the full list present). 4.1.0 perf round 2 (report docs/review-4.1.0/perf-round2-audit.md): adaptive:true re-sizes each subsequent batch from the MEASURED insert cost toward budgetMs (grow ×1.6 below 40% budget, shrink proportionally above, clamped to [minChunk, maxChunk] — the fixed 42-frame staircase at 2508 rows becomes machine-paced), and pipes: [{ ul, pieces, first, chunk }] streams several <ul>s from ONE head paint (the dead view's result list + marked-residue list; each pipe's first slice lands synchronously with the head, the same contract as the single list). Perf round 3 (2026-08-24): pipes mode is now adaptive too — one shared scale over every pipe's base chunk, fed by the WHOLE round's insert cost (all pipes share one frame budget); previously adaptive was silently ignored in pipes mode. The fold-during-stream cancel contract (f9d9e1b) is pinned by a dedicated test. Unit-tested by tests/list-chunks.test.js |
src/virtual-list.js |
Virtualized list painting, 4.1.0 实验室 (virtualScrollLab, options Labs group, DEFAULT OFF — promoted only after real-world soak; the P2 virtual-scrolling item in pre-release form). Same head/pieces contract as list-chunks.js; the views pick the painter per render from the flag (live: a chrome.storage.onChanged listener adopts the value and re-renders). Keeps ONLY the viewport window (+8-row overscan) in the DOM: piece heights are estimated (li-tag count × the 28px row height, group blocks = head + members), off-screen geometry is two paddings on the rows , scroll re-windows via a passive scroll listener (rAF-throttled), and a focusin edge extension pages the window when the first/last rendered row takes focus (a held ↓ keeps advancing). The pre-paint scrollTop is captured BEFORE the head swap (a real DOM clamps scrollTop when content collapses — reading after silently re-windowed to the top on every re-render); revealIndex seeds the initial window around a piece (the tab-groups first-activation current-tab scroll). 4.1.0 perf round 2: piece heights are MEASURED after every applied window (per-piece li offsetHeight, prefix sums rebuilt, paddings written from the UPDATED geometry) — visited regions get exact heights, so wide/panel two-line rows stop drifting the scrollbar (the LAB's chief limitation); unmeasurable pieces (unit doubles, never-laid-out cv:auto rows) keep their estimate. Remaining lab limitations: End/Home land on the last/first RENDERED row; clamped-index focus restores are approximate (id-based restores work — the handle's partial flag lets the views skip the index path). 4.1.0 perf round J: content-visibility:auto is mutually exclusive with the painter — a fresh window's rows skipped rendering AND hit-testing until the next scroll (blank viewport at 6000 bookmarks, repro diag-vl-6000.js), so the views toggle a .virtual-paint class and css/neat.css forces content-visibility:visible on those rows (cv buys nothing at ~40 windowed rows anyway; measured heights also become exact). Fold surgery round: the handle exposes fold(from, to, shouldHide) — hide/show a CONTIGUOUS piece range with no repaint (hidden pieces keep their last-measured height, contribute zero to tops; the range's rows are detached into a per-piece DocumentFragment and reinserted as the ORIGINAL nodes on show; paddings — the virtual scrollbar — follow the rebuilt tops exactly, a block entirely above the viewport compensates scrollTop so the view stays anchored, and pieces that leave the window are trimmed so painted rows + paddings always equal the window span); hiddenRanges opts let a render start pieces hidden so render-time folds expand surgically. The views feed it fold-shaped pieces (head and member body as SEPARATE pieces). 2026-08-28 audit fix round: external geometry changes invalidate the cached prefix sums (extension zoom rescales #container's internal layout space, a popup height drag changes the viewport, a width flip across the 480px container query turns rows two-line) and NONE of them fire scroll — a stale model pinned the scroll range so the list TAIL became unreachable at 120% zoom (repro diag/diag-vl-zoom-drag.js: bottom paddingBottom wedged at 188px, un-zoom and even a re-render with a real-px savedScroll didn't heal). Two ResizeObservers now feed a relayout(): the list's box (viewport change → re-window only) and the rows ul (its rendered height drifting from totalH() means ROW heights changed → heights reset to estimates, tops rebuilt, the window around the live scrollTop re-applied — the fresh-paint state, which re-measures the window); apply() always leaves ul height == totalH() (the paddings ARE the model) so the observers' own writes don't re-trigger; a baseline delivery primes the guard, cancel() disconnects both, and handle.relayout() exposes the recovery for tests/callers. Verified: diag phases B/C/D/E/H all blank 0/5 with the bottom padding 0 (tail reachable, un-zoom heals). Unit-tested by tests/virtual-list.test.js |
src/view-recent.js |
Staging view (velvet staging, keeps the recent view id/#view-recent container/showRecentBookmarks/disableRecentView keys — the tab title reads "暂存区"/"Staging"; palette /recent gained the alias staging): the upgraded recent tab as a decision workbench. ONE scroll container #staging-list (the registered listEl, first persistScroll user) holds sibling <ul>s — #staging-items (dual-state rows: id-anchored = bookmarked with a REAL-state star, id = null = url/title snapshot with a hollow star; render order ① the unbookmarked inbox bucket head with "new N" since lastSeenTs + favorite-all ② groups by createdAt ③ bookmarked loose rows) then the foldable #recent-head section divider (count + stage-all) and #recent-list (the classic getRecent rows, now with a hover ↑ stage toggle) — crossRowUl walks the regions. The staging local key persists the model (src/staging.js), store debounced + explicit views.updateBadges(); badge = item count. Tree-event sync: onCreated promotes matching id-less rows, onRemoved verifies per-URL through chrome.bookmarks.search (relink or fall back to id=null — never silently drops), onChanged updates snapshots, every tree rebuild feeds onTreeSnapshot for a full urlIndex relink (buildTreeSnapshot grew a urlIndex map), and chrome.storage.onChanged replays the whole object from the other document. Selection mode (dead/dupes machinery, two rungs): open/open-in-group/favorite/un-favorite (REAL create/remove — items stay)/group-assign dialog/move-copy via the extended picker (>10 move confirm)/delete (confirm + toastAction undo restoring bookmarks AND items)/remove (tree untouched)/clear (confirm); group + bucket heads are tri-state select-all units; entering expands all folds via snapshot; Esc exits. Group-level homing: head hover button + the staging group menu's save/copy-to-folder. The returned { refresh, api, onTreeSnapshot } feeds neat.js's lazy staging ctx (context-menu send entries, stats history-row button, tabgroups interop). UX rounds: the send glyph is the paper plane (STAGE_ICON line / STAGE_ICON_DONE solid pair), the per-bucket recently-added heads carry a hover send (bucket membership fixed at render time), the group menu gains select-all-in-group (one hop into selection mode preselected), and the row-menu "Copy/move to…" on an unbookmarked row routes through api.moveCopyItem (the §3.3 favorite-into-folder semantics — its li id is a data ordinal, not a bookmark id). Workbench round (2026-08-23, the 9-point review): (perf) chrome.storage.onChanged fires in the SAME document that wrote — every persistStaging echoed back ~200ms later as a phantom replay (double render + a stagingState swap that could strand batch closures on the stale object); the listener now skips echoes byte-identical to any of the last 16 strings we flushed (ownWrites), tree-event promotions/snapshot edits/relinks mutate state synchronously but coalesce their persist+render into one 120ms trailing tick (commitStagingSoon — a folder-send batch previously re-rendered the full list once per created bookmark), and probePermission only repaints when the history-permission verdict CHANGES (view entry painted twice before). (groups) the idle toolbar never disappears: summary left, the action cluster right-pinned (dead/dupes law) with 新建分组 (NewFolderDialog → manual group, empty manual groups render their head and survive pruning) + select-mode (hidden at 0 items); the group head carries the tabgroups-style hover quick tail [rename EDIT][place FOLDER_STAR] on the rows' right axis and F2 renames the focused head; the group menu gains 删除分组 (deleteGroup = group + members leave, confirm + toast-undo via restoreGroup). (DnD) staging rows are HTML5 drag sources (anchors opt out with draggable="false" so the li is the source; the tree's mousedown drag skips data-virtual rows anyway) — drop on a group head assigns (collapsed targets auto-expand), on the bucket head ungroups, on a row adopts that row's group, and dragging a group HEAD onto another reorders (reorderGroups, manual arrangement is staging-only bookkeeping). (CSS) the duplicated stale staging/folder-pick/toolbar blocks were removed from neat.css (~480 lines — the second copy overrode the new recipe: the collapsed chevron showed BOTH the ▸ glyph and the border-drawn icon, heads lost the fixed row height/8px rhythm); heads share the tabgroups/dupes recipe (8px lead via 4px gap + 4px glyph margins, 4px end padding, title flex:1, :focus protocol, shared 14px count pill), the HIERARCHY is tabgroups-style — member rows (bucket members included) indent 16px so their favicon column (32px) sits exactly under the group head's title / bucket head's star, loose rows keep the 16px baseline, narrow rows are min-height: --vbm-row-h (28px, the heads' height), and selection mode keeps the checkbox on the shared 8px axis while stepping the member anchor margin 28px (favicon 56px = the selecting heads' title/star column) — and every trailing button is a plain .row-btn vertically centered on ONE 28px-stride right axis shared with the toolbar's rightmost icon. The group quick tail is four always-visible buttons ([rename][place][dissolve][remove 移出暂存] — the danger delete-group was pulled off the head into the menu/selection mode, the rightmost slot is now the tree-safe group removal with confirm + undo; ≤400px folds rename/dissolve away) and the scissors .staging-cut divider separates the recent region (whose time heads share the 11px/8px/28px style). The selection action rung is iconified (nine 22px glyph buttons, title/aria labels) and openBookmarksInGroup was fixed for the urls-only call path (pickGroupColor(undefined) crash); a THIRD rung is the customizable move-to shortcut bar (stagingShortcuts local key, tabgroup-color dots + alias chips — normal mode is zero per-chip chrome: click = MOVE, manage lives in the right-edge [+]/[pencil] cluster, edit mode gives chips a dashed accent border (click = edit) and a floating red × over the color dot for delete; the editor is dialogs.StagingShortcutDialog — target folder via the shared picker's legacy single-select, alias input, the nine tab-group color swatches; move only, copy stays on the icon rung/menus). The action rung is width-aware: .staging-btn-label spans appear progressively (danger pair at ≥520px container, organize/favorite at ≥680px, all at ≥820px), and the shortcut bar's 「收藏到:」 label shows only ≥520px. The folder picker's filter input now caps at --dialog-content-width and shares the dialog-input recipe. Hierarchy (round G): member icon 40px = loose-row title = group-head title / bucket-star (24px indent + faint accent connection line); selecting keeps checkboxes on the 8px axis with a 36px anchor step (icon 64px). Perf: staging-only actions repaint ONLY banner+toolbar+#staging-items (renderStagingNow, anchored at .staging-cut; recent-region nodes and their favicons untouched, syncRecentStageButtons updates the recent stage glyphs in place) and a clean re-activation updates just the bucket's new-N pill (painted/lastRenderedRaw guards) — verified in a real Chromium via scripts/harness/diag/diag-staging-verify.js (hierarchy/axis/indent/selection-checkbox-axis/send-button-axis/quick-tail/DnD/reorder/entry-churn=0) with diag-staging-perf.js and diag-staging-geometry.js as the reproduction probes. A guide strip above the toolbar (「不再提醒」 dismiss, stagingGuideDismissed) introduces the workbench; the shortcut editor defers its post-save render so a newly added chip receives keyboard focus (←/→ walk it immediately), its alias input caps at --dialog-content-width, and the picker opens at z-index 210 above the editor (pins/recents chips visible). Perf round H (master merge): the full repaint rides the 4.1.0 chunked painter (paintListChunked, pipes mode — banners/toolbars/empty s land with the head, staging rows stream in 60+120 adaptive batches, the recentCount-bounded recent rows land with the head; a pending paint is cancelled before every partial repaint), staging rows joined the content-visibility:auto roster, and the row loops hoist per-render i18n labels (stagingFromHistory/star/remove + recent bucket labels resolve once per render). Perf round I (fold surgery): group/bucket folds no longer repaint the staging area - toggleGroupFold/toggleBucketFold sync the head li in place and remove/re-insert only its contiguous .staging-member rows (recent region untouched, lastRenderedRaw advances so re-activation never back-repaints); dupes/tabgroups got the same surgery (foldGroupSurgically). Fold-memory round (perf round J): the idle toolbar became the REAL staging section head #staging-head (chevron + bold title + count pill moved into the right cluster with [new group][select mode], folding the whole staging area - headCollapsed persists in the model); the recent time sections became REAL foldable group heads (li.recent-group-li with chevron/title/count pill/stage button, member rows carry data-recent-group, recentGroupCollapsed persists per bucket, folds are surgical via recentMemberHtml/recentGroupRows); both big heads are 32px tall with 14px/600 titles; the selection fold snapshot covers the new folds; both counts are unified pills (staging aria = stagingCount, recent aria = title . count); the guide banner gained the universal session x (risk-banner law) beside the permanent dismiss; the assign dialog show rules (.needStagingGroupAssign display + cover) were added - the toolbar group button used to write the body class with no CSS to reveal the dialog; .staging-remove joins the 680px label tier; STAR_X_ICON/STAGE_REMOVE_ICON redrawn as full-size glyphs with a bottom-right corner x (no shrink). Instant-fold round L: the two big heads fold/unfold via root classes (staging-area-collapsed / recent-area-collapsed) - the recent region always paints (rows hidden not dropped, the fetch is never skipped), the staging area keeps the prebuilt stagingRowsCache and the unfold drops it in ONE innerHTML (measured 3.2ms sync / 19ms settled vs the old 150-250ms full repaint); viewRecent was re-translated across all 42 non-en locales (zh_CN had stayed 最近添加). Axis round K: member rows carry a .staging-connector span and the flat 2px accent edge line became a REAL tree drawing (the tabgroups color-line law, muted) - a 1px light DASHED trunk drops from the PARENT NODE'S ICON axis (the head glyph-well center, 34 narrow / 35 wide = --stg-trunk-x; 2026-08-27 re-drawing - it starts BELOW the icon, +12px past the row middle, never over the glyph), each member row gets a dashed tick to its favicon LEFT edge (48/50 = --stg-tick-w), the last member closes the trunk with an elbow (staging-last; has-members gates the head's half, collapsed heads hang no line; selection mode keeps its own checkbox-axis calibration, still chevron-anchored at 39.5 - there the head well and member favicons share one column so the icon axis has no room); dashes are a 2px-on/3px-off repeating gradient in a 30% muted token. Peer-heads + icon rounds (2026-08-25): the three fold heads are PEERS of their member rows — the title inherits the body base font at weight 400 (the old 11/12px/500 pins read smaller AND bolder than the row titles), line-height mirrors .tree-item-link's 1.67em so heads ride min-height and the wide/panel form stretches every fold head to the two-line row height (3.34em, single-line content vertically centered — the creation-time/range .head-sub meta line shipped and was withdrawn the same day, its three locale keys deleted). Every fold head now leads with a GLYPH on the bucket star's slot: the tree's FOLDER_ICON on group heads, CLOCK_ICON on the recent time buckets — glyph left edge = the LOOSE row's TITLE column (40px; the star's old 2px lead parked it at 42, off its own documented column since inception) and the glyph y-center sits on the title ink center to ±0.02px (diag-verified narrow AND wide, where head height equals the two-line row to 0.02px). The recent time-bucket rows gained the staging-member 24px indent, so both areas share one hierarchy law: head glyph column = member favicon column (40px narrow / 64px selecting). The same round landed the selection-mode head alignment: the heads' first content glyph (group .head-main / bucket star) gains a 10px selecting-only inline lead so it stacks on the member rows' 64px first-glyph column (the long-claimed "64=64" was aspirational — the title sat at 54px, the star at 56px), and diag-staging-verify.js was recalibrated (bucket tail's rightmost is 移除暂存 not fav-all, chevron 18px fold lead, time-bucket title on the 40px axis) — the diag is ALL-PASS again. Unit-tested by tests/view-recent.test.js 2026-08-26 open×4 + named-group round: the group head quick tail reorders to SIX always-visible keys [open-all OPEN][open-as-tab-group TABS][rename][dissolve][place][remove] (open pair accent, drops off an empty manual group; ≤400px folds open-group/rename/dissolve); the recent time-bucket stage buttons and a new api.addItemsToNamedGroup(name, entries) land batches in a staging group NAMED after the origin (same-name group absorbs the append, staging.findGroupByName); api.groupUrls/groupName feed the group menu's open entries. Alignment round (diag-staging-geometry.js ALL-PASS, narrow+wide): member favicon left edge == the loose row's title axis, head glyph CENTER == member favicon center (the glyph rides the favicon's 20/22px well), head title == member title — the wide two-line form steps the head well +6px and the member indent 24→30px (a LATER cascade block — same specificity beats the narrow base only by order). Report round 3 (2026-08-26): the SAME three laws now hold in selection mode, re-anchored on the checkbox axis (diag-measured narrow 52/62/76 + wide 58/69/88): the member anchor margin steps 24/30px (was 36), the head glyph well re-docks to the member favicon start (the chevron's 12px tail margin is normal-mode chrome — the checkbox owns the selection lead) and the connector tick retargets the new favicon column; syncRecentStageButtons flips the FULL button (class + aria + labels + inner svg) through the shared flipStageBtn — a bucket-head send left never-sent rows accent-tinted but HOLLOW (.staged only re-colors; the solid-plane swap needs the icon innerHTML), verified by diag/diag-recent-stage-flip.js. Report round 6 (2026-08-26): the idle 暂存全部 send flips every result-row plane in place, and the history AREA shows only searchHistoryCount rows (options 搜索 → 最近搜索显示条数, default 5 — the render crops, the stored MRU cap is independent; the area height follows the cropped rows). Audit round (2026-08-26): the HEAD planes join the sync — bucket heads (.recent-group-stage) and the region's stage-all flip per the tabgroups group-head law (every member staged → staged + STAGE_ICON_DONE, aria-pressed; the send label/aria-name stay — heads are indicators, not toggles), and the sync now also runs on the FULL paint's settle tail (a first paint with everything already staged no longer renders hollow heads; the guard requires both querySelectorAll and querySelector so innerHTML-only test doubles skip quietly). |
src/staging.js |
Staging pure data model (velvet staging §0.3/0.4): the staging local key's JSON shape — dual-state items (id non-null = bookmarked anchor, null = url/title snapshot; NO favorite flag — id IS the state), URL as the uniqueness key (resends never duplicate and never touch the existing row's group), 500-item hard cap (batches reject whole), groups (createdAt order, sourceFolderId/sourceTabGroup merge, dissolve forgets the source), the unbookmarked-inbox bucket derivation (id=null && group=null) with lastSeenTs/newCount, and the relink engine (dead anchor → another same-url node or fall back to id=null — items only leave through the explicit move/delete/remove/clear exits). Zero chrome/DOM — the view and actions layers own persistence and rendering. Workbench round (2026-08-23): groups carry a manual flag (user-built via the toolbar 新建分组 / the assign dialog's new-name path) — pruneEmptyGroups keeps them through an emptied member set (a manual group is a landing zone, not a leftover), deleteGroup removes group + members together and returns a receipt restoreGroup undoes (the view's confirm + toast path), and reorderGroups(draggedId, targetId) lands a dragged head BEFORE its target, rebasing createdAt ascending so the create-time sort invariant survives manual arrangement. Pre-release audit round (2026-08-24): restoreItems(state, snaps, groups) is the group-aware undo counterpart of removeByUrls/clearAll/dissolve — it re-creates missing groups FIRST (same ids, so member snapshots reattach; add cannot do this) then re-adds item snapshots verbatim, with whole-batch cap rejection ({full:true} the caller must surface). Unit-tested by tests/staging.test.js 2026-08-26: findGroupByName(state, name) — the exact-name landing lookup for the named-group batch sends (oldest match wins, blanks never match) |
src/staging-relay.js |
The staging relay's shared row-button recipe (2026-08 relay round, spec in docs/plan-velvet/velvet-feat-staging.md §13): stageBtnHtml(api, item, _m) (the hover 发送到暂存 toggle — .staging-add-btn + .staged accent + STAGE_ICON/STAGE_ICON_DONE, the stats-view §2.3 original extracted), toggleStageItem(api, item) (one click = one toggle: unstaged → addItems, staged → removeByUrl; returns the new state or null when unavailable), flipStageBtn(btn, staged, _m) (in-place icon/label/class swap — the hosting views do NOT re-render on staging changes, so the click must flip its own button), and isStagedUrl(api, url). Consumers: view-dead rows+toolbar, view-dupes rows/group heads/toolbar, search rows. The dupes group heads wrap it with whole-membership semantics (stageGroupByKey + flipGroupBlock). Zero state of its own; the api is view-recent's, passed as ctx.staging/ctx.stagingApi. The whole relay stands down behind the stagingEnabled master switch (options 暂存和最近添加, SYNC key): each host view gates its builders on api.isEnabled(), view-recent collapses to the bare recently-added list, and context-menu.js hides every staging entry at open time. Consumers since the fourth relay round also include the TREE rows (tree-render's meta.tailHtml quick actions [编辑][发送到暂存][删除], the treeRowActions options switch) |
src/toolbar-fit.js |
Selection action-rung label fitting (extracted from view-recent 2026-08): fitToolbarLabels(bar) reveals the iconified batch buttons' text labels ONE BY ONE from the right edge as free width allows (measured per render — no container breakpoints), watchToolbarFit(listEl, fit) re-fits on width changes via one ResizeObserver. Buttons opt in with .vbm-fit-btn + .vbm-fit-label; consumers: view-recent (staging actions rung), view-dead + view-stats (the two-rung selecting bars, fifth fix round) |
What this file has done since we first saw it
Hashed on every crawl. A supply-chain change to an agent config is a question of when, not whether, so the history is kept rather than the latest state alone.
- yesterday First seen · 94 lines · 0 tokens per session scan A 2fb9b8b4007d
modules is an agent published in the GitHub repository windviki/vBookmarks (133 stars, last pushed 2d ago), licensed MIT. It costs nothing until one of its globs matches a file; then it loads 41,115 tokens. A static security scan graded it A with 0 findings. No closer match exists in the catalogue, so it is treated as the original; first seen 2026-08-30.
Other agents, from other repositories
product-lead
Use this agent when you need to translate user ideas or feature requests into actionable product requirements. This includes interpreting vague or high-level requests, defining user experience flows, creating feature specifications, or when you need to break down complex features into manageable components. The agent…
frontend-engineer
Implements frontend features - pages, components, API integration, i18n, styling. Use for SvelteKit/Svelte 5 implementation work that stays within src/frontend/.
i18n
你是一个精通 Vue3 国际化架构的前端专家(专注于 Vue3 + TypeScript + Composition API)。同时,你也是一位专业的 UI/UX 翻译专家,擅长将中文界面语言翻译为地道、简洁的英文。.
chat-agent-spec
应实现于: /src/everlingo/agents/agent.py ,主要实现在 class MainAgent 。.
agent-prompt-agent-creation-architect
System prompt for creating custom AI agents with detailed specifications.
ko-translator
구조화된 영어 콘텐츠를 자연스러운 한국어로 번역. JSON 입력을 받아 번역된 JSON 출력. Medium 아티클 변환 파이프라인의 2단계.