Skip to content

feat(browser): PR 9 follow-up — BookmarksBar + SitePermissionsPanel#313

Merged
jaylfc merged 3 commits into
masterfrom
feat/browser-pr-9-followup-ui
May 5, 2026
Merged

feat(browser): PR 9 follow-up — BookmarksBar + SitePermissionsPanel#313
jaylfc merged 3 commits into
masterfrom
feat/browser-pr-9-followup-ui

Conversation

@jaylfc
Copy link
Copy Markdown
Owner

@jaylfc jaylfc commented May 5, 2026

Summary

  • BookmarksBar: collapsible row of bookmark chips rendered below the address bar when the active profile has bookmarks. Each chip shows a Google S2 favicon + truncated title; click navigates the active tab; right-click opens a Remove context menu. Auto-hides when the list is empty. Syncs with the AddressBar star via the existing taos-browser:bookmark-changed event (skips self-dispatched events to avoid double re-fetch).
  • SitePermissionsPanel: modal table panel accessible via a new "Site permissions ›" row in SettingsPanel, alongside "Agent capabilities". Lists all per-host permission grants (host_pattern | permission | state) for the active profile with a per-row Revoke button. Uses new browser-site-permissions-api.ts wrappers calling GET/DELETE /api/desktop/browser/site-permissions.

Test Plan

  • vitest run — 789 tests pass across 108 files (6 new BookmarksBar tests, 8 new SitePermissionsPanel tests)
  • tsc --noEmit — 0 errors
  • Manual: open browser window with a bookmarked page → bar appears below address bar, chip navigates, right-click removes
  • Manual: Settings → Site permissions → table shows grants, Revoke removes row

Summary by CodeRabbit

Release Notes

  • New Features
    • Bookmarks bar: A new horizontally scrollable toolbar displays your saved bookmarks with favicons and titles for quick navigation. Click any bookmark to navigate directly.
    • Manage bookmarks: Right-click any bookmark to remove it from the bar.
    • Site permissions management: A new settings panel lets you view and revoke individual site permissions that have been granted to web applications.

@kilo-code-bot
Copy link
Copy Markdown

kilo-code-bot Bot commented May 5, 2026

Code Review Summary

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 0
SUGGESTION 0
Issue Details (click to expand)

CRITICAL

File Line Issue
desktop/src/apps/BrowserApp/BookmarksBar.test.tsx 117 Test expectation mismatch - expected string is incorrect
Other Observations (not in diff)

Issues found in unchanged code that cannot receive inline comments:

File Line Issue
Files Reviewed (7 files)
  • desktop/src/apps/BrowserApp/BookmarksBar.test.tsx - 1 issue
  • desktop/src/apps/BrowserApp/BookmarksBar.tsx - 0 issues
  • desktop/src/apps/BrowserApp/SitePermissionsPanel.tsx - 0 issues
  • desktop/src/apps/BrowserApp/SitePermissionsPanel.test.tsx - 0 issues
  • desktop/src/lib/browser-site-permissions-api.ts - 0 issues
  • desktop/src/apps/BrowserApp/BrowserApp.tsx - 0 issues
  • desktop/src/apps/BrowserApp/SettingsPanel.tsx - 0 issues

Reviewed by grok-code-fast-1:optimized:free · 147,783 tokens

]);

render(<BookmarksBar windowId={WINDOW_ID} profileId={PROFILE_ID} />);
await waitFor(() => screen.getByText("A Very Long Title Th…"));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CRITICAL: Test expectation mismatch - the truncate function slices to 20 characters and appends "…", resulting in "A Very Long Title Tha…", but the test expects "A Very Long Title Th…" (missing 'a'). This will cause the test to fail.

Suggested change
await waitFor(() => screen.getByText("A Very Long Title Th…"));
expect(screen.getByText("A Very Long Title Tha…"));

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

Warning

Rate limit exceeded

@jaylfc has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 44 minutes and 14 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 09e1c0c6-7a47-4327-b84b-d04da432d4f2

📥 Commits

Reviewing files that changed from the base of the PR and between 1e01987 and 8183206.

📒 Files selected for processing (2)
  • desktop/src/apps/BrowserApp/SitePermissionsPanel.tsx
  • desktop/src/lib/browser-site-permissions-api.ts
📝 Walkthrough

Walkthrough

This PR adds two new browser management features: a BookmarksBar component that displays bookmarks in a horizontal toolbar with context-menu-based removal capability, and a SitePermissionsPanel modal for viewing and revoking per-host site permission grants. Both include comprehensive test suites and are integrated into the app's layout and settings.

Changes

BookmarksBar Feature

Layer / File(s) Summary
Core Component
desktop/src/apps/BrowserApp/BookmarksBar.tsx
BookmarksBar loads bookmarks for a profile, renders chips with favicons and truncated titles, handles left-click navigation, right-click context menus, and removal via removeBookmark API. Includes sequence guard for load race conditions and taos-browser:bookmark-changed event dispatch to avoid re-processing own changes.
Integration
desktop/src/apps/BrowserApp/BrowserApp.tsx
BookmarksBar component is mounted in the desktop layout after AddressBar and before TabStrip, passing windowId and profileId.
Tests
desktop/src/apps/BrowserApp/BookmarksBar.test.tsx
Verifies rendering for empty/populated lists, chip navigation via navigateTab, context-menu removal flow, post-removal UI update, and title truncation.

SitePermissionsPanel Feature

Layer / File(s) Summary
API Client
desktop/src/lib/browser-site-permissions-api.ts
New module with listSitePermissions(profileId) and revokeSitePermission(profileId, hostPattern, permission) fetch wrappers; both include credentials and gracefully degrade to empty list or false on error.
Core Component
desktop/src/apps/BrowserApp/SitePermissionsPanel.tsx
Modal component that fetches and displays permission grants in a table. Supports revoking individual permissions, displays loading/empty/error states, closes via backdrop or Escape, and reloads after successful revoke.
Integration
desktop/src/apps/BrowserApp/SettingsPanel.tsx
Adds sitePermsOpen state and "Site permissions" button; extends Escape-key handler to close sitePermsOpen before closing the settings panel; conditionally renders SitePermissionsPanel with callback to set state.
Tests
desktop/src/apps/BrowserApp/SitePermissionsPanel.test.tsx
Verifies loading/empty states, table rendering, revoke success with list reload, revoke failure with error display, modal closure via Escape and backdrop, and accessibility attributes (aria-modal, aria-label).

Sequence Diagrams

sequenceDiagram
    participant User
    participant BookmarksBar as BookmarksBar<br/>Component
    participant Store as useBrowserStore
    participant API as bookmarks-api
    participant Events as EventTarget

    rect rgba(100, 150, 255, 0.5)
    Note over User,Events: Navigation Flow
    User->>BookmarksBar: Click chip
    BookmarksBar->>Store: navigateTab(windowId, tabId, url)
    Store->>Store: Update active tab URL
    end

    rect rgba(100, 200, 100, 0.5)
    Note over User,Events: Removal Flow
    User->>BookmarksBar: Right-click chip
    BookmarksBar->>BookmarksBar: Show context menu at cursor
    User->>BookmarksBar: Click "Remove bookmark"
    BookmarksBar->>API: removeBookmark(profileId, bookmarkId)
    API-->>BookmarksBar: success/failure
    BookmarksBar->>BookmarksBar: Update local bookmarks state
    BookmarksBar->>Events: Dispatch taos-browser:bookmark-changed<br/>(source: "bookmarks-bar")
    end
Loading
sequenceDiagram
    participant User
    participant SitePermsPanel as SitePermissionsPanel<br/>Component
    participant API as site-permissions-api
    
    rect rgba(100, 150, 255, 0.5)
    Note over User,API: Load and Display
    User->>SitePermsPanel: Modal opens
    SitePermsPanel->>API: listSitePermissions(profileId)
    API-->>SitePermsPanel: grants[]
    SitePermsPanel->>SitePermsPanel: Render permission table
    end

    rect rgba(200, 100, 100, 0.5)
    Note over User,API: Revoke Permission
    User->>SitePermsPanel: Click revoke button
    SitePermsPanel->>API: revokeSitePermission(profileId,<br/>hostPattern, permission)
    API-->>SitePermsPanel: true/false
    alt Success
        SitePermsPanel->>SitePermsPanel: Reload grants
    else Failure
        SitePermsPanel->>SitePermsPanel: Show inline error
    end
    end

    rect rgba(150, 150, 150, 0.5)
    Note over User,API: Close
    User->>SitePermsPanel: Press Escape or click backdrop
    SitePermsPanel->>SitePermsPanel: Call onClose callback
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • jaylfc/tinyagentos#309: This PR implements the backend bookmarks and site-permissions routes and stores that are called by the API clients and components added here.

Poem

🐰 Bookmarks bundled in a bar so neat,
Permissions listed, oh what a treat!
Context menus and modals appear,
Management features crystal clear,
Testing assured from end to start,
A browser feature, beautifully smart!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main additions: BookmarksBar and SitePermissionsPanel components as follow-ups to PR 9.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/browser-pr-9-followup-ui

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
desktop/src/apps/BrowserApp/BookmarksBar.tsx (2)

51-73: ⚡ Quick win

Wrap load in useCallback and add it to both useEffect dependency arrays

load closes over profileId but is re-created on every render and omitted from both effects' dependency arrays. This triggers react-hooks/exhaustive-deps lint errors and creates a narrow race: if profileId changes and the component re-renders (from another state update) before the second effect re-registers, the bookmark-changed handler still holds the previous load closure — meaning a concurrent event could trigger a fetch with the stale profileId.

♻️ Proposed fix
- async function load() {
-   const seq = ++loadSeqRef.current;
-   const list = await listBookmarks(profileId);
-   if (seq !== loadSeqRef.current) return;
-   setBookmarks(list);
- }
+ const load = useCallback(async () => {
+   const seq = ++loadSeqRef.current;
+   const list = await listBookmarks(profileId);
+   if (seq !== loadSeqRef.current) return;
+   setBookmarks(list);
+ }, [profileId]);

  useEffect(() => {
    load();
- }, [profileId]);
+ }, [load]);

  useEffect(() => {
    const handler = (e: Event) => {
      const ce = e as CustomEvent<{ profileId: string; url: string; bookmarkId: string | null; source?: string }>;
      if (ce.detail.profileId !== profileId) return;
      if (ce.detail.source === "bookmarks-bar") return;
      load();
    };
    window.addEventListener("taos-browser:bookmark-changed", handler);
    return () => window.removeEventListener("taos-browser:bookmark-changed", handler);
- }, [profileId]);
+ }, [load, profileId]);
🤖 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 `@desktop/src/apps/BrowserApp/BookmarksBar.tsx` around lines 51 - 73, The load
function closes over profileId and should be memoized so effects and the event
handler always call the current fetch; wrap load in useCallback with profileId
in its dependency array (keeping the existing loadSeqRef, listBookmarks and
setBookmarks usage inside), then add that memoized load to both useEffect
dependency arrays (the initial load effect and the bookmark-changed event
effect) so the event handler is re-registered with the latest load closure and
cleanup still removes the previous listener.

140-149: 💤 Low value

faviconUrl(bm.url) is called twice per chip on each render

Cache the result in the .map() callback to avoid the redundant new URL(...) parse.

♻️ Proposed fix
- {bookmarks.map((bm) => (
+ {bookmarks.map((bm) => {
+   const favicon = faviconUrl(bm.url);
+   return (
    <button
      key={bm.bookmark_id}
      ...
    >
-     {faviconUrl(bm.url) && (
+     {favicon && (
        <img
-         src={faviconUrl(bm.url)}
+         src={favicon}
          alt=""
          ...
        />
      )}
      <span>{truncate(bm.title)}</span>
    </button>
- ))}
+   );
+ })}
🤖 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 `@desktop/src/apps/BrowserApp/BookmarksBar.tsx` around lines 140 - 149, The
code calls faviconUrl(bm.url) twice per bookmark chip which repeats URL parsing;
inside the .map callback that renders each bookmark (where bm is used), call
faviconUrl(bm.url) once, store it in a local constant (e.g., const favicon =
faviconUrl(bm.url)) and then use that constant for both the conditional and the
img src to avoid redundant new URL(...) parses and improve performance.
desktop/src/apps/BrowserApp/BookmarksBar.test.tsx (1)

1-119: ⚡ Quick win

Consider adding tests for the taos-browser:bookmark-changed event listener

The bookmark-changed listener has three branches (wrong profileId → skip; source === "bookmarks-bar" → skip; valid event → reload) that are not covered. This is the same guard path that prevents double-reloads when the bar removes a bookmark itself.

Example skeleton:

it("reloads on bookmark-changed event from another source", async () => {
  vi.mocked(bookmarksApi.listBookmarks)
    .mockResolvedValueOnce([makeBookmark()])   // initial load
    .mockResolvedValueOnce([makeBookmark(), makeBookmark({ bookmark_id: "bm-2", title: "New" })]);

  render(<BookmarksBar windowId={WINDOW_ID} profileId={PROFILE_ID} />);
  await waitFor(() => screen.getByText("Example Site"));

  act(() => {
    window.dispatchEvent(new CustomEvent("taos-browser:bookmark-changed", {
      detail: { profileId: PROFILE_ID, url: "https://new.com", bookmarkId: "bm-2" },
    }));
  });

  await waitFor(() => screen.getByText("New"));
});

it("ignores bookmark-changed events with source bookmarks-bar", async () => {
  vi.mocked(bookmarksApi.listBookmarks).mockResolvedValue([makeBookmark()]);
  render(<BookmarksBar windowId={WINDOW_ID} profileId={PROFILE_ID} />);
  await waitFor(() => screen.getByText("Example Site"));

  act(() => {
    window.dispatchEvent(new CustomEvent("taos-browser:bookmark-changed", {
      detail: { profileId: PROFILE_ID, url: "https://example.com", bookmarkId: null, source: "bookmarks-bar" },
    }));
  });

  // listBookmarks should still only have been called once (the initial load)
  expect(bookmarksApi.listBookmarks).toHaveBeenCalledTimes(1);
});
🤖 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 `@desktop/src/apps/BrowserApp/BookmarksBar.test.tsx` around lines 1 - 119, Add
tests exercising the component's "taos-browser:bookmark-changed" listener to
cover the three guard branches: when the event has a mismatched profileId
(should be ignored), when event.detail.source === "bookmarks-bar" (should be
ignored to avoid double-reload), and when a valid external change occurs (should
call listBookmarks again and update UI). Use
vi.mocked(bookmarksApi.listBookmarks).mockResolvedValueOnce(...) to return an
initial list then a changed list for the reload case, render <BookmarksBar ...
/> to attach the listener, and dispatch window.dispatchEvent(new
CustomEvent("taos-browser:bookmark-changed", { detail: { profileId:
PROFILE_ID|other, bookmarkId, url, source } })) inside act() to trigger the
branches; assert that bookmarksApi.listBookmarks call count and DOM updates
match expectations.
🤖 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 `@desktop/src/apps/BrowserApp/SitePermissionsPanel.tsx`:
- Around line 53-64: The handleRevoke flow allows duplicate clicks; add a
pending flag to block re-entry and disable the UI until the request finishes:
introduce a state like isRevoking (or a map keyed by grant.host_pattern for
per-row tracking) used in handleRevoke and the revoke button disabled prop, set
the flag true before calling revokeSitePermission(profileId, grant.host_pattern,
grant.permission) and clear it in both the success path (after await load()) and
the catch path, and ensure error handling still calls setError; apply the same
pattern to the other revoke handler referenced at lines 133-137.

In `@desktop/src/lib/browser-site-permissions-api.ts`:
- Around line 20-31: The function listSitePermissions currently returns an empty
array on non-OK responses and exceptions, hiding server/auth errors; change it
to surface failures by throwing an Error when resp.ok is false (include
resp.status and resp.statusText or response body error) and by rethrowing or
throwing a descriptive Error in the catch block so the caller can distinguish
"no permissions" from "load error"; update the code around the resp.ok check in
listSitePermissions and the catch handler to throw meaningful errors (and ensure
callers handle Promise rejections).

---

Nitpick comments:
In `@desktop/src/apps/BrowserApp/BookmarksBar.test.tsx`:
- Around line 1-119: Add tests exercising the component's
"taos-browser:bookmark-changed" listener to cover the three guard branches: when
the event has a mismatched profileId (should be ignored), when
event.detail.source === "bookmarks-bar" (should be ignored to avoid
double-reload), and when a valid external change occurs (should call
listBookmarks again and update UI). Use
vi.mocked(bookmarksApi.listBookmarks).mockResolvedValueOnce(...) to return an
initial list then a changed list for the reload case, render <BookmarksBar ...
/> to attach the listener, and dispatch window.dispatchEvent(new
CustomEvent("taos-browser:bookmark-changed", { detail: { profileId:
PROFILE_ID|other, bookmarkId, url, source } })) inside act() to trigger the
branches; assert that bookmarksApi.listBookmarks call count and DOM updates
match expectations.

In `@desktop/src/apps/BrowserApp/BookmarksBar.tsx`:
- Around line 51-73: The load function closes over profileId and should be
memoized so effects and the event handler always call the current fetch; wrap
load in useCallback with profileId in its dependency array (keeping the existing
loadSeqRef, listBookmarks and setBookmarks usage inside), then add that memoized
load to both useEffect dependency arrays (the initial load effect and the
bookmark-changed event effect) so the event handler is re-registered with the
latest load closure and cleanup still removes the previous listener.
- Around line 140-149: The code calls faviconUrl(bm.url) twice per bookmark chip
which repeats URL parsing; inside the .map callback that renders each bookmark
(where bm is used), call faviconUrl(bm.url) once, store it in a local constant
(e.g., const favicon = faviconUrl(bm.url)) and then use that constant for both
the conditional and the img src to avoid redundant new URL(...) parses and
improve performance.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 24658784-dd7a-4687-912d-9306e1ae08ca

📥 Commits

Reviewing files that changed from the base of the PR and between f8d8e82 and 1e01987.

📒 Files selected for processing (7)
  • desktop/src/apps/BrowserApp/BookmarksBar.test.tsx
  • desktop/src/apps/BrowserApp/BookmarksBar.tsx
  • desktop/src/apps/BrowserApp/BrowserApp.tsx
  • desktop/src/apps/BrowserApp/SettingsPanel.tsx
  • desktop/src/apps/BrowserApp/SitePermissionsPanel.test.tsx
  • desktop/src/apps/BrowserApp/SitePermissionsPanel.tsx
  • desktop/src/lib/browser-site-permissions-api.ts

Comment thread desktop/src/apps/BrowserApp/SitePermissionsPanel.tsx
Comment thread desktop/src/lib/browser-site-permissions-api.ts
- listSitePermissions now throws on non-2xx / network failure instead of
  returning []. The panel had explicit error UX that was unreachable
  because the wrapper collapsed all failures into 'no permissions yet'.
  Reads now surface real failures so the user sees the retry message.
- handleRevoke now guards against duplicate clicks while a request is in
  flight via a revokingKey state. The button is disabled across all
  rows during revoke so a fast double-click can't fire two DELETEs for
  the same row or interleave revokes for different rows.

Note: CR/Kilo's CRITICAL on BookmarksBar.test.tsx:117 was a false
positive — both reviewers miscounted. truncate(20) does slice(0,20)
which yields chars 0-19 = 'A Very Long Title Th' (20 chars), then
appends '…'. Test correctly expects 'A Very Long Title Th…'.
@jaylfc
Copy link
Copy Markdown
Owner Author

jaylfc commented May 5, 2026

@coderabbitai @kilo-code-bot heads up — pushed 8183206 addressing two of the three findings:

Major (CR): browser-site-permissions-api.ts — listSitePermissions now throws on non-2xx / network failure instead of returning []. The panel had explicit error UX that was unreachable before because the wrapper collapsed every failure into "no permissions yet".

Minor (CR): SitePermissionsPanel.tsx — added revokingKey state that gates handleRevoke against duplicate clicks. Button is disabled while any revoke is in flight.

CRITICAL (CR + Kilo, BookmarksBar.test.tsx:117): this is a false positive — both of you miscounted by one character. The test is correct as written.

Input:  "A Very Long Title That Should Be Truncated"
Indices: 0 1 2 3 4 5 6 7 8 9 ...
         A   V e r y   L o n g   T i t l e   T h a t ...
         0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

slice(0, 20)  →  chars [0..19]  →  "A Very Long Title Th"  (20 chars)
+ "…"          →  "A Very Long Title Th…"

The test expects exactly "A Very Long Title Th…", which matches the implementation. Vitest 789/789 passing locally confirms it. Adopting your suggestion would have broken the test.

Please re-review.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

Tip

For best results, initiate chat on the files or code changes.

@jaylfc — thanks for the detailed breakdown and the fixes.

Fix 1 (Major — listSitePermissions throwing on failure): ✅ Confirmed. Making the wrapper propagate errors is the right call; the silent [] return was genuinely masking the panel's error UI.

Fix 2 (Minor — revokingKey double-click guard): ✅ Confirmed. Good approach using a keyed state rather than a simple boolean so concurrent revokes on different rows are handled correctly.

Fix 3 (Truncation test — false positive): You're right, I apologize for the noise. Manually re-checking:

"A Very Long Title That Should Be Truncated"
 [0]A [1]  [2]V [3]e [4]r [5]y [6]  [7]L [8]o [9]n [10]g [11]  [12]T [13]i [14]t [15]l [16]e [17]  [18]T [19]h
slice(0, 20) → "A Very Long Title Th"  (indices 0–19, 20 chars)
+ "…"        → "A Very Long Title Th…"

That matches the test expectation exactly. The review comment had an off-by-one error on my end — the test and implementation are both correct. The 789/789 passing suite is the ground truth here.


✏️ Learnings added
Learnt from: jaylfc
URL: https://github.com/jaylfc/tinyagentos/pull/313

Timestamp: 2026-05-05T16:54:17.041Z
Learning: In `desktop/src/apps/BrowserApp/BookmarksBar.tsx`, the `truncate(text, max = 20)` helper uses `text.slice(0, max) + "…"`, producing a result of `max` chars from the original string followed by the ellipsis character. The corresponding test in `BookmarksBar.test.tsx` correctly expects `"A Very Long Title Th…"` for a 20-char truncation. A prior review comment flagging this as wrong was a false positive.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@jaylfc jaylfc merged commit 234387f into master May 5, 2026
8 checks passed
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