Repository navigation
feat(shell): the tooltips XP had, on the chrome that only had a title= - #319
Merged
Merged
Conversation
Every control in the desktop shell carries a `title`, which buys the OS tooltip: one line, in the system font, after a delay nobody chose. Windows itself was richer. An Explorer infotip named a folder's contents; the tray's tooltips were live readouts; the clock's was the full date. /infotip.js draws those in Luna, on the taskbar app buttons, the tray, the clock, the desktop icons, the title-bar controls, and any form field carrying a tip. Nothing it says is decorative. A pin's "Contains: 19 pages" is counted from nav.js's own destination table, the tray's colo and build come from the same JSON its click-balloons already fetch (one fetch per page, shared), Notepad's note count from posts.json, the clock's date from the clock. A row with no value is dropped rather than filled in, the same rule the photo tooltip's EXIF rows follow. It re-draws `title` rather than replacing it: the attribute is emptied on the way in and restored on the way out, so AT still reads it at focus and a page with no JS keeps the native tooltip. `[data-tip]` is the opt-in for a control that never had one. Field tips land on the three /coffee inputs, both /lens address bars, and /search. hoist.js gains Windows' two delays, both defaulting OFF so the content surfaces keep the instant show they were tuned for: openMs (400ms cold dwell, skipped while a tip is already up, which is XP's reshow behaviour) and autopopMs (6s, which also covers the dismissal a pointer cannot — a cursor that leaves the viewport fires no pointerout). The dwell is what hides the lazy load, since the import runs during the wait. 1,713 B brotli, loaded on the first chrome hover and never on a coarse pointer. nav.js grew 1.1 KB minified and owns the target selector, which it passes in: two copies is how a loader and its module come to disagree about what has a tip. /nav.js's route-oracle ceiling goes 50,000 -> 65,000. It is a did-the-build- minify tripwire, not a byte budget, and a 241-byte gap turns it into one that fails on whichever unrelated change crosses it; a readable nav.js is ~98 KB, so it still catches the bypass it exists for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
aadhar-sh | 99ec663 | Aug 11 2026, 10:44 AM |
Contributor
Wire-size diff vs merge base
Brotli q11, the quality the edge serves. Every number here is deterministic: Client assetsTotal Brotli: 60.97 KiB → 63.56 KiB (+2.59 KiB, +4.2%) across 13 files, 5 changed.
PagesTotal Brotli: 476.77 KiB → 476.63 KiB (-0.14 KiB, -0.0%) across 46 files, 41 changed. No single file moved by 0.13 KiB or more. Plus 41 files under the 0.13 KiB floor, net -0.14 KiB — usually content-hash churn in Worker bundle269.18 KiB → 269.52 KiB gzip (+0.34 KiB, +0.1%) Largest module deltas (raw bytes in bundle):
Dictionary deltas (
|
…e chrome The first pass drew Luna infotips for the desktop shell and form fields. That left the rule half-kept: a titled citation link on an /lwe page and a taskbar button are the same promise to a visitor, and one of them still got the OS tooltip. The rule is the short one now — ANY [title] is an infotip, wherever it lives — so the shell selectors are left holding only what they alone can answer: the title-LESS chrome (the clock has no attribute to read) and the families whose card comes from live state rather than from the string. A titled link that LEAVES the site gets the shortcut treatment: the title stays the tip, and Target names the host, trimmed to about 46 characters with a visible ellipsis. The host is the decision a reader is making; a 70-char slug wrapped over three lines is not, and a silently shortened URL in a tooltip about where a link goes is the one dishonesty this surface cannot afford. Same-origin links keep the bare line, since a path is neither news nor a risk. An UNtitled link still gets nothing at all. Four richer surfaces are skipped by name (tooltip.js's photos/tracks/artists/ car links, /lens's glossary terms, serendipity's event covers), because each already draws its own card from the same engine. `.lx-term` is the sharp one: those ship a `title` as their no-JS fallback and lens.js strips it once its surface is live, so without the skip a race between two lazy modules would decide whether you got the glossary card or a flat line. Verified in a browser: the lens term opens its own card and the infotip module never even loads on that hover. "What was hovered" is now ONE function, passed from nav.js to the module rather than restated as a selector on each side — the skip list made the rule too big to keep in two places honestly. nav.js also retires its three loader listeners once the module starts: with the selector matching every titled element, leaving them on would run a second closest() walk per pointerover for the rest of the session, on a path that fires at every element boundary. Escaping matters more now that any page's strings reach the surface, and some of them are not ours (/inbox carries webmention titles, /around and /reading carry crawled text). Measured in a browser: a title holding `<img src=x onerror=alert(1)>` renders as text and creates no element. A new contract test pins every interpolation to esc() so a later edit cannot open one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
oddharsh
enabled auto-merge (squash)
August 11, 2026 10:43
oddharsh
added a commit
that referenced
this pull request
Aug 11, 2026
#319 failed this check twice the same way, sixteen minutes apart, on the feature head and then on a merge commit that only pulled main in. Different Request IDs, identical CAPIError. Worth stating outright, because an outage that reproduces on whatever you push next is the case where someone starts bisecting a diff for the cause of it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
oddharsh
added a commit
that referenced
this pull request
Aug 11, 2026
…t on (#321) Gotcha 27 recognised a Copilot Autofix crash from the outside: a null output title, an annotation at a directory, a repeating line number. #319's job log gives the actual error instead: CAPIError: 400 The requested model is not supported. The review agent asked GitHub's own model API for a model that API refused, threw inside runAgenticLoop, and exited 1. It never read the diff, so the check reports an outage in Copilot's model routing and no change to a branch can turn it green. The note now leads with that, keeps the fingerprint as history, and gives a test that names the failure: grep the job log for CAPIError. A real finding leaves that empty and posts an inline review comment on a source line instead. Three commits across two PRs in twenty-one minutes hit it, including this one, whose entire diff is the paragraph describing it. Also recorded: the workflow is dynamic/agents/github-advanced-security and lives nowhere in .github/workflows/, and a re-notification can arrive for a run that already failed hours earlier, so compare id and completed_at before investigating.
oddharsh
added a commit
that referenced
this pull request
Aug 11, 2026
…ore (#323) #319 shipped and ramped to 100% with nothing staged, so /updates and /restore stopped at v182 while production served the tooltips. This is the row that closes the gap. The title names three things a visitor can go and check rather than describing the implementation, because all three are read from live data at hover time: a taskbar button reporting its page count, the tray reading out the colo serving that request, the clock spelling out the date. bump-version.sh writes this file and nothing else. /updates renders the committed projection at build time, so the entry appears when this ships, and deploy:promote inserts it into D1 when traffic reaches 100%.
oddharsh
added a commit
that referenced
this pull request
Aug 11, 2026
…nd /restore (#327) #325 shipped the cracker without its changelog entry. bump-version.sh says to run it inside the PR being released; this is the follow-up that should have been part of that one, in the same shape as #323 was for #319. Projection only. /updates and /restore render the committed projection at BUILD time, so the entry has to reach main before the production build for the page to show it; deploy:promote writes the D1 row when traffic actually reaches 100%. Co-authored-by: Aadharsh Pannirselvam <19518661+oddharsh@users.noreply.github.com>
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.
Every control in the desktop shell carries a
title, which buys the OS tooltip: one line, in the system font, after a delay nobody chose. Windows itself was richer than that. An Explorer infotip named a folder's contents ("Size: 110 MB / Folders: … / Files: …"), the tray's tooltips were live readouts, the clock's was the full date, and the ToolTip control put a line under a field saying what it wanted before you got it wrong.holding/infotip.jsdraws those two shapes in Luna, on: taskbar app buttons, tray icons, the clock, the sound toggle, the Start orb, desktop icons, title-bar controls, and any form field carrying a tip.What it says, and where each line comes from
Nothing is decorative and nothing is guessed. A row with no value is dropped rather than filled in, the same rule the photo tooltip's EXIF rows follow.
posts.jsonfor Notepadwhoareyou.json+updates.jsontheir click-balloons fetch — one fetch per page, sharedtitleHow it treats
titleIt re-draws the attribute rather than replacing it: emptied on the way in, restored on the way out. AT still reads it at focus, and a page with JS off keeps the native tooltip.
[data-tip]is the explicit opt-in for a control that never had a native one. Stashing lives in the module and not in the loader on purpose — a loader that stripped titles itself would leave them stripped if the module never arrived.Field tips land on the three
/coffeeinputs, both/lensaddress bars, and/search.hoist.js
Gains Windows' two delays, both defaulting OFF so the content surfaces (photos, tracks, the Run preview) keep the instant show they were tuned for:
openMs— 400ms cold dwell (XP'sSPI_GETMOUSEHOVERTIME), skipped while a tip is already up, which is XP's reshow behaviour and what makes sweeping a row of buttons feel instant.autopopMs— 6s, which also covers the one dismissal a pointer cannot: a cursor that leaves the viewport for another window fires nopointerout.The dwell is also what hides the lazy load, since the import runs during the wait.
Cost
1,713 B brotli, loaded on the first chrome hover, never on a coarse pointer. nav.js grew 1.1 KB minified and owns the target selector, passing it to the module — two copies is how a loader and its module come to disagree about what has a tip. Derivation (the page counting) sits on the lazy side for the same reason.
/nav.js's route-oracle ceiling goes 50,000 → 65,000. It is a did-the-build-minify tripwire rather than a byte budget, and a 241-byte gap turns it into one that fails on whichever unrelated change happens to cross it. A readable nav.js is ~98 KB, so it still catches the bypass it exists for; perf-budget.mjs is where a real budget belongs.Verified
pnpm run build, 212 contract tests (3 new),perf-budgetgreen,routes:check122/122, cal's 59 tests, typecheck. Exercised in a browser againstwrangler dev: pin, tray, clock, desktop icon, and field tips all render with live data, and a field'stitleis confirmed restored after the pointer leaves.Content tooltips too (second commit)
The rule is the short one now: any
[title]is an infotip, wherever it lives. The shell-only list meant the promise was kept in the chrome and broken two inches lower, on the citation links and demo controls that had titles all along. What the shell selectors still buy is the title-LESS chrome (the clock has no attribute to read) and the families whose card is built from live state.A titled link that leaves the site gets the shortcut treatment — the title stays the tip, and
Targetnames the host, path trimmed to ~46 chars with a visible ellipsis (darko.audio/2018/04/dial-m-for-multi-bit-schi…). The host is the decision; a 70-character slug wrapped over three lines is not, and a silently shortened URL in a tooltip about where a link goes is the one dishonesty this surface cannot afford. Same-origin links keep the bare line. An untitled link still gets nothing.Four richer surfaces are skipped by name: tooltip.js's photos/tracks/artists/car links,
/lens's glossary terms, serendipity's event covers..lx-termis the sharp one — those ship atitleas their no-JS fallback andlens.jsstrips it once its own surface is live, so without the skip a race between two lazy modules would decide whether you got the glossary card or a flat line. Verified: hovering a lens term opens its own card and the infotip module never loads.Two consequences worth naming:
closest()walk perpointeroverfor the rest of the session, on a path that fires at every element boundary.Escaping is now load-bearing, because some of those strings are not ours (
/inboxcarries webmention titles;/aroundand/readingcarry crawled text). Measured in a browser: a title holding<img src=x onerror=alert(1)>renders as text and creates no element. A contract test pins every interpolation toesc().Re-verified: build, 214 contract tests (5 new), perf-budget green, routes:check 122/122. nav.js minified is 49,866 B.
🤖 Generated with Claude Code