feat(addressing): theorem:// client helper, console surfaces, desktop deep links - #81
Conversation
… deep links
Client half of DESIGN-THEOREM-URI (engine half lands in Theorem).
- packages/block-view/src/addressing.ts: the one client helper the brief
asks for, exported as @commonplace/block-view/addressing. Mirrors the
Rust grammar byte-for-byte; parseTheoremUri returns a discriminated
union rather than throwing, because paste handlers run on every paste.
Hand-parsed on purpose: WHATWG URL treats theorem: as a non-special
scheme and puts the tenant in host, which is how the old mobile router
silently dropped it.
- mobile: re-exports the shared grammar and keeps only the expo-router
mapping. Two corrections — the fragment is now #span={selector} per the
grammar, and the router carries ?v= and #span onto the route instead of
discarding them, since a shared clipping that opens unspanned is a link
that lies.
- console: the inspector footer renders the address where the bare id
was, cards carry copy-address, mention chips and action-pack context
carry the address, and the Composer and Search field accept pasted
addresses (offering, never auto-inserting) with refusals shown rather
than swallowed.
- desktop: tauri-plugin-deep-link registered on Tauri v2 with the
theorem scheme, plus an onOpenUrl acceptor in apps/web, which is the
surface the Tauri window actually renders.
Tests: console 131/131, mobile 47/47, tsc clean in both, import-fence,
register, motion, and icon gates clean.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
|
Warning Review limit reached
Next review available in: 30 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThe PR introduces a shared ChangesTheorem addressing
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Clipboard
participant Console
participant Addressing
participant WebShell
Clipboard->>Console: paste theorem:// text
Console->>Addressing: extract and parse address
Addressing-->>Console: address or refusal
Console->>WebShell: open encoded object route
WebShell->>Addressing: parse route address
Addressing-->>WebShell: resolved address
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Adds client-side support for canonical theorem:// addressing across shared grammar utilities, console UI/UX entry points (copy/paste/mentions/search), and desktop deep linking via Tauri—so objects can be named uniformly across surfaces and opened from OS-level links.
Changes:
- Introduces
@commonplace/block-view/addressingwith emit/parse helpers and span-selector support. - Wires
theorem://addresses into Console surfaces (inspector/card copy, action packs, composer paste offers, search resolve lane). - Enables Desktop deep links (Tauri deep-link plugin + web acceptor route and listener).
Reviewed changes
Copilot reviewed 32 out of 33 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Adds the Tauri deep-link plugin dependency to the lockfile. |
| packages/block-view/src/index.ts | Re-exports the new addressing helper from the block-view package entrypoint. |
| packages/block-view/src/addressing.ts | Implements the shared theorem:// grammar helpers (emit/parse/span/bounds/extract). |
| packages/block-view/package.json | Exposes ./addressing as a package export path. |
| crates/commonplace-desktop-runtime/src/lib.rs | Initializes deep-link plugin and registers scheme on Windows/Linux at runtime. |
| crates/commonplace-desktop-runtime/Cargo.toml | Adds tauri-plugin-deep-link Rust dependency. |
| apps/web/src/lib/theorem-address-route.ts | Maps parsed theorem addresses into the web shell’s object route via a single query param. |
| apps/web/src/lib/theorem-address-route.test.ts | Tests round-tripping and refusal behavior for the web route mapping. |
| apps/web/src/lib/desktop.ts | Adds dependency-free IPC wrappers for deep-link plugin (get_current, deep-link://new-url). |
| apps/web/src/components/commonplace/deeplink/TheoremDeepLink.tsx | Accepts deep-link URLs (cold + warm) and routes to the addressed object, surfacing refusals via toast. |
| apps/web/src/components/commonplace/deeplink/ObjectAddressView.tsx | Resolves the route query param into the existing drawer-based object surface (or refusal UI). |
| apps/web/src/app/(commonplace)/layout.tsx | Mounts the deep-link acceptor component inside the shell layout. |
| apps/web/src/app/(commonplace)/commonplace/object/page.tsx | Adds the static-export-safe acceptor route (/commonplace/object?address=...). |
| apps/mobile/src/addressing/theoremUriCore.ts | Switches mobile to shared grammar; keeps only expo-router mapping and carries v/span through. |
| apps/mobile/src/addressing/theoremUriCore.test.ts | Adds/updates tests to validate shared grammar parity and routing propagation. |
| apps/desktop/src-tauri/tauri.conf.json | Registers the theorem scheme via the deep-link plugin config. |
| apps/desktop/src-tauri/capabilities/default.json | Grants deep-link:default capability to the desktop app. |
| apps/desktop/package.json | Adds the JS deep-link plugin dependency for the desktop bundle. |
| apps/console/src/views/RecordInspector.tsx | Replaces bare id footer with canonical address display + copy button. |
| apps/console/src/views/CardView.tsx | Adds copy-address affordance to full-size cards. |
| apps/console/src/lib/use-copy.ts | Introduces a console-local clipboard hook with “copied/unavailable” state. |
| apps/console/src/lib/thread-store.ts | Allows staged refs to serialize as canonical addresses when available. |
| apps/console/src/lib/shell-store.ts | Adds address to staged object chips using tenant from the store. |
| apps/console/src/lib/object-address.ts | Centralizes console minting of theorem addresses for objects/loose parts. |
| apps/console/src/lib/object-address.test.ts | Tests console bindings (chip/pack/ref) against the shared grammar. |
| apps/console/src/lib/action-pack.ts | Includes canonical address in action-pack context entries and equality checks. |
| apps/console/src/components/shell/SearchField.tsx | Detects pasted addresses, refuses cross-tenant, and offers “Open …” resolve row. |
| apps/console/src/components/shell/icons.tsx | Adds copy icon glyph for the new address affordance. |
| apps/console/src/components/shell/CopyAddressButton.tsx | Implements a reusable copy-address button using the new clipboard hook. |
| apps/console/src/components/shell/ActionSheet.tsx | Ensures auto-suggested chips carry addresses and staged refs preserve addresses. |
| apps/console/src/components/composer/Composer.tsx | Makes mentions serialize to theorem addresses; adds “paste offer” flow for addresses. |
| apps/console/CLAUDE.md | Documents the addressing + copy subsystem boundaries and single sources of truth. |
| apps/console/AGENTS.md | Mirrors the addressing + copy subsystem documentation for agent contributors. |
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| case 'exact': | ||
| exact = decodeURIComponent(value); | ||
| break; | ||
| case 'prefix': | ||
| prefix = decodeURIComponent(value); | ||
| break; | ||
| case 'suffix': | ||
| suffix = decodeURIComponent(value); | ||
| break; |
| return { | ||
| ok: true, | ||
| address: { | ||
| tenant: decodeURIComponent(tenant), | ||
| kind: decodeURIComponent(kind), | ||
| id: decodeURIComponent(id), | ||
| ...(graphVersion !== undefined ? { graphVersion } : {}), | ||
| ...(span !== undefined ? { span } : {}), | ||
| }, | ||
| }; |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
apps/console/src/components/shell/SearchField.tsx (1)
40-77: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicate address-detection/refusal logic with
Composer.tsx'sonPaste.
probeAddressre-implements the same extract-address, tenant-mismatch refusal, and "no theorem address" fallback logic already present inComposer.tsx'sonPastehandler (same wording, same control flow). Two independent copies risk drifting as either surface evolves (e.g. refusal wording changes in one but not the other).Consider extracting a shared
detectTheoremAddress(raw, tenant)helper (e.g. alongsideobject-address.ts) that bothComposer.tsxandSearchField.tsxcall, so paste and search share one source of truth for detection and refusal messaging.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/console/src/components/shell/SearchField.tsx` around lines 40 - 77, Extract the shared address detection and tenant-refusal flow from probeAddress and Composer.tsx’s onPaste into a shared detectTheoremAddress(raw, tenant) helper near object-address utilities. Preserve the existing handling for mode prefixes, embedded addresses, tenant mismatches, malformed tokens, and the “no theorem address” fallback, then update both callers to use the helper and retain their surface-specific behavior.apps/console/src/components/composer/Composer.tsx (2)
249-300: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winPaste offer/refusal panels aren't announced to screen readers.
CopyAddressButton(apps/console/src/components/shell/CopyAddressButton.tsx) usesaria-live="polite"for its status text; these new panels renderpasteRefusaland the "Pasted address" offer without any live region, so a screen-reader user who just pasted won't be told the offer or refusal appeared.♿ Proposed fix
- {pasted ? ( - <div - className="flex flex-wrap items-center gap-1 border-b border-ij-seam px-2 pt-2" - data-composer-paste-offer - > + {pasted ? ( + <div + className="flex flex-wrap items-center gap-1 border-b border-ij-seam px-2 pt-2" + data-composer-paste-offer + role="status" + aria-live="polite" + >Apply the same
role="status" aria-live="polite"to thepasteRefusalcontainer.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/console/src/components/composer/Composer.tsx` around lines 249 - 300, Update the paste offer and refusal panel containers in the Composer render to use the same role="status" and aria-live="polite" live-region behavior as CopyAddressButton, ensuring both newly rendered “Pasted address” offers and pasteRefusal messages are announced to screen readers.
127-202: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winNo test coverage for the new paste-detection flow.
onPaste,acceptPastedChip, andkeepPastedTextimplement several branches (tenant match, tenant mismatch, malformed address, no address) with real behavioral consequences (tenant-boundary refusal, chip staging), but no test file was included covering this logic.Want me to draft a Vitest suite (mocking
ClipboardEvent/useComposerRuntime) covering the tenant-match, tenant-mismatch, and malformed-address branches?🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/console/src/components/composer/Composer.tsx` around lines 127 - 202, The new paste-detection flow lacks tests for its behavioral branches. Add a focused Vitest suite covering onPaste, acceptPastedChip, and keepPastedText, including same-tenant address staging, cross-tenant refusal with plain-text preservation, malformed or address-free input refusal messaging, canonical chip creation, and keeping pasted text with the updated character count; mock the clipboard event and composer runtime as needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/console/src/components/composer/Composer.tsx`:
- Around line 192-202: Update keepPastedText to append pasted.raw with an
appropriate separator when existing composer text does not already provide one,
then truncate the resulting value to MAX_CHARACTERS before calling
composerRuntime.setText and setCharacterCount. Preserve the existing early
return and clear the pending paste after applying the bounded text.
In `@packages/block-view/src/addressing.ts`:
- Around line 309-323: Update extractTheoremAddress to remove surrounding
sentence punctuation from each whitespace-delimited token before calling
looksLikeTheoremAddress and parseTheoremUri. Preserve valid URI characters while
stripping trailing punctuation such as periods and closing parentheses, so
extracted ids never include prose noise.
---
Nitpick comments:
In `@apps/console/src/components/composer/Composer.tsx`:
- Around line 249-300: Update the paste offer and refusal panel containers in
the Composer render to use the same role="status" and aria-live="polite"
live-region behavior as CopyAddressButton, ensuring both newly rendered “Pasted
address” offers and pasteRefusal messages are announced to screen readers.
- Around line 127-202: The new paste-detection flow lacks tests for its
behavioral branches. Add a focused Vitest suite covering onPaste,
acceptPastedChip, and keepPastedText, including same-tenant address staging,
cross-tenant refusal with plain-text preservation, malformed or address-free
input refusal messaging, canonical chip creation, and keeping pasted text with
the updated character count; mock the clipboard event and composer runtime as
needed.
In `@apps/console/src/components/shell/SearchField.tsx`:
- Around line 40-77: Extract the shared address detection and tenant-refusal
flow from probeAddress and Composer.tsx’s onPaste into a shared
detectTheoremAddress(raw, tenant) helper near object-address utilities. Preserve
the existing handling for mode prefixes, embedded addresses, tenant mismatches,
malformed tokens, and the “no theorem address” fallback, then update both
callers to use the helper and retain their surface-specific behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: f9065f18-33c0-4292-bd6e-c19d82f4d6ea
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (32)
apps/console/AGENTS.mdapps/console/CLAUDE.mdapps/console/src/components/composer/Composer.tsxapps/console/src/components/shell/ActionSheet.tsxapps/console/src/components/shell/CopyAddressButton.tsxapps/console/src/components/shell/SearchField.tsxapps/console/src/components/shell/icons.tsxapps/console/src/lib/action-pack.tsapps/console/src/lib/object-address.test.tsapps/console/src/lib/object-address.tsapps/console/src/lib/shell-store.tsapps/console/src/lib/thread-store.tsapps/console/src/lib/use-copy.tsapps/console/src/views/CardView.tsxapps/console/src/views/RecordInspector.tsxapps/desktop/package.jsonapps/desktop/src-tauri/capabilities/default.jsonapps/desktop/src-tauri/tauri.conf.jsonapps/mobile/src/addressing/theoremUriCore.test.tsapps/mobile/src/addressing/theoremUriCore.tsapps/web/src/app/(commonplace)/commonplace/object/page.tsxapps/web/src/app/(commonplace)/layout.tsxapps/web/src/components/commonplace/deeplink/ObjectAddressView.tsxapps/web/src/components/commonplace/deeplink/TheoremDeepLink.tsxapps/web/src/lib/desktop.tsapps/web/src/lib/theorem-address-route.test.tsapps/web/src/lib/theorem-address-route.tscrates/commonplace-desktop-runtime/Cargo.tomlcrates/commonplace-desktop-runtime/src/lib.rspackages/block-view/package.jsonpackages/block-view/src/addressing.tspackages/block-view/src/index.ts
| const keepPastedText = useCallback(() => { | ||
| if (!pasted) return; | ||
| // The paste was held, so the caret offset is gone: the text appends. The | ||
| // person sees exactly what they copied, which is the point of the offer. | ||
| const next = `${composerRuntime.getState().text}${pasted.raw}`; | ||
| composerRuntime.setText(next); | ||
| // The runtime write bypasses the textarea's change event, so the counter | ||
| // is told directly rather than left reading a stale length. | ||
| setCharacterCount(next.length); | ||
| setPasted(null); | ||
| }, [composerRuntime, pasted]); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
keepPastedText bypasses the composer's character limit and can run words together.
composerRuntime.setText(next) writes programmatically, so the textarea's maxLength={MAX_CHARACTERS} (line 306) never applies here; a long share-sheet clipboard blob can push the composer well past the 2000-character budget that every other input path enforces. The raw text is also appended with no separator, so it can run into the end of existing composer text.
🩹 Proposed fix
const keepPastedText = useCallback(() => {
if (!pasted) return;
// The paste was held, so the caret offset is gone: the text appends. The
// person sees exactly what they copied, which is the point of the offer.
- const next = `${composerRuntime.getState().text}${pasted.raw}`;
+ const current = composerRuntime.getState().text;
+ const separator = current && !current.endsWith('\n') ? '\n' : '';
+ const next = `${current}${separator}${pasted.raw}`.slice(0, MAX_CHARACTERS);
composerRuntime.setText(next);
// The runtime write bypasses the textarea's change event, so the counter
// is told directly rather than left reading a stale length.
setCharacterCount(next.length);
setPasted(null);
}, [composerRuntime, pasted]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const keepPastedText = useCallback(() => { | |
| if (!pasted) return; | |
| // The paste was held, so the caret offset is gone: the text appends. The | |
| // person sees exactly what they copied, which is the point of the offer. | |
| const next = `${composerRuntime.getState().text}${pasted.raw}`; | |
| composerRuntime.setText(next); | |
| // The runtime write bypasses the textarea's change event, so the counter | |
| // is told directly rather than left reading a stale length. | |
| setCharacterCount(next.length); | |
| setPasted(null); | |
| }, [composerRuntime, pasted]); | |
| const keepPastedText = useCallback(() => { | |
| if (!pasted) return; | |
| // The paste was held, so the caret offset is gone: the text appends. The | |
| // person sees exactly what they copied, which is the point of the offer. | |
| const current = composerRuntime.getState().text; | |
| const separator = current && !current.endsWith('\n') ? '\n' : ''; | |
| const next = `${current}${separator}${pasted.raw}`.slice(0, MAX_CHARACTERS); | |
| composerRuntime.setText(next); | |
| // The runtime write bypasses the textarea's change event, so the counter | |
| // is told directly rather than left reading a stale length. | |
| setCharacterCount(next.length); | |
| setPasted(null); | |
| }, [composerRuntime, pasted]); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/console/src/components/composer/Composer.tsx` around lines 192 - 202,
Update keepPastedText to append pasted.raw with an appropriate separator when
existing composer text does not already provide one, then truncate the resulting
value to MAX_CHARACTERS before calling composerRuntime.setText and
setCharacterCount. Preserve the existing early return and clear the pending
paste after applying the bounded text.
| /** | ||
| * Pull the first `theorem://` address out of arbitrary pasted text. | ||
| * | ||
| * Share sheets wrap an address in a title and a newline (`shareObject` in the | ||
| * mobile object drawer does exactly that), so accepting a paste means finding | ||
| * the address inside the noise rather than demanding a bare URI. | ||
| */ | ||
| export function extractTheoremAddress(text: string): TheoremAddress | undefined { | ||
| for (const token of text.split(/\s+/)) { | ||
| if (!looksLikeTheoremAddress(token)) continue; | ||
| const parsed = parseTheoremUri(token.trim()); | ||
| if (parsed.ok) return parsed.address; | ||
| } | ||
| return undefined; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Trailing punctuation can leak into the extracted id.
extractTheoremAddress splits pasted text purely on whitespace (text.split(/\s+/)), so a token adjacent to sentence punctuation (e.g. theorem://t/doc/d1. or (theorem://t/doc/d1)) gets parsed with that punctuation folded into id, since most punctuation characters don't make parseTheoremUri refuse. This silently produces a wrong-but-plausible address from ordinary prose containing a link, rather than surfacing a refusal or stripping the noise.
🩹 Suggested fix: trim trailing punctuation before parsing
export function extractTheoremAddress(text: string): TheoremAddress | undefined {
for (const token of text.split(/\s+/)) {
if (!looksLikeTheoremAddress(token)) continue;
- const parsed = parseTheoremUri(token.trim());
+ const parsed = parseTheoremUri(token.trim().replace(/[).,;:!?'"]+$/, ''));
if (parsed.ok) return parsed.address;
}
return undefined;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** | |
| * Pull the first `theorem://` address out of arbitrary pasted text. | |
| * | |
| * Share sheets wrap an address in a title and a newline (`shareObject` in the | |
| * mobile object drawer does exactly that), so accepting a paste means finding | |
| * the address inside the noise rather than demanding a bare URI. | |
| */ | |
| export function extractTheoremAddress(text: string): TheoremAddress | undefined { | |
| for (const token of text.split(/\s+/)) { | |
| if (!looksLikeTheoremAddress(token)) continue; | |
| const parsed = parseTheoremUri(token.trim()); | |
| if (parsed.ok) return parsed.address; | |
| } | |
| return undefined; | |
| } | |
| /** | |
| * Pull the first `theorem://` address out of arbitrary pasted text. | |
| * | |
| * Share sheets wrap an address in a title and a newline (`shareObject` in the | |
| * mobile object drawer does exactly that), so accepting a paste means finding | |
| * the address inside the noise rather than demanding a bare URI. | |
| */ | |
| export function extractTheoremAddress(text: string): TheoremAddress | undefined { | |
| for (const token of text.split(/\s+/)) { | |
| if (!looksLikeTheoremAddress(token)) continue; | |
| const parsed = parseTheoremUri(token.trim().replace(/[).,;:!?'"]+$/, '')); | |
| if (parsed.ok) return parsed.address; | |
| } | |
| return undefined; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/block-view/src/addressing.ts` around lines 309 - 323, Update
extractTheoremAddress to remove surrounding sentence punctuation from each
whitespace-delimited token before calling looksLikeTheoremAddress and
parseTheoremUri. Preserve valid URI characters while stripping trailing
punctuation such as periods and closing parentheses, so extracted ids never
include prose noise.
TraceGroup's onClick called onOpenEvidence?.(proposal, reference) but proposal was never one of its props: it exists only in the outer WhyTrace scope. `next build` failed with "Cannot find name 'proposal'", which took the whole apps/web build down on main, not just on a branch. The callback signature already declares (proposal, reference), and all three call sites have proposal in scope, so threading it through is the fix the code was written to expect. Out of scope for the addressing work; included because it blocks CI here and blocks the web build on main.
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
Client half of DESIGN-THEOREM-URI. The engine half is a companion PR in the Theorem repo; this one is standalone-mergeable (the client grammar does not depend on the resolver shipping first).
What this does
Every object gets one address that works everywhere a person or an agent can point:
The one client helper —
packages/block-view/src/addressing.ts, exported as@commonplace/block-view/addressing. Mirrors the Rust emitter byte-for-byte (the encode set matchesencodeURIComponentexactly), so an address minted on a phone and one minted in the engine compare equal as strings.parseTheoremUrireturns a discriminated union instead of throwing, because paste handlers run on every paste.It is hand-parsed on purpose: WHATWG
new URL()treatstheorem:as a non-special scheme and puts the tenant inhost, notpathname— which is exactly how the previous mobile router silently dropped the tenant segment.Mobile now re-exports the shared grammar and keeps only the expo-router mapping. Two corrections:
#span={selector}per the grammar (it was a bare#<encoded string>)routeForTheoremUricarries?v=and#spanonto the route instead of discarding them. A shared clipping that opens unspanned is a link that lies.Console — the inspector footer renders the canonical address where the bare id used to, cards carry copy-address, mention chips and action-pack context carry the address, and the Composer and Search field accept pasted addresses. Paste offers rather than auto-inserts (matching the mobile Omnibar's "offer, never auto-insert" precedent), and refusals are shown rather than swallowed. Cross-tenant addresses are refused client-side with the tenant named; the engine still reconciles.
Desktop —
tauri-plugin-deep-linkon Tauri v2 with thetheoremscheme registered, plus anonOpenUrlacceptor inapps/web, which is the surface the Tauri window actually renders (frontendDistpoints there, not atapps/desktop/src).Validation
tsc --noEmit(console, mobile, web)Notes for review
packages/block-viewis consumed asfile:../../packages/block-viewby console, web, and mobile, and pnpm materializes that as a copied snapshot, not a live link — so edits topackages/*reach no consumer untilpnpm installre-runs. This bit during development.workspace:*would link it live but touches the Railway deploy path, so it is left as a deliberate decision rather than folded in here.data-*hooks already in place (data-inspector-address,data-copy-address,data-address-resolve,data-address-refusal,data-composer-paste-offer).🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
theorem://addresses for objects, including optional version and text-span details.Bug Fixes