Skip to content

Unified search: prerendered→live hydration (modes + diagnostic attribute)#5177

Merged
habdelra merged 7 commits into
mainfrom
cs-11435-unified-search-hydration-lazy-get-linksself-modes-diagnostic
Jun 10, 2026
Merged

Unified search: prerendered→live hydration (modes + diagnostic attribute)#5177
habdelra merged 7 commits into
mainfrom
cs-11435-unified-search-hydration-lazy-get-linksself-modes-diagnostic

Conversation

@habdelra

@habdelra habdelra commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Phase 1 / Stream B (host rendering) of Unify prerendered-card-search and card-search: prerendered→live hydration for unified search results.

Stacked on #5176. Review the top commit here; base retargets to main as the stack lands.

Behavior

An HTML-backed unified search row ships an identity-only card + inert rendered-html — the live data is not in the response, so making it a live, running card costs one GET links.self. HydratableCard owns that lifecycle:

  • Renders the inert HTML, then swaps it for a live <CardRenderer @codeRef={{renderType}}> on a host-side gesture: none (stay inert), hover, click, or touch. The mode is a host UX choice and never travels on the wire.
  • The trigger is a listener on the rendered-html element, owned by the rendering path — not the operator-mode overlay — so it behaves identically in operator mode, host mode, and published views with no separate fallback.
  • A full live row (no inert HTML) resolves its instance directly; an error rendering never hydrates.
  • Hydration resolves links.self through the getCard resource (one GET, deposit in the Store, track live) and renders the live card under the collection's render type.
  • A data-hydration attribute carries the gesture (or hydrated after the swap) for diagnosis — no user-facing affordance.

Operator-mode coexistence

The overlay (container interaction) and hydration (making the card live) are orthogonal layers. The optional cardComponentModifier from the card context is applied to whichever element is shown, so the overlay's ElementTracker anchors to the inert HTML and re-anchors to the live card after the swap — the inert element unregisters as the live element registers. Host mode / published views have no such context, so a no-op stands in.

Demo

This is a screen capture of the hydration

Screencast.from.2026-06-09.21-42-04.mp4

Tests

Integration tests render HydratableCard directly (the full search-UI migration is a later step), covering:

  • Host mode (no overlay) — hover → one GET links.self, the card enters the Store, inert HTML swaps to a live CardRenderer.
  • Published view (no overlay) — same, via the click gesture.
  • Operator mode (overlay present) — hydration and overlay coexist; the swap re-registers the new element with the ElementTracker (exactly one tracked element, now the live one).
  • none stays inert, marks data-hydration="none", and never fetches; error rows never hydrate.

All 5 pass locally against the host test-services stack.

HydratableCard renders an HTML-backed search row inert, then swaps it for a
live <CardRenderer> on a host-side gesture: none (stay inert), hover, click,
or touch. The trigger is a listener owned by the rendering path itself — not
the operator-mode overlay — so hydration behaves identically in operator
mode, host mode, and published views. A full live row (no inert HTML)
resolves its instance immediately; an error rendering never hydrates.

Hydration resolves links.self through the getCard resource (one GET, deposit
in the Store, track live) and renders the live card under the collection's
render type. The optional cardComponentModifier from the card context is
applied to whichever element is shown, so the overlay's ElementTracker
anchors to the inert HTML and re-anchors to the live card after the swap. A
data-hydration attribute carries the gesture (or "hydrated") for diagnosis.

Tests cover host mode, published view, and operator-mode overlay coexistence
(re-registration with the ElementTracker), plus the none and error cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fc4b80158e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/host/app/components/card-search/hydratable-card.gts Outdated
@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Preview deployments

Host Test Results

    1 files      1 suites   1h 53m 4s ⏱️
3 033 tests 3 018 ✅ 15 💤 0 ❌
3 052 runs  3 037 ✅ 15 💤 0 ❌

Results for commit 0b23d64.

Realm Server Test Results

    1 files  ± 0      1 suites  ±0   10m 6s ⏱️ -48s
1 610 tests +35  1 610 ✅ +35  0 💤 ±0  0 ❌ ±0 
1 701 runs  +35  1 701 ✅ +35  0 💤 ±0  0 ❌ ±0 

Results for commit 0b23d64. ± Comparison against earlier commit cbbc07e.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Introduces a new host-side HydratableCard component to support unified-search rows that initially render inert prerendered HTML and optionally “hydrate” into a live CardRenderer on a user gesture, while preserving operator-mode overlay tracking via cardComponentModifier.

Changes:

  • Added HydratableCard component implementing gesture-based hydration (none/hover/click/touch) and a diagnostic data-hydration attribute.
  • Integrated Store-backed live resolution via getCard once hydration is triggered (or immediately for fully-live rows).
  • Added integration tests covering host mode, published view, operator-mode overlay re-anchoring, none, and error rows.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
packages/host/app/components/card-search/hydratable-card.gts New component that swaps inert HTML for live card rendering on gesture and reuses overlay element tracking.
packages/host/tests/integration/components/hydratable-card-test.gts New integration tests validating hydration behavior and operator-mode ElementTracker re-registration.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/host/app/components/card-search/hydratable-card.gts
Comment thread packages/host/app/components/card-search/hydratable-card.gts
Comment thread packages/host/app/components/card-search/hydratable-card.gts
habdelra and others added 2 commits June 9, 2026 22:04
…source type

- Consume `getCard` through `GetCardContextName` (a `@cached` getter so the
  provider is injected before it runs) instead of importing the resource
  helper directly, so the component uses the Store the active host context
  provides.
- Guard the `cardContext` read with an `isDestroying`/`isDestroyed` check plus
  an owner-destroyed catch, so reading the context while the component is being
  torn down can't throw.
- Carry a `type` arg (`card` | `file-meta`) through to `getCard`, and resolve
  the live instance as `BaseDef`, so a full live `file-meta` row renders its
  `FileDef` instead of nothing.
- Drop the redundant explicit tracker on the live `CardRenderer`: a
  delegate-rendered card registers itself through the card context, so the
  inert→live swap re-anchors the overlay on its own. The inert HTML still needs
  the explicit tracker (raw HTML isn't a card component).

Tests provide `GetCardContext`, and the operator-mode test asserts precisely
that the re-registered live element carries the live card instance.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ve instance

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread packages/host/app/components/card-search/hydratable-card.gts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment thread packages/host/app/components/card-search/hydratable-card.gts
Comment thread packages/host/tests/integration/components/hydratable-card-test.gts
…s hydration

- liveCard short-circuits before touching cardResource when there's no id to
  resolve, so an inert row that never hydrates (e.g. mode none) never creates a
  getCard resource. The resource is `@cached` (one per component) and parented
  to the component, so getCard drops its Store reference on teardown — no leak.
- Keep the live render wrapper-free: the diagnostic / test attributes ride
  `...attributes` onto the card-api container (which spreads them regardless of
  the userland template), so no extra element is needed (which would perturb
  grid / child-selector styling). Locked in with a `data-hydration="hydrated"`
  assertion after the swap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment on lines +49 to +54
let event =
mode === 'hover'
? 'mouseenter'
: mode === 'touch'
? 'touchstart'
: 'click';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

How about a focus event?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Claude Code 🤖] Good call — done in 306ff3f. In hover mode the trigger now also listens for focusin (it bubbles, unlike focus, so keyboard focus landing anywhere in the card subtree counts), hydrating identically for pointer-hover and keyboard-focus. Added a test asserting a focusin hydrates the inert HTML and flips data-hydration to hydrated, same as mouseenter.

The hover gesture hydrated only on `mouseenter`, leaving keyboard users on the
inert prerendered HTML. In hover mode the trigger now also listens for
`focusin` (which bubbles, so focus landing anywhere in the card subtree during
keyboard navigation counts), hydrating identically for pointer and keyboard.

Test: in hover mode a `focusin` hydrates the inert HTML into the live card and
flips `data-hydration` to `hydrated`, exactly as `mouseenter` does.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@burieberry burieberry left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Here's from claude -- do as you see fit:

Must fix (confirmed bugs)

  • #1hydrationId stale on @cardId change → change to @tracked private hydrated = false
  • #2OWNER_DESTROYED_ERROR = 'destroyed' → replace with the full string from prerendered-card-search.gts

Should fix (pre-existing, surfaced by this PR)

  • #3search-content.gts has = 'OWNER_DESTROYED_ERROR' (the name, not the value) — broken guard, easy one-liner fix

Worth a comment at minimum

  • #4 — the @component == null fast-path has no gesture gate; a comment explaining that full live rows always resolve immediately would prevent a future caller from misusing @isError reactively

Leave for follow-up (cleanup, no runtime impact today)

  • #5noopCardModifier duplication, trackElement guard duplication, @type snapshot — none break current behavior

… match

- Track hydration as a boolean instead of the `@cardId` captured at gesture
  time, so a recycled HydratableCard whose `@cardId` changes resolves the new
  card rather than a stale captured one.
- Match the full owner-destroyed message in the `trackElement` guard instead of
  the loose `destroyed` substring, so an unrelated error isn't swallowed.
- Fix the same guard in `search-content.gts`, which compared against the
  literal name `OWNER_DESTROYED_ERROR` (so it never matched) instead of the
  actual error message.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@habdelra

habdelra commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

[Claude Code 🤖] Thanks — worked through these (cbbc07e + 306ff3f):

  • 1 (hydration stale on @cardId change) — fixed: hydration is now tracked as a boolean, not the id captured at gesture time, so resolvedId resolves the current @cardId. (Not reachable today — no list-caller renders HydratableCard yet — but a future {{#each}} would have hit it.)
  • 2 (OWNER_DESTROYED_ERROR = 'destroyed') — fixed: now matches the full "Cannot call \.lookup('renderer:-dom')` after the owner has been destroyed"` so an unrelated error isn't swallowed.
  • 3 (search-content.gts compared the literal name, never matched) — fixed to the real message.
  • 4 (@component == null fast-path) — added a comment noting a full live row resolves immediately and ungated.
  • 5 (noop/guard/@type-snapshot duplication) — leaving as a follow-up; no runtime impact.

@habdelra
habdelra changed the base branch from cs-11434-unified-search-host-consumer-selective-store-inflate-live to main June 10, 2026 16:40
@habdelra
habdelra merged commit 9e33fa5 into main Jun 10, 2026
71 of 73 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants