fix(home): render the home screen when the home tab is active - #429
Merged
Conversation
The application body is drawn from a single `{#if}`: the consequent is
the markdown container, the `{:else}` is `<HomePage>`. Its condition was
activeTab && (activeTab.path !== '' || activeTab.title !== 'Recents') && !showHome
which described the home tab as it was first written — `{ path: '',
title: 'Recents' }`. A week later the home tab became `{ path: 'HOME',
title: 'Home' }` and both halves went permanently true, so the home tab
fell into the markdown container and rendered an empty document. The
later i18n pass made the title half unrecoverable in 26 locales rather
than one.
The sentinel path now has a name. `src/lib/utils/homeTab.ts` owns
`HOME_TAB_PATH` and `isHomePath`; `hasRealFilePath` is expressed in
terms of the latter, and the ten sites that spelled the literal call one
of the two. The render gate asks `isHomePath`, not `hasRealFilePath` —
an untitled buffer has no file either, but it is still a document and
still needs the editor.
`singleImplementationConvention` pins the literal to that one file, so a
future reader of `tab.path` cannot invent its own idea of the home tab
in silence, which is how this rotted.
Also fixes a second reading of the same stale assumption: a tab's
tooltip was `tab.path || 'Recents'`, which showed the raw sentinel on
the home tab and "Recents" on every untitled buffer.
Ctrl+T is untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 6, 2026
Merged
PathGao
added a commit
that referenced
this pull request
Aug 6, 2026
…one meaning for Ctrl+T (#480) * feat(editor): give the six unbound formatting commands a shortcut `fmt-inline-code`, `fmt-code-block`, `fmt-quote` and the three headings have been in the toolbar and the command palette since v2.6.12 and answered no key. The report (#121) is from a QWERTZ user, for whom the backtick is a dead key: typing a code fence needs three dead-key escapes, which is why the request is for a shortcut and not for the command. Bold, Italic and Underline already had Ctrl/Cmd+B/I/U. These six now have: | action | Windows/Linux | macOS | why that chord | |--------------------|----------------|--------------|---------------------------------| | `fmt-heading-1..3` | Ctrl+1/2/3 | Cmd+1/2/3 | Typora, Mark Text | | `fmt-inline-code` | Ctrl+Shift+E | ⇧⌘E | GitHub's Ctrl+E, plus Shift | | `fmt-code-block` | Ctrl+Shift+F | ⇧⌘F | no precedent survives Monaco | | `fmt-quote` | Ctrl+Shift+. | ⇧⌘. | GitHub | Every one of them was checked against standalone Monaco's OWN defaults, dumped from the installed monaco-editor 0.55.1 rather than read off VS Code's documentation — the two keymaps are not the same, and `editor.addAction` registers at weight 1000, above every Monaco default, so a clash does not error: our action simply wins and Monaco's loses its key. That check rejected the obvious candidates: - Ctrl/Cmd+E for inline code (GitHub's chord) is `view-toggle-edit` here already — and Ctrl+E for edit/read is itself the mainstream reading, in Obsidian and Mark Text. So inline code takes the same letter with Shift. - Ctrl/Cmd+Shift+K for a code block (Typora, Mark Text) is `editor.action.deleteLines` on BOTH platforms, not just on Windows. Their macOS alternative, Cmd+Option+C, is `toggleFindCaseSensitive`. No mainstream code-fence chord is free, so this one is a deviation and says so. - Ctrl/Cmd+Shift+Q for a blockquote (Typora, Mark Text) is the macOS system Log Out on ⇧⌘Q, which is exactly why both of them use Cmd+Option+Q there. GitHub's Ctrl+Shift+. is the same on both platforms, and Shift+. is `>`. Quote is the one binding that takes a chord away from Monaco: `editor.action.inPlaceReplace.down`, which has no menu or toolbar presence here, means nothing in Markdown, and stays in the command palette. Headings stop at 3 because the app has three heading actions. 4, 5 and 6 are left unbound rather than given to something else, so completing Typora's range later moves nothing. The toolbar hint is the second copy of each of these facts, so it is filled in too, and the test holds the two together — which it did not for Bold, Italic and Underline either. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * fix(editor): settle Ctrl+T on new file, wherever the focus is Ctrl/Cmd+T meant two things (#392). Monaco resolves its keybindings on the editor's container node and calls stopPropagation() when it consumes one, so the document-level handler in MarkdownViewer.svelte only ever runs for keystrokes the editor did not claim. Inside the editor, `file-new` answered Ctrl+T and opened an Untitled buffer; outside it, the handler opened a Home tab. Same key, different document, decided by where the caret happened to be. The issue reported three paths. There are now two: the macOS native menu was cut back to Settings and Quit by #281 and no longer takes Cmd+T at all, and this asserts that, so a fourth meaning cannot be added above the other two without failing. The app's own labels had already picked a side. The tab strip's + button is titled "New Tab (Ctrl+T)" and calls `addNewTab`; the app menu prints Ctrl/Cmd+T beside "New File". The branch was the odd one out, not the labels, which is what @alecdotdev's "new file seems the common reading" says in #392. `file-new` also binds Ctrl/Cmd+N, and the document handler knew about only one of the two — so Ctrl+N did nothing at all outside the editor. Both keys are handled here now, and the test asserts the two layers agree rather than asserting either one in isolation. ## The Home tab `addHomeTab()` had exactly one caller in the app, and it was this branch, so unifying Ctrl+T does remove the only way to open the home screen AS A TAB. What it does not remove is the home screen: the app menu's Home item toggles `showHome`, which renders the same `<HomePage>` with the same recent files, pinned tags and callbacks. The tab form was already the weaker of the two — it is dropped on session restore (`hasRealFilePath` is false for the sentinel) and closed when windows are merged, so it never survived a restart anyway. `addHomeTab` and the `isHomePath` special cases stay. The sentinel still has to be recognised for snapshots written by older builds, and #429's render gate is tested against it. Removing the now-callerless constructor is a separate cleanup and is deliberately not bundled here. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.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.
Addresses the blank-render half of #392 (thanks @PathGao):
The
Ctrl+Thalf of that issue is deliberately untouched and stays open. No keybinding, no menu wiring, and no change to which tab typeCtrl+Tcreates — which of the three meanings it should settle on is a product decision, not a bug fix.One
{#if}draws the whole appThe consequent is the markdown container (editor + preview); the
{:else}is<HomePage>. The home screen therefore appears only when the condition is false, and the condition was:That is an accurate description of a tab that stopped existing in January.
git log -Sdates it exactly:0694991— 24 Jan{ path: '', title: 'Recents' }. Both halves go false, the{:else}runs, the gate is correctedfb555— 31 Jan, "roughly match viewer-editor scroll location"{ path: 'HOME', title: 'Home' }. Both halves are now permanently true. The gate is byte-for-byte unchanged and has been wrong ever sinced14bb33— 31 Mar, i18ntitlebecomest('tabs.home', lang). The dead half is now dead in 26 languages instead of oneNote what the breaking commit was about. The tab-shape change was incidental to a scroll fix, and the coupling was invisible: a template comparing a user-facing localised string against an English literal is not something
npm run checkcan type,svelte-checkcan flag, or any store-level test can see. It only shows up by looking at the branch the app actually takes.showHomecould not rescue it either. It is a separate, older route to the same screen — a transient flag set by the Home toolbar button — and an$effectkeyed onactiveTabIdsets it tofalseon every tab switch, including the oneaddHomeTab()performs on itself.The sentinel gets a name
'HOME'is a magic string sitting in the field that otherwise holds a filesystem path. It was spelled at ten comparison sites across five files, and the reader that mattered most was not one of them — it had invented its own idea of what a home tab is. That is the reason this could rot silently, so the fix is to make there be one idea.New
src/lib/utils/homeTab.ts(29 lines) ownsHOME_TAB_PATHandisHomePath. Every site now calls one of them, andhasRealFilePath— the project's existing "is this path a file" predicate, already used byserializeState,restoreState, the window-session restore and the tab context menu — is redefined in terms ofisHomePathso the two cannot disagree.The gate asks
isHomePath, nothasRealFilePath. That distinction is the whole fix.hasRealFilePathis false for an untitled buffer too, and an untitled buffer is a document — it needs the editor. Gating the container on it would have traded a blank home tab for a blank new file, which is whyan untitled buffer renders the document containeris in the suite (and is the test that goes red if you try it — verified below).Four call sites collapsed into
hasRealFilePathrather thanisHomePath, becausepath !== '' && path !== 'HOME'is that predicate, spelled out.Why it cannot rot the same way twice
A new rule in
singleImplementationConvention.test.tspins the literal'HOME'tohomeTab.tsand nowhere else insrc. The marker is the string, not the helper: a second site re-implementing the check is precisely a site that spells the string and does not mentionisHomePath. Doc comments were reworded to sayHOME_TAB_PATH, a symbol a rename carries.This is the guard the old code lacked. Verified directly: rewriting the fixed gate as
tabManager.activeTab.path !== 'HOME'is behaviourally correct and every behaviour test stays green — only the convention rule fails. That is the case the rule exists for.Tests
scripts/homeTabRender.test.tsparsesMarkdownViewer.sveltewith the project's ownsvelte/compiler, locates the{#if}whose{:else}renders<HomePage>(skipping themode === 'loading' / 'installer' / 'uninstall'ancestors, which contain it too), and evaluates that exact expression against the realTabManager. Free identifiers are read off the parsed expression, so a rewrite using a different helper fails with "the gate references X" rather than a bareReferenceError.It asserts nothing about how the condition is spelled. Rewrite it any way that keeps the home tab out of the document container and it stays green.
git checkout HEAD~1 -- src/)Baseline red, verbatim:
the active home tab fell into the markdown container instead of the {:else} branch that renders <HomePage> — the user sees a blank page. Gate: tabManager.activeTab && (tabManager.activeTab.path !== '' || …)The 4 green on both sides are the fence — a gate that simply always chose
HomePagewould pass the repro and fail all of these: a saved file gets the container, an untitled buffer gets the container, an empty window gets the home screen, andshowHomestill overlays the home screen on any tab.Mutation checks
hasRealFilePathinstead ofisHomePathan untitled buffer renders the document containerisHomePathanswers on''instead of the sentinel'HOME'(behaviour unchanged)HOME_TAB_PATHto'__markpad_home__'homeSentinelSnapshotgoes red on the two legacy-snapshot casesThat last row is the useful one — see Not covered.
Seen rendering, not only reasoned about
Reasoning about an
{#if}from source is weak evidence, so the SvelteKit frontend was driven in a browser behind a temporary local Tauri-bridge stub (not committed —src/app.htmlis untouched in this diff). New file →Ctrl+Twith focus outside Monaco:主页tab activates and the body is empty white — the reported symptom, reproduceddocument.querySelector('.markdown-container')isnullA second reading of the same stale assumption
Tab.svelteset its tooltip totab.path || 'Recents', written under the same obsolete premise. In today's app that shows the raw sentinelHOMEon the home tab, and the untranslated stringRecentson every untitled buffer. NowhasRealFilePath(tab.path) ? tab.path : tab.title— a real path where there is one, the tab's own localised title otherwise. Confirmed in the browser:主页and无标题 1. This is the last occurrence of'Recents'insrc.Not covered
HOMEwould be mistaken for the home tab. Every path the app takes in is absolute (dialog, drag-drop, file association, wikilink resolution, Save As), and the consequences are cosmetic — disabled context-menu items, no session persistence — so this is not fixed here. It is now a one-line change instead of a search-and-replace, but not a free one:homeSentinelSnapshot.test.tsrejects the literal'HOME'out of snapshots written by older builds, so changing the value means keeping the old one on the reject list. The mutation run above confirms that test is what catches it.kind: 'file' | 'untitled' | 'home'discriminant onTabwould make the collision impossible and let the compiler check exhaustiveness. It touchesserializeState/restoreState/ the cross-window transfer payload and their migrations — much wider than a render bug warrants. Left for whoever wants it; this change makes it a smaller job than it was.Ctrl+T's three meanings. Untouched, and Ctrl+T means three different things, and the Home tab it opens renders blank #392 stays open for it.🤖 Generated with Claude Code