Skip to content

experiment: anchored tabs (name TBD) - #1578

Draft
Umbranoxio wants to merge 25 commits into
imputnet:mainfrom
Umbranoxio:anchored-tabs
Draft

experiment: anchored tabs (name TBD)#1578
Umbranoxio wants to merge 25 commits into
imputnet:mainfrom
Umbranoxio:anchored-tabs

Conversation

@Umbranoxio

@Umbranoxio Umbranoxio commented May 4, 2026

Copy link
Copy Markdown

For your pull request to not get closed without review, please confirm that:

  • An issue exists where the maintainers agreed that this should be implemented
    (an approved feature request, or confirmed bug).
  • I tested that my contribution works locally, and does not break anything,
    otherwise I have marked my PR as draft.
  • If my contribution is non-trivial, I did not use AI to write most of it. (gh needs indeterminate checkboxes... unsure what the benchmark for "most of it" is but it was used for assistance to help familiarize me with parts of chromium / heliums toolchain i didn't understand, but not in an agentic / vibe codey way, a lot of what i wrote was by hand with some autocomplete and chatting on the side. if y'all feel this crosses the "most of it" line, i apologize)
  • I understand that I will be permanently banned from interacting with this
    organization if I lied by checking any of these checkboxes.

Tested on (check one or more):

  • Windows
  • macOS
  • Linux

this pr adds an experiment i've just been calling anchored-tabs... it's pretty similar arc/zen style tab handling so they work more like "bookmarks" or "folders" -- i focused primarily at the vertical tab experience

here's a lil video showing some of it off

zenium.lol.mp4

this was a weekend project but i've been working on how i'd approach this in my head for quite some time now, i'll be using my own fork (which just has these changes) as my daily driver from here on out so if i spot any issues i'll try and maintain it as much as i can

sorry for the yap but also worth noting; lots of hacks were required to make this work the way it does, i understand & respect this is a very large set of changes coming outa nowhere and i had considered just keeping it for myself so if it's too much of a burden i'd understand if this was put on the backburner and/or picked apart to be repurposed for a nicer PR in the future

What's missing / unpolished(?)

i'm sure upon review more will be found but:

  • some missing i18n
  • flag / setting awkwardness
    • if just the flag is set the user doesn't get the "full experience" as it piggy-backs off of the functionality of Continue where you left off, so what the new On startup setting i've added [Experimental] Continue where you left off with tab anchoring does is enable the anchored tabs flag while also acting like Continue where you left off... the fact that this was such a mouthful to explain definitely makes me think this needs a different approach
  • proper regression testing -- this is a pretty dang large set of changes, i ran it through some basic testing but more could be desired.. thorough testing in other layouts, playing with toggling the flag / experimental setting on & off in different states definitely needs to be done more in depth
  • "spaces"
    -definitely out of scope, but feels missing when put into the context of what this PR is trying to achieve
  • alternative multi window handling (tabs persisting per window like zen)
    • same as above; feels a smidge out of scope, but with these additions it feels like it should be a thing

Changelog

available on request, y'all seem to want short descriptions and this is already super long so i've amended it


Resolves #172 resolves #258 resolves #999 resolves #1240 resolves #1388 resolves #1404 resolves #1444 resolves #1476 resolves #1501 resolves #1503 (some only partially)

@greptile-apps

greptile-apps Bot commented May 4, 2026

Copy link
Copy Markdown

Reviews (1): Last reviewed commit: "helium/anchored-tabs: refresh patches fo..." | Re-trigger Greptile

tab.extra_data, true /* from_session_restore */, is_active_browser);
DCHECK(web_contents);

- TabUIHelper::RestoreCustomTitleFromExtraData(web_contents, tab.extra_data);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Dead variable: restored_tab_offset is always 0

restored_tab_offset is declared as 0 and never mutated, so tab_index = i + initial_tab_count + restored_tab_offset is identical to tab_index = i + initial_tab_count. The variable appears to have been intended to offset restored-tab indices by 1 to account for the anchored NTP inserted at position 0 during helium_anchor_restore, but the offset was never set. Either the offset should be set to 1 when helium_anchor_restore is true (so restored tabs land after the NTP), or the variable should be removed and the original expression restored. Without the offset, the NTP at index 0 gets displaced to a non-deterministic position as subsequent restored tabs are inserted at indices 0, 1, 2, ….

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

fixed

Comment on lines +502 to +503
+ break;
+ }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Dialog title uses context-menu string; placeholder text is hardcoded

ShowTabRenameDialog sets the dialog title to IDS_TAB_CXMENU_RENAMETAB ("Rename tab…" with a trailing ellipsis), but a dedicated IDS_TAB_RENAME_DIALOG_TITLE string ("Rename tab", no ellipsis) is already defined in the same GRD patch for exactly this purpose. Similarly, the textfield label is hardcoded as u"New tab name" rather than using IDS_TAB_RENAME_DIALOG_PLACEHOLDER. Both i18n strings are declared but unused, which means they are dead string resources and the dialog text cannot be localised. The SetTitle call should reference IDS_TAB_RENAME_DIALOG_TITLE, and the AddTextfield label should use l10n_util::GetStringUTF16(IDS_TAB_RENAME_DIALOG_PLACEHOLDER).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

oops, cleaned that up

Comment on lines +1443 to +1469
+ const bool should_defer_navigation =
+ (features::IsHeliumAnchoredTabsEnabled() && !local_tab_->IsActivated() &&
+ contents()->WasDiscarded()) ||
+ (data_sharing::features::IsDataSharingFunctionalityEnabled() &&
+ !local_tab_->IsActivated());
+ if (!should_defer_navigation) {
PerformNavigation(url);
} else {
favicon::FaviconService* favicon_service =
@@ -278,6 +284,14 @@ void SavedTabGroupWebContentsListener::D
SavedTabGroupTab* tab = group->GetTab(local_tab_id());
CHECK(tab);

+ if (features::IsHeliumAnchoredTabsEnabled()) {
+ TabUIHelper* const helper = TabUIHelper::From(local_tab_);
+ if (helper && !helper->anchored_url().is_valid()) {
+ helper->SetAnchoredURL(tab->url(), tab->title());
+ }
+ return;
+ }
+
service_->NavigateTab(group->local_group_id().value(), local_tab_id(),
contents()->GetURL(), contents()->GetTitle());

Index: src/chrome/browser/ui/tabs/tab_menu_model.cc
===================================================================
--- src.orig/chrome/browser/ui/tabs/tab_menu_model.cc

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 DeferredNavigationCommitted skips tab-group sync service notification when anchored tabs is enabled

The early return added here prevents service_->NavigateTab(...) from being called after a deferred navigation commits in a saved tab group. As a result, the TabGroupSyncService never learns about the navigation, so the saved-tab-group cloud state retains the stale URL indefinitely. For shared (synced) tab groups this means collaborators see an outdated URL for the tab for the lifetime of the session. The SetAnchoredURL call only updates in-memory state; calling service_->NavigateTab(...) afterward (even with the anchored URL rather than the current URL) would keep sync consistent without breaking the anchored-URL invariant.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

this is intentional.. anchored tabs should keep their saved url stable while the live tab can still navigate, syncing contents()->GetURL() here would replace the anchor

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

That's a fair point — the whole semantic of an anchor is that the saved URL stays fixed regardless of where the live tab navigates, so propagating the current URL back to sync would break that invariant. Understood.

The one edge case worth keeping in mind: if a user explicitly changes the anchor (e.g. sets a new anchor URL), you'd want that to sync out. As long as SetAnchoredURL with persist=true routes through service_->NavigateTab (or an equivalent) for that case, this is fine.

Tip: You can give me more context with .greptile/rules.md so I get this right next time.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

yep, the command, SetAnchoredURL updates local/session state, then SetAnchoredURLToCurrent calls NavigateTab with the new anchor

Comment on lines +2814 to +2852
+ for (int i = model_index + 1; i < model->count(); ++i) {
+ if ((!target_group.has_value() ||
+ model->GetTabGroupForTab(i) != target_group) &&
+ model->GetWebContentsAt(i) &&
+ !model->GetWebContentsAt(i)->WasDiscarded()) {
+ return i;
+ }
+ }
+ for (int i = model_index - 1; i >= 0; --i) {
+ if ((!target_group.has_value() ||
+ model->GetTabGroupForTab(i) != target_group) &&
+ model->GetWebContentsAt(i) &&
+ !model->GetWebContentsAt(i)->WasDiscarded()) {
+ return i;
+ }
+ }
+ if (model_index + 1 < model->count()) {
+ content::WebContents* const contents =
+ model->GetWebContentsAt(model_index + 1);
+ if (contents && !contents->WasDiscarded()) {
+ return model_index + 1;
+ }
+ }
+ if (model_index - 1 >= 0) {
+ content::WebContents* const contents =
+ model->GetWebContentsAt(model_index - 1);
+ if (contents && !contents->WasDiscarded()) {
+ return model_index - 1;
+ }
+ }
+
+ return std::nullopt;
+}
+
+bool ActivateFallbackTab(TabStripModel* model,
+ int model_index,
+ std::optional<tab_groups::TabGroupId> target_group) {
+ const std::optional<int> fallback_index =
+ FindFallbackTabIndex(model, model_index, target_group);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 FindFallbackTabIndex: last two if blocks are redundant with the preceding loops

The first two for loops already iterate over model_index + 1 … count()-1 and model_index - 1 … 0, so the two trailing if checks for model_index ± 1 are always reached only after those exact indices have already been visited. The difference is that the trailing blocks omit the target_group filter, which means they can return a tab in the same group as the one being hibernated — but only if it is non-discarded. This subtle fallback is not documented and may be unintentional; if the intent really is "return any adjacent non-discarded tab regardless of group", that logic could be pulled into a clearly named third pass to avoid confusion.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

this is also intentional, the first passes prefer a tab outside the group, but the final fallback avoids opening a new tab when a usable live tab is right next to it

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

That makes sense — the two-tier strategy is clear once you know it: prefer a tab outside the group, then fall back to any adjacent live tab to avoid spawning a new one.

A short comment above those final if blocks would make this obvious to future readers without requiring them to reason it out. Something like:

// Fallback: if no out-of-group tab exists, accept any adjacent live tab
// to avoid opening a new tab unnecessarily.

Otherwise, happy to defer to your judgement here.

Tip: You can give me more context with .greptile/rules.md so I get this right next time.

@ErdemGKSL

Copy link
Copy Markdown

I recently switched zen to helium, loved this pr, waiting for this patch!

@liranRitmus

Copy link
Copy Markdown

extreamilty improtant feature! looking forward might be what i missing to do the transisiton

@AleyaDevelopment

Copy link
Copy Markdown

To me this fits as a pinned tab in terms of naming convention. I think Zen got this right with how they name items. I feel like a pinned tab means the original URL should be stored and resetable.

@Umbranoxio

Copy link
Copy Markdown
Author

as mentioned in the original pr i've been using this as my daily driver for about 3 weeks, it's been a pleasant experience and i haven't felt the need to reach for zen (my previous daily driver) since switching

a few bugs (one resulting in an easily reproducible crash) but nothing that's affected my day to day too harshly

some parts of the experiment is still rough around the edges and the aforementioned bugs still need to be addressed. I have a busy period ahead of me for the next 2 - 3 weeks so until i'm able to give this some love i'll be marking the pr as a draft as it's definitely not in the state where it's fit for merge

@dumbmoron / @wukko any feedback would be greatly appreciated for me to come back to after my busy period and if you'd like to further cleanup your prs feel free to close this one and i'll request y'all to re-open this one / i can make a new one once i've made some much needed changes

@Umbranoxio
Umbranoxio marked this pull request as draft May 22, 2026 14:50
@vlnd0

vlnd0 commented May 25, 2026

Copy link
Copy Markdown

@wukko mate, hi — I'm really interested in this review and happy to help if needed. Let me know.

@vlnd0

vlnd0 commented May 29, 2026

Copy link
Copy Markdown

Hi @Umbranoxio — built the macOS version from your PR today, this is huge! One small ask: could standalone tabs be anchorable too, not only ones inside tab groups?

@vlnd0

vlnd0 commented May 30, 2026

Copy link
Copy Markdown

make update to mr
Umbranoxio#1

made standalone tabs be able to be anchorable too

@turnipsforme

Copy link
Copy Markdown

This is AWESOME!! Would be huge for folks coming from Dia/Arc - super excited to see it implemented, thank you for your hard work!

@vlnd0

vlnd0 commented Jun 16, 2026

Copy link
Copy Markdown

guys, this is totaly musthave, do ya need any help?

@tisonkun

Copy link
Copy Markdown

May a pinned tab have no group? I found that forcing the creation of a group before pinning any tab is an issue.

Besides, Ctrl+D or another shortcut for pinning a tab can help.

@Chaoscontrol

Copy link
Copy Markdown

As others have said, this PR is the thing keeping me from ditching Zen and FINALLY finding a Chrome replacement to Arc.
Pinned tabs with resettable URL.

It's the single must-have feature that I expect from any browser.

I'm not familiar of how to build it to start using it, even if rough, but I'll definitely do some research and start testing this.

Thank you SO much.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

8 participants