Skip to content

refactor: BlockUpdate typed events + BlockContext provider pattern#289

Merged
toadkicker merged 5 commits intomainfrom
feat/6b0d-browsing-the-web
Apr 15, 2026
Merged

refactor: BlockUpdate typed events + BlockContext provider pattern#289
toadkicker merged 5 commits intomainfrom
feat/6b0d-browsing-the-web

Conversation

@toadkicker
Copy link
Copy Markdown
Contributor

Summary

  • BlockUpdate — new wire type in papillon-shared that carries only backend-owned fields. linked_block_ids and auto_expand are structurally absent, so the type system prevents events from ever overwriting frontend-only state. All 9 emit sites in commands/canvas.rs migrated from CanvasBlock to BlockUpdate.

  • apply_block_event simplified — the save-and-restore pattern is deleted. The new body does direct field assignment; linked_block_ids and auto_expand are never in scope. New frontend-only fields added to CanvasBlock in future are automatically safe without touching this function.

  • BlockContext provider — replaces SourceBlockId(pub String) with a #[derive(Clone, Copy)] Leptos context struct carrying all per-block reactive UI state (id, expanded, show_reprompt, reprompt_value). BlockRenderer provides it via a single provide_context(block_ctx). BrowseBar drops its block_id and on_expand props and reads from context. generic.rs ExternalUrl arm migrated to BlockContext.

Test plan

  • cargo test -p papillon-shared — 250 tests pass
  • cargo check -p papillon — zero warnings (unused CanvasBlock import removed)
  • cargo check --target wasm32-unknown-unknown (frontend) — clean
  • grep -rn "SourceBlockId" apps/papillon/ — zero source matches
  • grep -n "linked_block_ids.*Vec::new\|auto_expand.*false" commands/canvas.rs — zero matches
  • Manual smoke: submit prompt → phase dots animate, prompt_text preserved on final block
  • Manual smoke: browse block auto-expand retained across all phase events
  • Manual smoke: in-block URL bar navigates to linked block, old block collapses

🤖 Generated with Claude Code

Todd Baur and others added 5 commits April 14, 2026 22:15
Entering pap://ebay.com (or any external domain) in the address bar now
browses the site as a canvas block instead of misrouting to a registry peer.

Registries are federated and announced at handshake time — not addressed
via the URI scheme. The old is_registry_host() treated every dotted FQDN
as a registry, causing pap://domain.com to fall through to On-Device AI.

Changes:
- pap_uri.rs: split is_registry_host into is_local_registry (localhost/IP,
  still Registry) and is_web_domain (dotted FQDN → HttpsEndpoint). External
  domains now resolve to HttpsEndpoint("https://..."), triggering the
  existing starts_with:["https://"] intent rule → Web Page Reader agent.
  Path and port are preserved (pap://ebay.com/electronics works). 41 tests.
- templates.rs: add WebPageTemplate rendering schema:WebPage JSON-LD as a
  browser-tab-style block — URL bar (pap:// link for in-canvas navigation),
  title, byline (publisher · author · date), description, body text extract.
- mod.rs: register WebPageTemplate in create_default_registry().
- topbar.rs: pap:// suggestion dropdown now shows "Browse pap://domain.com"
  for web domains and auto-submits on selection; catalog agent completions
  are unchanged.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
**Change 1 — Ask Once**
- `preference_engine`: add `has_approved_scopes` (checks prior grants cover all
  required scopes) and `save_approved_scopes` (upserts approval record, unlike
  `record_scope_approved` which silently bails on missing rows)
- `canvas.rs` (backend): widen `auto_approve` to skip gate when prior approval
  exists; call `save_approved_scopes` after each grant so approval persists

**Change 2 — Linked Data Navigation**
- `canvas.rs` (frontend): `resolve_prompt_text` now returns `Option<ResolvedUri>`
  instead of `Option<String>`; `dispatch_prompt_inner` gains `resolved_uri` +
  `source_block_id`; `submit_agent_link` accepts optional source block ID
- `mod.rs`: expose `SourceBlockId` Leptos context (current block's ID) so child
  renderers can link new blocks back to their origin
- `generic.rs`: `ExternalUrl` arm rewrites `https://` → `pap://` and renders as
  `<a class="pap-link typed-url-nav">` with `submit_agent_link(url, source_id)`;
  any url field on any schema.org entity is now a canvas navigation action
- `web_reader.rs`: 2k → 8k text limit; paragraph-aware `extract_visible_text`
  preserving `\n\n` at block-level element boundaries; `extract_mentions`
  collects up to 12 same-origin links as `schema:WebPage` `mentions` array

**Change 3 — Browse Block Expansion**
- `types.rs`: add `auto_expand: bool` (serde default false) to `CanvasBlock`
- `canvas.rs` (frontend): set `auto_expand` from `ResolvedUri::HttpsEndpoint`;
  `apply_block_event` preserves `auto_expand` (backend events carry default false)
- `mod.rs`: `expanded` signal initialised from `block.auto_expand`; dynamic
  `canvas-block--expanded` class when resolved+expanded; hover-reveal expand
  toggle button on all resolved blocks; `BrowseBar` component shown in expanded
  browse blocks — prefills URL, Enter spawns linked block + collapses self
- `main.css`: `.canvas-block--expanded` (fixed, full viewport), `.block-expand-btn`
  (hover-reveal absolute), `.canvas-block-browse-bar` + `.browse-bar-input`,
  `.typed-url-nav` (purple, monospace)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the brittle save-and-restore in `apply_block_event` with a
structural type-level solution.

## BlockUpdate (backend → frontend wire type)
- New `BlockUpdate` struct in `papillon-shared/types.rs` carries ONLY
  backend-owned fields. `linked_block_ids` and `auto_expand` are
  structurally absent — the compiler enforces that events can never
  overwrite frontend-only state.
- `BlockEvent.block` changed from `CanvasBlock` to `BlockUpdate`.
- All 9 emit sites in `commands/canvas.rs` migrated to `BlockUpdate`,
  dropping `linked_block_ids: Vec::new()` and `auto_expand: false`.

## apply_block_event (frontend state)
- Signature changes from `(event_block: CanvasBlock)` to
  `(update: BlockUpdate)`.
- Body replaced: direct field assignment, no save/restore loop.
  New frontend-only fields on `CanvasBlock` are automatically safe.

## BlockContext (Leptos provider)
- Replaces `SourceBlockId(pub String)` with `BlockContext`, a
  `#[derive(Clone, Copy)]` context struct carrying all per-block
  reactive UI state: `id`, `expanded`, `show_reprompt`, `reprompt_value`.
- `BlockRenderer` packs these into a single `provide_context(block_ctx)`
  instead of declaring four separate locals.
- `BrowseBar` drops its `block_id: String` and `on_expand: RwSignal<bool>`
  props; reads `BlockContext` from context instead.
- `generic.rs` ExternalUrl arm migrates from `SourceBlockId` to
  `BlockContext.id.get_value()`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ate/BlockContext

Incoming main had 3 conflicting files (block_renderer/mod.rs,
block_renderer/generic.rs, state/canvas.rs) still referencing
SourceBlockId and the old apply_block_event save/restore body.
Resolved all conflicts by keeping our refactor in full.

Also stripped 9 `auto_expand: false` lines that git auto-merged back
into commands/canvas.rs from the origin/main side (BlockUpdate has no
such field by design).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@toadkicker toadkicker merged commit c058715 into main Apr 15, 2026
1 check passed
@github-actions
Copy link
Copy Markdown

Benchmark Regression Report

PAP Protocol Benchmark Regression Check
========================================
Baseline: .bench-baseline/baseline.json
Threshold: 30%

  ed25519_keypair_generation                17.3 µs  (baseline: 19.7 µs, -12.5%)  [ok]
  did_key_derivation                         1.4 µs  (baseline: 1.5 µs, -7.9%)  [ok]
  mandate_create_sign                       21.9 µs  (baseline: 23.9 µs, -8.5%)  [ok]
  mandate_chain_verify_depth3              139.2 µs  (baseline: 126.9 µs, +9.6%)  [ok]
  sd_jwt_issue_5claims                      25.1 µs  (baseline: 27.9 µs, -9.7%)  [ok]
  sd_jwt_verify_disclose_3of5               49.0 µs  (baseline: 44.3 µs, +10.6%)  [ok]
  session_open_full_lifecycle               98.0 µs  (baseline: 108.7 µs, -9.8%)  [ok]
  receipt_create_cosign                     44.2 µs  (baseline: 48.9 µs, -9.7%)  [ok]
  federation_announce_local                 53.7 µs  (baseline: 56.0 µs, -4.1%)  [ok]

All benchmarks within 30% of baseline.

Threshold: 10% regression vs baseline from main

toadkicker pushed a commit that referenced this pull request Apr 15, 2026
- types.rs:1257 — remove `auto_expand: false` from BlockUpdate test
  initialiser; `auto_expand` was dropped from the struct in the #289
  refactor but the test literal was not updated, causing a compile error
  (E0560) across Check, Test, WASM backend, and Justfile recipes
- web_reader.rs:112 — remove needless borrow `&final_url`; Rust deref-
  coerces String → &str automatically, Clippy's needless_borrow lint
  (-D warnings) was failing Check, Test, Clippy, and Justfile recipes

Both errors were pre-existing on main and blocked every PR. All CI
checks for papillon-extension PR #290 should now be green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
toadkicker added a commit that referenced this pull request Apr 15, 2026
* feat(extension): auto-intercept https:// link clicks → pap://

Ordinary left-clicks on https:// links are now routed through the PAP
handshake automatically, making zero-trust browsing the default when
Papillon is active — no right-click context menu required.

Changes:
- content-script: capture-phase click handler converts https:// hrefs to
  pap:// and sends HTTPS_LINK_CLICKED to the service worker; guards pass
  through modifier clicks (Ctrl/Meta/Shift), Alt-click (per-click opt-out),
  synthetic events, download links, and per-domain exclusions; storage state
  is loaded at script init and kept live via chrome.storage.onChanged
- service-worker: new HTTPS_LINK_CLICKED case swaps https:// → pap:// and
  calls openHandshakeTab with the original URL as fallback
- types: HttpsLinkClicked interface + ExtensionMessage union member
- popup: global auto-intercept toggle (aria role=switch, default on) and
  per-domain "Disable on this site" button backed by chrome.storage.sync
- popup.css: toggle track/thumb via ::after + aria-checked; domain button
  teal accent when excluded

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(extension): add unit tests for uri, discovery, and intercept logic

Brings test coverage from 1 file (db.ts) to 4 files with 117 tests passing.

- src/lib/uri.test.ts (28 tests): full coverage of parsePapUri, httpsUrlToPap,
  isPapUri, toEndpoint, toHttpsEndpoint, isBrowserCompatible — all pure
  functions, zero mocks needed

- src/lib/discovery.test.ts (23 tests): validateManifest (pure, boundary checks
  for field lengths and array limits) + fetchManifest (fetch stubbed via
  vi.stubGlobal — covers 200/404/500, wrong Content-Type, oversized bodies,
  network errors, origin-only vs full-path URLs)

- src/content/intercept-logic.ts (new): extracts resolveInterceptUrl() from
  content-script.ts as a pure, side-effect-free function so it can be tested
  in Node without jsdom or Chrome API mocks

- src/content/intercept-logic.test.ts (25 tests): exercises all 8 guard
  conditions (button, modifiers, isTrusted, download attr, scheme, state
  flags, domain exclusion) plus happy-path URL resolution and relative
  href handling

- vitest.config.ts: enable @vitest/coverage-v8 with text + lcov reporters
  and a 60% lines threshold

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(extension): eliminate duplicate constants and magic values

- New src/lib/constants.ts: single source of truth for the two
  chrome.storage.sync key strings (autoInterceptHttps, excludedDomains)
  previously duplicated across content-script.ts and popup.ts
- Export NATIVE_APP_ID from native-messaging.ts; remove the identical
  private copy in service-worker.ts
- discovery.ts: replace 5 repeated typeof/length checks with an
  isValidString() type-predicate helper; name every magic limit
  (MAX_AGENT_ID_LEN, MAX_TOOLS, MAX_CATEGORIES, etc.); fix the unsafe
  `as string` cast in categories by using a type-predicate filter
- service-worker.ts: file-local named constants for badge hex colors
  (BADGE_COLOR_GOLD, BADGE_COLOR_PURPLE) and context-menu ID
  (CONTEXT_MENU_UPGRADE_ID) replace four inline magic strings
- popup.ts: $<T>() DOM helper replaces nine verbose getElementById calls

All 117 tests pass unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(rust): correct two regressions from BlockUpdate refactor (#289)

- types.rs:1257 — remove `auto_expand: false` from BlockUpdate test
  initialiser; `auto_expand` was dropped from the struct in the #289
  refactor but the test literal was not updated, causing a compile error
  (E0560) across Check, Test, WASM backend, and Justfile recipes
- web_reader.rs:112 — remove needless borrow `&final_url`; Rust deref-
  coerces String → &str automatically, Clippy's needless_borrow lint
  (-D warnings) was failing Check, Test, Clippy, and Justfile recipes

Both errors were pre-existing on main and blocked every PR. All CI
checks for papillon-extension PR #290 should now be green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Todd Baur <todd@baursoftware.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
toadkicker pushed a commit that referenced this pull request Apr 15, 2026
After rebasing on main (BlockUpdate typed events + BlockContext provider
pattern), the 6 emit sites in dataset_discovery.rs used the old CanvasBlock
struct in BlockEvent — now expects BlockUpdate.

Changes:
- Replace CanvasBlock with BlockUpdate at all 6 emit sites, dropping
  linked_block_ids (frontend-only field, absent from BlockUpdate by design)
- Introduce HandshakeConfig struct to reduce run_dataset_handshake from 8
  to 5 args (fixes clippy::too_many_arguments)
- Add HandshakeTaskResult type alias for JoinSet element type
  (fixes clippy::type_complexity)
- Use *= for relevance_score blending (fixes clippy::assign_op_pattern)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
toadkicker added a commit that referenced this pull request Apr 15, 2026
…#291)

* feat: Croissant ML dataset discovery via federation-native PAP agents

Adds parallel multi-source ML dataset discovery routed through the full
6-phase PAP handshake. A new `schema:DatasetAction` intent routes dataset
queries to all matching zero-disclosure agents concurrently via JoinSet,
rather than picking a single top-scored agent.

Architecture:
- Two TOML catalog agents (HuggingFace Hub, OpenML) require zero disclosure
  and self-configure via the existing FederatedRegistry — adding a third
  source requires only a new .toml file, no Rust changes
- `canvas_discover_datasets` fans out to all `schema:DatasetAction` agents
  in parallel, merges results sorted by preference-blended relevance score,
  and emits progressive `block_updated` events during the JoinSet fan-out
- Long-horizon memex integration: FTS5 prior-art check (7-day TTL) surfaces
  cached results immediately while fresh handshakes run; per-agent episodes
  feed PreferenceEngine signals so the system learns provider preference over time
- `DatasetState` Leptos context (keyed by block_id) carries discovery phase
  FSM and memex hints to the `DatasetSearchTemplate` block renderer
- Mandate TTL set to 7 days — dataset metadata is durable; the existing
  Active→Degraded→ReadOnly decay UI prompts re-query naturally

New files: dataset_types.rs, dataset_discovery.rs, dataset_template.rs,
           dataset.rs (state), huggingface_datasets.toml, openml_datasets.toml

All 385 tests passing; papillon-shared, papillon backend, and papillon-frontend
WASM targets compile clean.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* test(intent): expand coverage from 32 to 47 tests

Closes gaps identified in intent rule coverage:

- Free Dictionary: 3 new tests (define/meaning of/definition of) — rule had
  zero coverage previously; any regression would have gone undetected
- Clean-query stripping: 3 tests verify the third return value of detect_intent()
  (previously almost never checked); confirms "dataset sentiment analysis" strips
  to "sentiment analysis", "define photosynthesis" to "photosynthesis", etc.
- Rule ordering conflicts: 3 tests document first-match-wins behavior for
  ambiguous inputs (weather beats dataset, dataset beats books, dataset beats
  arXiv) — establishes intended semantics and guards against rule reordering
- Missing keywords: 2 tests for "temperature" (weather rule) and "population of"
  (REST Countries rule) which were declared in RULES but never exercised

All 47 intent tests pass; 0 regressions.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: adapt dataset_discovery to BlockUpdate wire type from #289 refactor

After rebasing on main (BlockUpdate typed events + BlockContext provider
pattern), the 6 emit sites in dataset_discovery.rs used the old CanvasBlock
struct in BlockEvent — now expects BlockUpdate.

Changes:
- Replace CanvasBlock with BlockUpdate at all 6 emit sites, dropping
  linked_block_ids (frontend-only field, absent from BlockUpdate by design)
- Introduce HandshakeConfig struct to reduce run_dataset_handshake from 8
  to 5 args (fixes clippy::too_many_arguments)
- Add HandshakeTaskResult type alias for JoinSet element type
  (fixes clippy::type_complexity)
- Use *= for relevance_score blending (fixes clippy::assign_op_pattern)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Todd Baur <todd@baursoftware.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
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.

1 participant