Draft
Conversation
Add a new pinned_tabs table to the drizzle schema with columns for unique_id (PK), profile_id, default_url, favicon_url, and position. Includes an index on profile_id for efficient per-profile queries. Generated the corresponding SQL migration (0001_tough_scream.sql).
Define PersistedPinnedTabData (DB-backed fields: uniqueId, profileId, defaultUrl, faviconUrl, position) and PinnedTabData (extends with runtime associatedTabId for live browser tab association).
Full controller with in-memory cache backed by SQLite, supporting: - CRUD operations (create, remove, reorder, get by profile) - Runtime tab associations via bidirectional maps (pinned <-> browser tab) - Position normalization to keep ordering consistent - Change listener system for broadcasting updates to renderer - Singleton export for use across main process
Add pinned-tabs IPC handlers for: get-data, create-from-tab, click, double-click, remove, reorder, and show-context-menu. Register the handler module in the IPC index. Wire tab destruction events to break pinned tab associations. Load pinned tabs from DB at app startup.
Add FlowPinnedTabsAPI interface with methods for getData, createFromTab, click, doubleClick, remove, reorder, showContextMenu, and onChange. Implement the API in the preload script mapping to IPC channels, wrap with browser permissions, and add pinnedTabs to the global flow type.
Create PinnedTabsProvider with usePinnedTabs hook that manages per-profile pinned tab state, listens for changes via flow.pinnedTabs.onChange, and exposes actions (create, click, doubleClick, remove, reorder, contextMenu). Mount the provider inside TabsProvider in the browser UI component tree.
Rewrite PinGrid to use real data from usePinnedTabs(), accept tab-group drags to create pinned tabs, and show an empty state with drag hint. Rewrite PinnedTabButton to accept PinnedTabData with click/double-click/ context-menu handlers, drag-and-drop reordering between pinned tabs using left/right edge detection, active state gradient borders via useFaviconColors, and dragging opacity feedback.
Replace the broken PinnedTabButton reference with a self-contained SlotButton component that replicates the visual appearance (favicon with gradient border on active/winner state) without requiring any pinned-tab-specific props like drag-and-drop or context menus.
Contributor
Build artifacts for all platforms are ready! 🚀Download the artifacts for: One-line installer (Unstable):bunx flow-debug-build --open 22533891881(execution 22533891881 / attempt 1) |
This is a desktop app, not a website — pointer cursor is inappropriate for interactive elements.
When a browser tab is associated with a pinned tab, it's already visible in the pin grid — showing it again in the tab list below is redundant. Filter out associated tabs at the SpaceContentPage level before rendering tab groups. Associations are runtime-only (in-memory maps, never persisted), so on app restart all tabs reappear in the list until re-associated.
Greptile SummaryImplements a comprehensive pinned tabs feature with persistent URL shortcuts displayed in the sidebar. Users can drag browser tabs to pin them, click to activate/create associated tabs, double-click to navigate back to the default URL, and reorder via drag-and-drop. Architecture:
Key implementation details:
Confidence Score: 5/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant PinGrid as Pin Grid (React)
participant Provider as PinnedTabsProvider
participant Preload as Flow API
participant IPC as IPC Handler
participant Controller as PinnedTabsController
participant TabsCtrl as TabsController
User->>PinGrid: Click pinned tab button
PinGrid->>Provider: click(pinnedTabId)
Provider->>Preload: flow.pinnedTabs.click()
Preload->>IPC: invoke("pinned-tabs:click")
alt Associated tab exists
IPC->>Controller: getAssociatedTabId()
Controller-->>IPC: tabId
IPC->>TabsCtrl: getTabById(tabId)
TabsCtrl-->>IPC: tab
IPC->>TabsCtrl: setActiveTab(tab)
IPC-->>Preload: true
else No associated tab
IPC->>Controller: getById(pinnedTabId)
Controller-->>IPC: pinnedTab
IPC->>TabsCtrl: createTab(defaultUrl, ephemeral: true)
TabsCtrl-->>IPC: newTab
IPC->>Controller: associateTab(pinnedTabId, newTab.id)
Controller->>Controller: Update associations map
Controller->>Controller: notifyChanged()
IPC->>TabsCtrl: setActiveTab(newTab)
IPC-->>Preload: true
end
Preload-->>Provider: true
Note over Controller: Change notification triggers
Controller->>IPC: onChanged callback
IPC->>Preload: send("pinned-tabs:on-changed")
Preload->>Provider: onChanged(allByProfile)
Provider->>Provider: Update state
Provider->>PinGrid: Re-render with updated data
Last reviewed commit: 4e7d8cd |
The isActive check was comparing associatedTabId === focusedTabId, but both are null on launch (no associations, no focused tab), causing null === null to be true for every pinned tab. Now requires associatedTabId to be non-null before comparing.
Add an 'ephemeral' flag to Tab that prevents persistence to the database. Ephemeral tabs: - Are never written to the tabs table (markDirty is skipped) - Are never saved to recently closed on destroy - Are filtered out of tab data sent to the renderer (no sidebar flash) - Do not survive app restart When a tab is dragged to pin, makeTabEphemeral() removes its existing DB row and triggers a structural refresh so the renderer drops it from the tab list. When a pinned tab creates a new associated tab via click, it's created with ephemeral: true from the start.
…border works getWindowTabsData() was building the space/profile sets from visibleTabs (ephemeral-filtered), causing focusedTabs/activeTabs maps to miss spaces whose only tab is ephemeral. The pin grid's active border check then failed because focusedTabId was undefined for the current space. Fix: iterate over all tabs (including ephemeral) for windowSpaces and windowProfiles, while keeping visibleTabs only for the tabDatas array sent to the sidebar tab list.
…TabData TabsFocusedIdContext was set to focusedTab?.id, where focusedTab was resolved by looking up the ID in tabById (built from tabsData.tabs). Since ephemeral tabs are excluded from tabsData.tabs, any ephemeral focused tab resolved to null, making focusedTabId null in the renderer. The pin grid compares associatedTabId against focusedTabId to show the active border — with focusedTabId always null for ephemeral tabs, the border never appeared. Fix: read the raw numeric ID directly from tabsData.focusedTabIds for the current space. This bypasses the TabData resolution and preserves ephemeral tab IDs that the pin grid needs for active state detection.
Add the ability to drag pinned tab cards from the pin grid back down to the tab list area to unpin them. This: 1. Adds makeTabPersistent() to TabsController — the reverse of makeTabEphemeral(). Re-serializes the tab and marks it dirty so it gets persisted again, then triggers a structural change so the renderer adds it back to the sidebar tab list. 2. Adds pinned-tabs:unpin-to-tab-list IPC handler that removes the pinned tab and makes the associated browser tab persistent again. 3. Wires through the full API chain: Flow interface, preload, and PinnedTabsProvider all expose unpinToTabList(). 4. Updates TabDropTarget to accept 'pinned-tab' type drags in addition to 'tab-group' drags. Dropping a pinned tab on the tab list area calls unpinToTabList() which removes the pin and shows the tab.
Two issues fixed: 1. Pinned tabs could only be dropped on TabDropTarget (bottom of list). Now TabGroup components also accept 'pinned-tab' type drags, so users can drop between existing tabs at any position. 2. Unpinned tabs always appeared at the top of the list. The unpinToTabList call now accepts an optional position parameter that flows through the full stack (Flow API -> preload -> IPC -> controller). The IPC handler sets the tab's position before making it persistent and calls normalizePositions to keep the list contiguous.
…rs, and unpin without association - Add focusedTabUrls to WindowTabsData so the renderer can display the address bar URL for ephemeral (pinned-tab) focused tabs - Escalate ephemeral tab content changes to structural refreshes so focusedTabUrls stays current - Show subtle background highlight on pin grid when dragging a tab over existing pins - Increase empty state border/text opacity from /10 and /30 to /20 and /50 - Create a new persistent tab when unpinning a pinned tab that has no associated browser tab (drag to tab list)
…ed tabs - Context menu 'Unpin' now destroys the associated ephemeral tab so it doesn't remain alive but invisible in the background - Switch PinnedTabButton from onClick to onMouseDown for activation, matching SidebarTab behavior and eliminating ~100-200ms perceived delay
Member
Author
|
@greptile review |
…ndicators Reuses the existing closest-edge drop indicators from pinned tab reordering so that browser tabs dragged onto the pin grid can be inserted at a specific position rather than always appending to the end. The full chain (controller, IPC, Flow API, provider, UI) now supports an optional position parameter on createFromTab.
Position 0 creates a tie with the existing first item, making sort order unpredictable. Using -0.5 ensures correct ordering before normalizePositions reassigns contiguous integers.
Member
Author
|
@greptile review |
Adds PinGridCarousel — a passive follower carousel that renders one PinGrid page per space and smooth-scrolls in sync when the active space changes. The carousel uses overflow-x-hidden (no user swipe) since space switching is driven by the tab list carousel and space switcher. PinGrid now accepts profileId as a prop instead of reading from context, allowing each carousel page to render pins for its space's profile.
Groups consecutive same-profile spaces into one carousel page so switching between them causes no scroll animation. Uses a spaceIndex-to-pageIndex mapping to resolve the correct page for the current space.
…thin the same profile Pinned tabs are per-profile, but associated ephemeral tabs were stuck in the space where they were created. This caused two issues: 1. Clicking a pinned tab in Space B that was opened in Space A would force the user back to Space A instead of showing the tab in Space B. 2. Switching between spaces of the same profile would hide the associated tab. Fix by moving ephemeral pinned-tab-associated tabs to the current space: - On space change: auto-move all ephemeral tabs for the profile to the new space - On click/double-click: move the associated tab to the current space before activating
…idden from carousel
…nsform-based animation The scroll-based carousel used overflow-x: hidden, which per CSS spec forces overflow-y to compute as auto, creating a vertical scroll context that prevented the nested SidebarScrollArea from scrolling. Replaced with CSS transform + transition on an inner wrapper. The outer container uses overflow: clip which doesn't create a scroll context, allowing the SidebarScrollArea's max-h-40 overflow to work correctly.
… div and using flex layout
The <div ref={dropRef}> wrapping SidebarScrollArea broke CSS height resolution
for the Radix Viewport's height: 100%. Move drop target ref onto the grid div
inside the scroll area instead, and switch SidebarScrollArea from height: 100%
to flex-1 min-h-0 so the Viewport height is determined by flex layout (which
honors max-height) rather than percentage resolution (which requires an explicit
height property on the parent).
…tract, and code quality
- Send focusedTabLoadingStates/focusedTabFullscreenStates in WindowTabsData so
loading indicator, reload/stop button, and fullscreen guard work for ephemeral
pinned-tab tabs
- Skip ephemeral tabs in the archive/sleep interval to prevent silent destruction
- Return { ...pinnedTab, associatedTabId } from create-from-tab IPC to match the
PinnedTabData contract
- Fall back to async spacesController.get() when getCached() misses in the
current-space-changed handler
- Convert PinGridCarousel render-time side effect to useEffect
…rkaround maps Include ephemeral tabs (pinned-tab-associated) in the main tabs array with an ephemeral flag on TabData, letting the renderer filter them from the sidebar tab list. This removes the three workaround types/maps (focusedTabUrls, focusedTabLoadingStates, focusedTabFullscreenStates) and their plumbing, simplifying the data flow for ephemeral tab state.
Member
Author
|
@greptile review |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements a full Pinned Tabs feature — persistent URL shortcuts displayed in the sidebar pin grid. Users can drag browser tabs onto the grid to pin them, click to activate/create associated tabs, double-click to navigate back to the default URL, and reorder via drag-and-drop.
Changes
Database & Types
pinned_tabstable to drizzle schema (unique_id,profile_id,default_url,favicon_url,position) with profile indexdrizzle/0001_tough_scream.sql)PersistedPinnedTabDataandPinnedTabDatatypes (runtimeassociatedTabIdfor live tab tracking)Main Process
get-data,create-from-tab,click,double-click,remove,reorder,show-context-menu(with native context menu for Unpin/Reset to Default/Copy URL)Preload / Flow API
FlowPinnedTabsAPIinterface with full method set +onChangedcallbackwrapAPI(pinnedTabsAPI, "browser")permissionspinnedTabsto globalflowtype declarationRenderer
flow.pinnedTabs.onChanged, exposes all actionsTabsProviderin browser UI component treeusePinnedTabs(), accepts tab-group drags to create pinned tabs, shows empty state with drag hintuseFaviconColors, dragging opacitySpaceContentPagelevel), since they're already visible in the pin gridSlotButtoncomponent that replicates the visual style without pinned-tab-specific logicUI Polish
cursor-pointerfrom pinned tab buttons — this is a desktop app, not a websiteKey Design Decisions
tabs— pinned tabs are conceptually distinct persistent bookmarks, not browser tabsVerification
bun run typecheckpasses cleanly (bothtypecheck:nodeandtypecheck:web, zero errors)