Repository navigation
feat: presentation mode — a per-viewer, display-only privacy screen (#1803) - #1991
Conversation
…1803) Sharing a screen shares the whole portal: the home catalog lists every space the viewer can read, "last accessed" names whatever they touched this morning, pinned tiles sit at the top, link-preview cards carry names and logos, and `@` completions offer node names as you type. #1803 asks for a quick toggle that takes the confidential ones off those surfaces — and notes why the workarounds fail: renaming display fields is GLOBAL, and stripping the tile regions loses the functionality. The model, in two facts, both on the VIEWER's own profile and never on the node: User.HiddenPaths the marks — set from the node menu, per node User.PresentationMode the toggle — set from the user menu `PresentationScreen` is the one place they become a rendering decision: a pure, immutable predicate over PATHS. A mark on its own hides NOTHING while the mode is off, which is what makes the whole thing reversible in one click with no restore step. Marking a space covers its subtree, by segment — the path IS the name, so listing `Acme/Q3-Renewal` would leak exactly what marking `Acme` was meant to keep off the screen. 🚨 It is display-only, and the code says so. It grants nothing and denies nothing: no permission, no AccessContext, no query, nothing reachable from the permission evaluator. A marked node stays readable, stays reachable by URL, stays in the viewer's own search results, and is unchanged for every other user. `MarkingGrantsNothingAndDeniesNothing` pins all three against a live mesh. Resolution follows the display-time seam: WHO is asking comes off the live AccessContext, resolved ONCE on the render turn and passed down as a value. The VALUE, unlike the time zone, is read live off the viewer's own User node — because a toggle is flipped in the seconds before a screen share, and a context snapshot taken at circuit open would still say "off" at exactly that moment. A stale zone is a cosmetic lag; a stale screen is the leak. Surfaces: the MeshSearch family (home catalog — which is where the former Spaces / Last Read / Last Edited tabs live — plus pinned tiles, "Shared with me", node catalogs, tree levels, graph navigator), OG cards, and both `@` completion paths. Pinned tiles and the shared band filter BEFORE the query is built, because their paths are interpolated into the control's query string and travel into the `hq=` URL; everywhere else the query is deliberately left alone — a `-path:` clause would leak the same name and start turning this into a second permission system. Nothing paints before the screen is known: MeshSearchView starts its result source on the first screen emission, so a marked item cannot flash. A faulting profile stream HOLDS the last known screen rather than resetting to neutral, and says so in the log — never widen a screen on a fault. Follow-ups, deliberately out: the React client's renderer, the strict variant that hides recent-activity regions wholesale, a keyboard shortcut, and #1922's session projection, which this is the prerequisite for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The screen is joined into the node menu's reactive stream, so a viewer whose User node does not exist yet must resolve — promptly, and to the neutral screen. If that ever stopped emitting the menu would simply never render, with nothing in any log to say why: a CombineLatest stalls silently when one leg never produces a value. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The header indicator sits left of the avatar and needs the container to be a flex row to stay on its optical line. A no-op when the indicator is absent — the container then has a single child. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a per-viewer “presentation mode” privacy screen that hides user-marked paths from display-only surfaces (tiles, OG cards, search views, and @ completions) without affecting access control, node reachability, or query results—aligning with issue #1803 and acting as a prerequisite for #1922.
Changes:
- Introduces
PresentationScreen+ profile-backed preferences (User.HiddenPaths,User.PresentationMode) and resolver helpers to project a viewer’s live screen state. - Wires the screen into key UI surfaces (Blazor
MeshSearchView, home catalog/pins/shared band, OG cards, node menu marking) and completion pipelines. - Adds localization keys (server + React mirror) plus comprehensive unit/integration tests and docs.
Reviewed changes
Copilot reviewed 26 out of 26 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| test/MeshWeaver.Graph.Test/PresentationSurfacesTest.cs | Tests that named UI surfaces drop hidden items (including query-string construction cases). |
| test/MeshWeaver.Graph.Test/PresentationScreenTest.cs | Tests core predicate semantics, normalization, preference transforms, resolver fault behavior, and completion-path projection. |
| test/MeshWeaver.Graph.Test/PresentationScreenMeshTest.cs | Live-mesh integration tests for per-user isolation and “grants/denies nothing” invariants. |
| src/MeshWeaver.OgCard/OgCardLayoutArea.cs | Applies viewer screen to OG-card targets and rebuilds card grid when the screen changes. |
| src/MeshWeaver.Messaging.Hub/Localization/strings.en.json | Adds presentation-mode localized strings (EN). |
| src/MeshWeaver.Messaging.Hub/Localization/strings.de.json | Adds presentation-mode localized strings (DE). |
| src/MeshWeaver.Mesh.Contract/Security/User.cs | Adds HiddenPaths + PresentationMode fields to the user profile contract with display-only semantics. |
| src/MeshWeaver.Mesh.Contract/Security/PresentationScreenExtensions.cs | Adds the resolver seam to obtain a viewer’s live PresentationScreen from profile streams. |
| src/MeshWeaver.Mesh.Contract/Security/PresentationScreen.cs | Implements the immutable, display-only predicate (Hides, Filter, Retain). |
| src/MeshWeaver.Mesh.Contract/Security/PresentationPreference.cs | Adds pure transforms for applying marks and toggling mode on a user profile. |
| src/MeshWeaver.Mesh.Contract/MeshWeaver.Mesh.Contract.csproj | Grants test assembly access to internals for resolver fault-path testing. |
| src/MeshWeaver.Hosting/MeshWeaver.Hosting.csproj | Grants test assembly access to internals for completion projection tests. |
| src/MeshWeaver.Hosting/Completion/ChatCompletionOrchestrator.cs | Applies presentation screen filtering at a single seam across completion producers. |
| src/MeshWeaver.Graph/UserActivityLayoutAreas.cs | Filters pinned/shared-band surfaces at query-construction time and threads screen into home catalog rendering. |
| src/MeshWeaver.Graph/PresentationLayoutArea.cs | Adds node-menu “Hide/Show in presentation mode” actions that write to the viewer’s own profile. |
| src/MeshWeaver.Graph/MeshNodeLayoutAreas.cs | Registers presentation layout areas alongside existing node menu actions. |
| src/MeshWeaver.Graph/MeshNodeAutocompleteProvider.cs | Applies viewer screen to mesh-node autocomplete results. |
| src/MeshWeaver.Graph/Configuration/NodeMenuItemsExtensions.cs | Joins viewer screen into the reactive node-menu provider so the hide/show entry flips instantly. |
| src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-21-presentation-mode.md | Adds a “What’s New” entry for the feature. |
| src/MeshWeaver.Documentation/Data/GUI/PresentationMode.md | Adds detailed design/usage docs and rules for adding new screened surfaces. |
| src/MeshWeaver.Blazor/Components/MeshSearchView.razor.cs | Gates first paint on screen resolution and filters painted results/tree levels/navigator below-rail. |
| src/MeshWeaver.Blazor.Portal/Components/UserProfile.razor.css | Styles the header indicator for active presentation mode. |
| src/MeshWeaver.Blazor.Portal/Components/UserProfile.razor.cs | Subscribes to the live viewer screen and implements the toggle write to the user profile. |
| src/MeshWeaver.Blazor.Portal/Components/UserProfile.razor | Adds header indicator + user-menu toggle UI wired to localization keys. |
| clients/react/src/i18n/strings.en.json | Mirrors new localization keys into the React catalog (EN). |
| clients/react/src/i18n/strings.de.json | Mirrors new localization keys into the React catalog (DE). |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Test Results (shard 2)3 076 tests +77 3 076 ✅ +77 8m 10s ⏱️ -5s Results for commit ae6a31e. ± Comparison against base commit 9ee895b. This pull request removes 1 and adds 78 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
Test Results 59 files ± 0 59 suites ±0 48m 11s ⏱️ -31s Results for commit ae6a31e. ± Comparison against base commit 9ee895b. This pull request removes 1 and adds 89 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
# Conflicts: # src/MeshWeaver.Graph/UserActivityLayoutAreas.cs
Copilot's review on #1991, and it is right. The completion pipeline's fast-path tested `screen == PresentationScreen.Off`, but a viewer who has MARKED things and then turned the mode OFF holds a screen that is not `Off` by reference and must still hide nothing. Filter was correctly a no-op for them — but the branch also drops emptied categories, so a legitimately empty completion group was being suppressed for someone who is not presenting at all. `PresentationScreen.HidesAnything` is the question that was actually meant (Active AND something marked); Hides and Filter now ask it too. The projection is extracted as `ChatCompletionOrchestrator.Screened(batches, screen)` so the hides-nothing leg is EXECUTABLE by a test rather than only inferable from a predicate — that leg is exactly the one that was wrong, and a test asserting the predicate alone would not have caught it. Control: restoring `== Off` fails Completions_AreUntouchedWhenTheScreenHidesNothing and nothing else. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # src/MeshWeaver.Graph/UserActivityLayoutAreas.cs
CI run 32490339312 was red on two things. One was mine; one was not.1.
|
| check | result |
|---|---|
Does anything in MeshWeaver.AI / MeshWeaver.AI.Test name a symbol this PR adds or changes? |
no — grep -rlE "PresentationScreen|PresentationPreference|PresentationLayoutArea|ChatCompletionOrchestrator|BuildPinnedItems|BuildHome|MeshNodeAutocompleteProvider|OgCard" over both trees returns nothing |
| What actually failed? | ThreadTokenUsageTest.cs:262 — WaitForUsage(threadPath, "usage_cancel_model", …, 10_000), i.e. a 10-second wall-clock wait that "emitted nothing at all". It ran out of time; it did not read a wrong value |
| Is that class given the box? | No. ThreadTokenUsageTest carries no [Collection], so under AI.Test's parallelizeTestCollections: true, maxParallelThreads: 4 it runs 4-wide on a 4-vCPU runner. 8807a6d8c ("Give the five machine-hungry AI.Test classes the box, and parallelise the rest", 2026-08-10) serialised only AgentChatClientDeadlockTest, CrossHubPatchAtomicityTest, DelegationDeadlockTest, InboxToolIntegrationTest, PatchWorkspaceAckTest into ConcurrencyStressCollection |
| Does that commit describe this failure mode? | Yes, verbatim: "a handful of classes deliberately saturate the machine and then judge the result on a wall clock" |
| Does it pass on this branch? | 10/10, 9s locally |
So: the same family as the reclassification, one class over — a wall-clock-bounded AI.Test class left out of the concurrency-stress collection. That is a real gap in the test, not noise, and the fix belongs in its own PR: either add ThreadTokenUsageTest to ConcurrencyStressCollection, or make that wait event-driven. Not widening the 10s bound, which would be the band-aid. I have deliberately not touched it here.
For the record, neither of the two candidates raised out-of-band matches this run: no Plugins compile against this PR gate failed (the failing jobs were shard 3 of MeshWeaver Build and Test only), and this PR never touches OpenGraphPreviewService — only OgCardLayoutArea.cs, with its InternalsVisibleTo unchanged.
Also in this push
- Copilot's review finding, fixed —
3a6658eba. It was right. See the thread for the mechanism; short version: the fast-path testedscreen == PresentationScreen.Off, but marks-with-mode-off is a different value that must also hide nothing.Filterwas already a no-op there, so no item was wrongly dropped — but the same branch's empty-batch drop was not, suppressing a legitimately empty completion category for a viewer who is not presenting. Now gated onPresentationScreen.HidesAnything, with the projection extracted asChatCompletionOrchestrator.Screened(batches, screen)so the hides-nothing leg is executed by a test rather than inferred from a predicate. Control: restoring== Offfails exactly one test. - Two merges from
main, which reworked this feature's largest surface underneath it: the home is now a tabbed page (Shared with me / Pinned / Apps / Spaces), andBuildAppsgainednodeOwnerIdand~/system-area entries. The screen is re-applied to the new shape — and the tabbed home makes the rule sharper, not weaker: Shared-with-me, Pinned and Apps each interpolate the viewer's paths into their control's query string, so all three are screened before the query is built, while the Spaces list stays a generic query filtered where its results are painted. A~/entry names no node and is passed through untouched. New tests: a tab whose entire content is marked disappears (an empty tab labelled "Pinned" would itself say something), and the Apps query drops a marked app. - Revert-check after both merges:
git diff origin/main...HEAD --name-onlyis exactly this PR's own files.
main turned the home into tabs (Shared with me / Pinned / Apps / Spaces) while this was in flight, and the tabbed shape makes the rule SHARPER rather than weaker: three of the four tabs interpolate the viewer's own paths into their control's query string, so all three are screened before the query is built, while Spaces stays a generic query filtered where its results are painted. Also records the two things the review turned up: a tab the screen empties is dropped rather than shown empty, and "ask HidesAnything, never == Off" is now rule zero for a new surface. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
BuildApps says in a comment that a "~/" entry names no node and is therefore never screened. A comment is not a test. Marks both the real app and the system area, and asserts what is left is the system-area dock rather than the "no apps" placeholder — so the pass-through is a fact, not an intention. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # src/MeshWeaver.OgCard/OgCardLayoutArea.cs
|
"Import, OgCard and their samples leave the platform" landed on main while this was in flight and deleted src/MeshWeaver.OgCard, so the screening this PR added to OgCardLayoutArea went with it rather than being resurrected: the file's merge is resolved as deleted, and its two tests are removed with the surface they covered. Claiming a privacy surface the code no longer has is worse than not having it, so the design doc, the What's New entry and the test class's own summary now say so, and the follow-up is recorded where the other gaps are — carrying the one rule worth keeping across the move: a screened target is DROPPED, not redacted, because a card titled "hidden" still tells the room that something is being hidden. Also refreshes the pathless-item example in Filter_KeepsItemsThatAreNotNodes, which named an OG card; it now names a "~/" system area, which is in this repo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The HidesAnything rule arrived last chronologically and was numbered 0, which reads as an afterthought bolted onto a finished list. Renumbered 1..5 in the order you actually hit them when writing a surface: resolve once, do not paint early, ask the right predicate, never widen on a fault, never consult it for anything but rendering. No content change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Handoff notes — things that are not in the PR description1. The riskiest line, and it is deliberate. The consequence to know: if catalogs or node menus ever show universal spinners, look here first. That symptom would mean the viewer's own profile stream is dead — a portal-level outage the spinner is honestly reporting — not a bug in this feature. The fix would be upstream, never a timeout here. 2. That fault path is already confirmed in the wild, not just unit-tested. CI run 3. Expect a new warning line in test output. 4. Known untested surface, bounded. There is no bunit in this repo, so the ~3 wiring points inside 5. Two tests assert reference equality on 6. The home page is under active construction. Three merges reshaped it under this PR in about an hour (tabbed home; |
Closes an unbounded await this feature introduced in the broadest possible place. DefaultNodeMenuProvider joined the viewer's screen into its CombineLatest, so every node-menu render in the process began awaiting a subscription to the VIEWER's own User node — a node that need not exist (a test identity, a caller mid-onboarding, a virtual user). A stream that ERRORS is answered by LastKnownOnFault; a stream that merely never produces is not, and the join then stalls silently and forever, OUTSIDE any test's methodTimeout. The symptom is a wall-clock hang with no failing test to point at. The menu never needed the wait. There the screen decides only WHICH OF TWO LABELS the presentation entry carries — Hide vs Show — for the node the viewer is already looking at. A briefly wrong label is cosmetic; a menu that never renders is not. The tile surfaces keep their gate, because painting early THERE is the leak the feature exists to prevent. So the rule is now named rather than inline: ViewerScreen().Seeded() where the screen only labels, plain ViewerScreen() where painting early would leak. "This viewer has no profile" is a defined, screened-safe answer — they have marked nothing — and it belongs in the stream as a VALUE, never as something to wait for. Deliberately NOT a timeout: a timeout on a privacy screen falls back to the neutral value, i.e. fails OPEN, which is the one direction that must not happen silently. Pinned by ASeededScreen_RendersEvenWhenTheProfileStreamNeverProduces, which drives a genuinely never-producing source and asserts on the EMISSION LIST — so it catches a hang without being able to hang itself. Control: removing the seed fails it (and its sibling) in 32ms. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Shard 2
|
| grep | hits |
|---|---|
GetConsolidatedTypeParameters |
0 |
Cci.MetadataWriter |
0 |
NullReferenceException |
0 |
canary |
0 |
What the log does and does not say
MeshWeaver.Graph.Test started 16:42:33 and hit the cap at 16:50:32 having emitted no test results at all. methodTimeout is 30 s, so an 8-minute wall clock is necessarily outside a test method — fixture, class init or disposal, exactly as the marker classifies it.
It cannot tell us which test hung: the last console output is the module-packing tests, but my tests emit no console output, so silence is what they look like either way.
Four comparisons that bound it
| tree | Graph.Test |
|---|---|
main @ 9d345a2a |
65 s, 1408 tests, exit=0 |
this branch @ c03bcc163 (pre-merge, same test files) |
exit=0 |
this branch @ fd8be497 (post-merge) |
exit=124, no results |
| merged tree, locally, ×3 | 1455/1455 in ~53 s |
So it is not my +45 tests tipping a near-cap suite (main has 7 minutes of headroom), and it is not deterministic. Today's two main failures are shard 4, a different shape.
(A local run did stall mid-investigation and I nearly reported it as a reproduction — it was not. ps showed my host gone and another session's MeshWeaver.Markdown.Test at 62% CPU on the same box. Contended-machine timing is not evidence; recording it so nobody re-derives it.)
The real finding
DefaultNodeMenuProvider joined the viewer's screen into its CombineLatest, so every node-menu render in the process began awaiting a subscription to the viewer's own User node — a node that need not exist. A stream that errors is answered by LastKnownOnFault (observed working in run 32490339312); a stream that merely never produces is not, and the join then stalls silently and forever, outside any method timeout. That is precisely the shape flagged, in the broadest place it could have been, and the menu never needed the wait: there the screen decides only which of two labels the entry carries for the node the viewer is already looking at.
Fixed by making the absence a value, not a wait — ViewerScreen().Seeded(), per the directive, and explicitly not a timeout: a timeout on a privacy screen falls back to the neutral value, i.e. fails open, the one direction that must not happen silently. The tile surfaces keep their gate, because painting early there is the actual leak. The gate-vs-seed distinction is now rule 2 in Doc/GUI/PresentationMode rather than an inline .StartWith someone could delete.
Pinned by ASeededScreen_RendersEvenWhenTheProfileStreamNeverProduces, which drives a genuinely never-producing source and asserts on the emission list — so it catches a hang without being able to hang itself. Control: removing the seed fails it and its sibling in 32 ms.
Honest bottom line
I can prove #890 is not it. I can prove my change introduced this unbounded await and that it is now gone. I cannot prove it was the cause of that specific 8-minute hang — it did not reproduce in three full local runs, and the pre-merge tree with the same tests was green. If shard 2 times out again on ed068f3b, the menu is no longer a candidate and the next place to look is the compile/packaging machinery that owns the last console line (cf. #612, a non-#890 exit=124 in this same suite).
Local after the fix: presentation suite 63/63, MenuAccessControlTest 9/9 (and 1 s instead of 2 s — the seed removed a real wait).
…menu gate Caught with a stack dump of a locally reproduced hang, so this is mechanism rather than inference: SemaphoreSlim.Wait() System.Reactive…ObservableImpl.GetEnumerator.MoveNext() System.Linq.Enumerable.ToArray PresentationScreenTest.Completions_DropAnEmptiedCategory_OnlyWhilePresenting() .ToEnumerable() is a BLOCKING bridge — the same shape as .Wait(), which this codebase bans everywhere else. It parks the calling thread on a semaphore until the source produces, so when the source schedules its own work onto that same thread the test self-deadlocks. Which thread it lands on depends on the scheduler ToObservable() picks, which is why it is intermittent: one full local run passed with this exact test present, two later ones wedged for 31 and 15 minutes at ~0% CPU. 🚨 And xUnit's methodTimeout CANNOT abort a thread parked in a native wait, so instead of a 30s test failure you get an unbounded HOST wedge — exit=124 with no failing test to point at. That is precisely the CI symptom, and it is why the 30s bound looked like it had been bypassed. Both call sites now subscribe and collect, on ImmediateScheduler so delivery is synchronous and scheduler-independent — the pattern the rest of this file already used. Also commits the tile-surface annotations held back earlier: PinnedAreaView and CatalogAreaView carry an explicit "NOT .Seeded() — deliberately", pointing at rule 2. The menu now reads as the pattern when it is the exception, and the flash seeding them would reintroduce is a leak rather than a cosmetic wobble. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes #1803. Prerequisite for Systemorph/MeshWeaver.Plugins#976.
The problem
Sharing your screen shares the whole portal. The home catalog lists every space you can read,
"last accessed" names whatever you touched this morning, pinned tiles sit at the top, link-preview
cards carry names and logos, and the moment you type
@the suggestions offer up node names.#1803 also says why the obvious workarounds fail: renaming a node's display fields is global
(every user sees the renamed tile, and it needs a restore step), and stripping the tile regions out
of the home body loses the functionality.
What ships
Presentation mode — a per-user, display-only privacy screen. Mark a node from its node menu
(Hide in presentation mode), flip Presentation mode on from the user menu before you share,
and everything you marked leaves your own tiles, cards and completions. A glasses glyph in the
header says the screen is up. Turn it off and everything is back — no restore step.
The four design decisions
1. What "hidden" means — display-only, and the code says so
PresentationScreenis a pure, immutable predicate over PATHS: no permission, noAccessContext, no node read, no query, and nothing in it reachable from the permission evaluator.Hides(path)means do not paint this; it says nothing about whether the viewer may read the node,and no caller may use it to decide a read, a write or a route.
🚨 It must never become an access-control mechanism. A second permission system that can
disagree with the real one is worse than none, and the one that disagrees quietly is the one that
ships a bug nobody can see. So the claim is pinned by a test rather than by review —
MarkingGrantsNothingAndDeniesNothingmarks a space with the mode ON, against a live mesh, andasserts all three:
GetEffectivePermissions(space, alice)is bit-identical before and afterGetMeshNodeStream(space)and its child still resolvepath:Acmefor alice still returns itTwo facts make an item disappear, and a mark on its own hides nothing while the mode is off.
That is what makes the whole thing reversible in one click, and what keeps a stale mark from
quietly removing something from a user's home forever.
Marking a space covers its subtree, by path segment. The path IS the name: listing
Acme/Q3-Renewalunder "last edited" would leak exactly what markingAcmewas meant to keep offthe screen.
Acmenever hidesAcmeCorp.2. Where the marking lives — the viewer's own profile
User.HiddenPathsandUser.PresentationMode, besidePinnedPaths, on the viewer's node —never on the node being marked. That is the whole point of #1803's complaint about the rename
workaround, and
AMarkedSpaceIsHiddenForTheMarkerOnlypins the negative: Bob is presenting too,with his own marks, and Alice's mark is invisible to him.
Reading it follows the display-time seam, with one deliberate difference. WHO is asking comes
off the live
AccessContext— request-scoped first, then per-circuit — resolved once on therender turn and passed down as a value, exactly like
ViewerZoneId()/ViewerLocale(). TheVALUE, unlike the time zone, is read live off the viewer's own
Usernode: a time zone changesabout never, so a snapshot taken when the circuit opened is always right, but a presentation toggle
is flipped in the seconds before a screen share, and a context snapshot would still say "off" at
exactly that moment. A stale zone is a cosmetic lag; a stale screen is the leak.
ScreenOfbinds toGetMeshNodeStream(viewer)— the shared handle the portal chrome already holds open — so thetoggle is instant and costs no new machinery.
3. Where the filter goes — where a surface PAINTS, never in the query
MeshSearchView— one filter over the results everyMeshSearchControlrendersBuildPinnedItems— before the query string is builtBuildHome/BuildCatalog— sameBuildApps— same. A~/entry is a system AREA, not a mesh path, so it passes through@completionsChatCompletionOrchestrator.Screened(one seam for every producer) +MeshNodeAutocompleteProviderThree of the home's four tabs filter earlier than the paint, and that exception is the interesting
part: a pinned path, a shared-band target and an installed app path are all interpolated into the
control's query string, which the search view exposes in its options editor and carries in the
hq=parameter of "open in search". A marked name reaching the address bar mid-presentation is theleak, whether or not a card for it is ever drawn. A tab the screen empties is dropped, not shown
empty — a tab labelled "Pinned" with nothing under it says something by itself. Everywhere else the query is deliberately left alone — a
-path:Acmeclause would put thesame name in the same URL and start turning this into decision 1's second permission system.
TheCatalogQueryItselfIsNeverRewrittenpins that.Nothing paints before the screen is known.
MeshSearchViewstarts its result source on thefirst screen emission, so a marked item cannot flash. Anonymous / system / hub callers resolve
synchronously to
Off, so those views start in the same turn they always did.A faulting profile stream HOLDS the last known screen and logs it, rather than resetting to
neutral. Resetting mid-presentation is the leak the feature exists to prevent; refusing to emit at
all would hang every surface waiting on it. Both legs are unit-tested against a faulting source,
including the per-subscription isolation of the latch.
4. What the viewer sees
A glasses glyph in the portal header, present only while the mode is on (language-neutral icon +
translated tooltip), and the toggle in the user menu showing
Presentation mode · On/Off. The nodemenu entry flips between Hide and Show live, and shows the mark even while the mode is off —
that is how you curate the list before you start presenting. Marking is not gated on any
permission of the target: you are editing your own profile, not that node.
Non-vacuity
Two control runs on the same machine, each neutering one half of the change:
PresentationScreen.Hides()forced tofalseHides()restored; the three surface call sites handedPresentationScreen.Offinstead of the viewer's screenThe second is the one that matters: it discriminates "the predicate works" from "the surface is
actually wired to it".
Suites run (local, Release)
MeshWeaver.Graph.Test(full)MeshWeaver.Autocomplete.TestMeshWeaver.Documentation.Test— link integrityMeshWeaver.Messaging.Hub.Test— localizationNew tests: 44 across
PresentationScreenTest(the pure screen, the two writes, the resolver'sfault behaviour, the completion projection),
PresentationSurfacesTest(every named surface, bothpolarities), and
PresentationScreenMeshTest(live mesh: per-user isolation, grants/denies nothing,the toggle without a reload, a viewer with no profile yet).
Deliberately out of scope — the follow-ups
your portal is exactly the moment you want your confidential names off it, so shipping remote
control first would mean the first person to use it shares their engagement list. The share's
lifecycle — explicitly initiated, per-share, with the initiator seeing who is watching and the
privacy screen engaging for its duration — is designed but not built.
MeshWeaver.OgCardleft the platform in Import, OgCard and their samples leave the platform #1975 while this was inflight, so that surface is no longer in this repo. It now lives in MeshWeaver.Plugins at
src/MeshWeaver.OgCard/OgCardLayoutArea.cs, with tests insrc/MeshWeaver.OgCard.Test/— and itbuilds via
ProjectReferenceto$(MeshWeaverRoot)/src/MeshWeaver.Graph, not a NuGet package,so
PresentationScreenandhost.ViewerScreen()are available there the moment this merges. Norelease needed; the follow-up is small.
The rule to carry across: a screened target is dropped, not redacted — a card whose title
reads "hidden" still tells the room that something is being hidden. Concretely that was
Render→ filter the parsed targets → drop the whole area when none survive, rather thansubstituting a placeholder card.
MeshSearchrenderer does not apply the screen yet.Doc/GUI/PresentationModecarries the design, the rules for adding a surface, and that samenot-yet-covered list.
🤖 Generated with Claude Code