Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,8 @@ you to another of its own hosts, too wide and a suffix test lets
file to the name a site is remembered under, what a rule covers and what it must
not, the third state (a field nobody set follows the browser-wide switch rather
than meaning "off"), a round trip through the disk, a hand-edited file that cannot
stop the browser starting, and the alternation the switch key depends on. It
also compiles `src/nui/ProxyAddress.cs` (issue #97) and holds it to the one
stop the browser starting, the round of favourites the switch key walks and the
two-site alternation it falls back to. It also compiles `src/nui/ProxyAddress.cs` (issue #97) and holds it to the one
thing that must not go wrong there: the proxy login reaches the engine's login
call and nothing that is shown, logged or served on `:8081`. Needs only the
.NET 6 SDK under `~/.dotnet-local`.
Expand Down
57 changes: 51 additions & 6 deletions docs/INTERNALS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2182,13 +2182,28 @@ television that will not stop reloading is a different order of bad.

### Switching sites

`SiteRules.OtherSite` walks the history — which is kept most-recent-first — and
returns the first entry that is not on this site. From a page on B that is the
`SiteRules.OtherSite` goes round the favourites, in the order their tiles sit on
the start screen: from a site with a favourite to the next favourite's site,
wrapping at the end, and from a site with none to the first. Two tiles on one site
are one stop. Each stop opens the last page the history has on that site, so a
return to Spotify lands where Spotify was left; the tile's own address is the
answer only for a site never visited. With no round to go — no favourites, or every
favourite on the site already open — it falls back to the first shape below, and
from the start screen it opens the most recent site either way. It needs no new
screen: a list of sites is what the start screen already is, and the reporter had
said he did not want to go there.

**The first shape walked the history, and could only ever reach two sites.** It
returned the first history entry not on this site: from a page on B that is the
page on A the user came from, and from A it is the page on B they just left, so
one action alternates between the two sites somebody is going back and forth
between. That is #75 as it was asked, and it needs no new screen: a list of recent
sites is what the start screen already is, and the reporter had said he did not
want to go there.
one action alternated between the two sites somebody is going back and forth
between. That is #75 as it was asked — "between two sites" — and it is also why
his third site never came up (#100, 2026-10-03: "it only switches between 2 sites
instagram and spotify not to 3rd favourite tiktok"): the history is always ordered
by what was just left, so the two most recent sites trade places for ever and the
third is never the first entry off the current site. His own word for the fix,
"favourite", is the list he already keeps in an order he can see, so that is the
round.

**It is a menu row before it is a key.** Every digit and `Info` were already spoken
for, so the only button left on a Samsung remote is the red / A one — and which
Expand Down Expand Up @@ -3428,6 +3443,36 @@ the one its report has to come from. The state is:
reels and on a live stream, and the issue closes on that report. If a freeze
near a live stream ever does come back, the `dur`/`live page` markers from
`build-e1a648d` are already in the probe and the reading above still applies.
**It came back the same evening (2026-10-02, 17:08 UTC), and #100 was reopened
on 2026-10-03:** "Video freeze occured again because of livestream", plus a
second complaint, that *Switch site* "only switches between 2 sites instagram and
spotify not to 3rd favourite tiktok". The page attached to it does not carry the
freeze, and the reason is worth saying once: the report keeps one launch back,
and the launch in its `previous run` block (22:23:58–22:36:14 on the TV's clock)
has TikTok open twice, for six seconds at launch and for eleven seconds from a
start-screen tile at 22:33, with no `mse:` line, no `hold:` line and no
`load finished` for either — he left for the start screen both times before the
page had finished loading. Between them is an Instagram reels session (fifteen
`<video>`s readied at once, none held because none was playing, no stall) and
after them Spotify. *Switch site* was never pressed in that launch (`navigate:`
is only on the two start-ups), so the switch complaint is from a session the page
does not hold either, and it does not need one: the shape above explains it
exactly. What the trail does say is where the hold is *not*: `InstallMediaWatch`
runs from `PageLoadFinished`, so a TikTok page that has not finished loading has
neither the probe nor the hold in it, and on the trail that worked (19:14:34 the
day before) TikTok's landing page took two seconds to finish once and fourteen
the second time. A reel that plays inside that window is unprotected and
unrecorded. That is not built on — nothing says his freeze was in it, and the
earlier freeze trail had the hold in and working — but it is the first place to
look if the next trail shows a stall with no `hold:` line in front of it.
**Shipped in the switch build: the round of favourites** (see *Switching sites*
above; `tools/siterules` holds it). **Waiting on:** the freeze itself, which needs
the page from a launch that *had* it: `:8081` while the app is still open after
the freeze (the `this run` block carries it), or at the very next launch (the
`previous run` block does), and not a launch later, because that is the one the
report forgets. Read it by the markers above: `dur inf` and `live page` name the
stream, a `hold:` line in front of the stall says the hold was in, no `hold:` line
on a page that was playing says it was not.

Five things about that set are settled and should not be re-derived: **key `5` is
his, not ours** — the engine's overlay path is the only one that gives him a
Expand Down
88 changes: 78 additions & 10 deletions src/common/SiteRules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -308,17 +308,85 @@ public static SiteRule Forget(string url)
}

/// <summary>
/// The most recent entry in the history that is on a different site from
/// the address given, or null when there is none.
/// Where the switch action goes from the address given, or null when there
/// is nowhere to go.
///
/// It goes round the favourites, in the order they sit on the start screen:
/// from a site that has a favourite, to the site of the next favourite after
/// it, wrapping round at the end; from a site that has none, to the first.
/// Two favourites on one site count as one stop. Each stop opens the last
/// page the history has on that site, so a return to Spotify lands where
/// Spotify was left and not on the tile's address — the tile is the answer
/// only for a site never visited.
///
/// Issue #75, and the reason it is this and not a list: the history is kept
/// most-recent-first, so from a page on B the first entry that is not on B
/// is the page on A the user came from — and once they are on A the first
/// entry that is not on A is the page on B they just left. One action
/// therefore alternates between the two sites somebody is going back and
/// forth between, which is the question as it was asked. From the start
/// screen (a null address, since it is on no site) it opens the most recent
/// site instead.
/// Issue #75 asked for a way between <em>two</em> sites, and the first
/// shape of this walked the history instead: from B the first entry not on
/// B is the page on A the user came from, and from A it is B again, so one
/// action alternated between the two sites somebody is going back and forth
/// between. The reporter's third site is what that shape cannot reach — the
/// history is always ordered by what was just left, so the two most recent
/// sites trade places for ever and the third never comes up. The favourites
/// are the list he already keeps, in an order he can see, so they are the
/// round. The history walk stays as the answer when there is no round to go:
/// no favourites, or every favourite on the site already open. From the
/// start screen (a null address, since it is on no site) it opens the most
/// recent site either way.
/// </summary>
public static Bookmark OtherSite(IList<Bookmark> favourites, IList<Bookmark> history, string currentUrl)
{
string here = KeyFor(currentUrl);
if (here != null && favourites != null)
{
// The distinct sites among the favourites, in tile order.
var sites = new List<string>();
for (int i = 0; i < favourites.Count; i++)
{
string site = KeyFor(favourites[i].Url);
if (site != null && !sites.Contains(site))
{
sites.Add(site);
}
}

int at = sites.IndexOf(here);
if (sites.Count > (at >= 0 ? 1 : 0))
{
string next = sites[at < 0 ? 0 : (at + 1) % sites.Count];
return LastOn(history, next) ?? LastOn(favourites, next);
}
}

return OtherSite(history, currentUrl);
}

/// <summary>The first entry in the list that is on the site named, or null.</summary>
private static Bookmark LastOn(IList<Bookmark> list, string site)
{
if (list == null)
{
return null;
}

for (int i = 0; i < list.Count; i++)
{
if (string.Equals(KeyFor(list[i].Url), site, StringComparison.Ordinal))
{
return list[i];
}
}

return null;
}

/// <summary>
/// The most recent entry in the history that is on a different site from
/// the address given, or null when there is none. The first shape of the
/// switch (see above), and still what it does when there is no round of
/// favourites to go: the history is kept most-recent-first, so from a page
/// on B the first entry that is not on B is the page on A the user came
/// from — and once they are on A the first entry that is not on A is the
/// page on B they just left. From the start screen (a null address, since
/// it is on no site) it opens the most recent site instead.
/// </summary>
public static Bookmark OtherSite(IList<Bookmark> history, string currentUrl)
{
Expand Down
11 changes: 6 additions & 5 deletions src/nui/NuiBrowserApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2342,14 +2342,15 @@ private void CycleIdentity()
}

/// <summary>
/// Opens the most recent page that is not on this site — issue #75. From a
/// page on B that is where the user came from, and from there it is B
/// again, so the one action goes back and forth between the two sites
/// somebody is actually using.
/// Goes round the favourites — issue #75, and #100's note that the first
/// shape only ever reached two sites. From a site with a favourite it opens
/// the next favourite's site, at the last page seen there; with no round to
/// go it falls back to the site the user just came from. SiteRules.OtherSite
/// has the reasoning.
/// </summary>
private void SwitchSite()
{
Bookmark other = SiteRules.OtherSite(Store.RecentHistory, PageUrl());
Bookmark other = SiteRules.OtherSite(Store.AllFavourites, Store.RecentHistory, PageUrl());
if (other == null)
{
Flash("No other site to switch to yet");
Expand Down
61 changes: 56 additions & 5 deletions tools/siterules/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,15 @@ private static int Main(string[] args)

// ------------------------------------------------- 5. the other site
//
// Issue #75. History is most-recent-first, so the first entry that is
// not on this site is the site the user came from — and from there, the
// first that is not on *that* one is the site they left. One action
// therefore alternates between the two, which is how the question was
// asked.
// Issue #75, and #100's complaint about it. With no favourites the
// switch walks the history: it is most-recent-first, so the first entry
// that is not on this site is the site the user came from — and from
// there, the first that is not on *that* one is the site they left. One
// action therefore alternates between the two, which is how #75 was
// asked, and which is also why a third site never came up: the two most
// recent sites trade places for ever. With favourites it goes round
// them instead, in tile order, each at the last page seen on that site.
var none = new List<Bookmark>();
var history = new List<Bookmark>
{
new Bookmark("https://open.spotify.com/album/2", "Spotify"),
Expand Down Expand Up @@ -232,6 +236,53 @@ private static int Main(string[] args)
new Bookmark("https://b.test/", "B"),
}, "https://a.test/").Url == "https://b.test/",
"a generated page in the history is skipped, not switched to");
Check(SiteRules.OtherSite(none, history, "https://open.spotify.com/album/2").Url ==
"https://www.instagram.com/reels/x/" &&
SiteRules.OtherSite(null, history, other.Url).Url == "https://open.spotify.com/album/2",
"with no favourites the three-argument form is the same walk");

// The round. Three favourites in tile order, two of them on sites the
// history knows; a return lands on the page that was left there, not on
// the tile, and the one site never visited opens at its tile.
var favourites = new List<Bookmark>
{
new Bookmark("https://www.instagram.com/", "Instagram"),
new Bookmark("https://open.spotify.com/", "Spotify"),
new Bookmark("https://open.spotify.com/playlist/9", "Spotify again"),
new Bookmark("https://www.tiktok.com/", "TikTok"),
};
Bookmark step1 = SiteRules.OtherSite(favourites, history, "https://www.instagram.com/reels/x/");
Check(step1 != null && step1.Url == "https://open.spotify.com/album/2",
"from Instagram the round goes to Spotify, at the page left there");
Bookmark step2 = SiteRules.OtherSite(favourites, history, step1.Url);
Check(step2 != null && step2.Url == "https://www.tiktok.com/",
"from Spotify it goes past the second Spotify tile to TikTok, at its tile (never visited)");
Bookmark step3 = SiteRules.OtherSite(favourites, history, "https://www.tiktok.com/@someone/live");
Check(step3 != null && step3.Url == "https://www.instagram.com/reels/x/",
"from TikTok it wraps round to Instagram, at the page left there");
Check(SiteRules.OtherSite(favourites, history, "https://www.google.com/search?q=x").Url ==
"https://www.instagram.com/reels/x/",
"from a site with no favourite it goes to the first favourite's site");
Check(SiteRules.OtherSite(favourites, history, HomePage.BaseUrl).Url == "https://open.spotify.com/album/2",
"from the start screen it still opens the most recent site");
Check(SiteRules.OtherSite(favourites, new List<Bookmark>(), "https://open.spotify.com/album/2").Url ==
"https://www.tiktok.com/",
"with no history at all each stop is its tile");
var oneSite = new List<Bookmark>
{
new Bookmark("https://open.spotify.com/", "Spotify"),
new Bookmark("https://open.spotify.com/playlist/9", "Spotify again"),
};
Check(SiteRules.OtherSite(oneSite, history, "https://open.spotify.com/album/2").Url ==
"https://www.instagram.com/reels/x/",
"every favourite on this site is no round, so it falls back to the site just left");
Check(SiteRules.OtherSite(oneSite, history, "https://www.instagram.com/").Url ==
"https://open.spotify.com/album/2",
"and from elsewhere the one favourite site is where it goes");
Check(SiteRules.OtherSite(new List<Bookmark> { new Bookmark(AsEngineUrl("<html></html>"), "Overscan") },
history, "https://open.spotify.com/album/2").Url ==
"https://www.instagram.com/reels/x/",
"a favourite that is a generated page is not a stop");

// ------------------------------------------------------- 6. the proxy
//
Expand Down
10 changes: 6 additions & 4 deletions tools/siterules/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@
# 4. the file: a round trip through the disk, forgetting that reports whether
# it did anything, and a hand-edited or future-build file that cannot stop
# the browser from starting;
# 5. the other site: that it alternates between the two sites somebody is going
# back and forth between, treats another host of the same site as the same
# site, skips this app's own generated pages, and has an answer when there
# is nowhere to go.
# 5. the other site: that with favourites it goes round them in tile order,
# each at the last page seen on that site (issue 100's third site), and
# without them alternates between the two sites somebody is going back and
# forth between; treats another host of the same site as the same site,
# skips this app's own generated pages, and has an answer when there is
# nowhere to go.
# 6. the proxy (issue 97, NUI only): the third field follows the same rules as
# the other two, and the typed address is taken apart so the login never
# reaches the engine's proxy string or anything shown on screen or on the
Expand Down
Loading