Skip to content

iOS Workbench (read-only), workbench plugin DX, and dropping Expo#272

Merged
haowei2000 merged 3 commits into
developfrom
claude/ios-workbench-plugin-feasibility-0aa4f8
Jul 20, 2026
Merged

iOS Workbench (read-only), workbench plugin DX, and dropping Expo#272
haowei2000 merged 3 commits into
developfrom
claude/ios-workbench-plugin-feasibility-0aa4f8

Conversation

@haowei2000

Copy link
Copy Markdown
Collaborator

Three related changes, one per commit.

1 — Workbench plugin developer experience

Writing a renderer plugin worked; iterating on one didn't. Protocol, manifest and sandbox are untouched — this is pure DX.

  • Hot reload. "Watch file" holds the picked .html handle and polls its mtime, so saving in your editor reloads the plugin in place. File System Access API, so the button is Chromium-only; drag-and-drop stays universal.
  • A protocol inspector. An opaque-origin sandbox swallows console output and uncaught errors, so a broken renderer appeared as a blank iframe with nothing in the host console. Session-loaded (⏱) plugins now get a Dev panel listing every cheers:* message both ways, plus a new cheers:log the SDK uses to forward uncaught errors and unhandled rejections. Installed plugins stay silent — a development affordance, not telemetry. Older hosts ignore it (protocol-1 growth rule).
  • A real build template. The guide claimed "bundled framework code" was fine but shipped no way to produce the required single file. Adds a React + TypeScript template on vite-plugin-singlefile — verified to emit one 199 KB index.html with the embedded manifest intact, far under the 2 MiB cap.

Also fixes a documentation bug: the Chinese design original said cheers:render is re-sent on external edits. It isn't — there are exactly two triggers (cheers:ready, and a conflicted save), as the English spec already said. That contract is now prominent in both, because "my board didn't refresh" is correct behaviour and authors must design for it.

2 — Dropping Expo (ADR change)

Supersedes decision 2 of the 2026-07-06 mobile strategy. In the 13 days after it was accepted the Expo scaffold was never started — still zero Expo/RN code in the repo — while apps/ios took three commits, and the push design was revised to direct APNs precisely because the shipping client is the native SwiftUI app.

iOS = native SwiftUI, long-term. Android = frozen (fixes only; PWA covers it). apps/mobile/ will not be created. Freezing Android removes the 2x-maintenance argument without a rewrite. Costs are recorded explicitly so they aren't relitigated: no native Android velocity, no React-team leverage on mobile. The original analysis is retained unedited.

3 — iOS Workbench, read-only

The Workbench menu entry resolved to a placeholder sheet. It now browses the channel's file workspace: the tree, plus exact file bytes — the "Raw" half of the web Workbench. Native lenses are the follow-up; editing (fs.write + if_version) after that. No new transport or server work: fs.ls/fs.read ride the resource_req RPC the ViewBoards already use.

Two bugs the on-device pass caught that a code review would not have:

  • fs.ls returns a flat, recursive list of full paths and emits no is_dir rows at all. The first cut rendered draft/paper.md as a root-level paper.md and lost every folder. The hierarchy is now derived client-side, mirroring the web buildTree. FsEntry deliberately no longer exposes a basename helper — reading only the last segment is what caused this.
  • Long lines were silently clipped with no way to scroll to them (maxWidth: .infinity inside a horizontal ScrollView pins content to the viewport). Invisibly truncated JSON is the worst kind of wrong.

Refresh is a header button, not pull-to-refresh: inside a presentationDetents sheet the downward pan is claimed by the sheet's own resize gesture, so .refreshable never fires — verified, not assumed.

Untrusted-content posture follows ViewBoards: inert Text, no markup, no tappable links.

Verification

  • iOS: BUILD SUCCEEDED, zero warnings. Driven end-to-end in the simulator against the local kind gateway — login → research-lab → Workbench → correct folder tree → drill down → open papers.json → horizontal scroll reveals full lines → back out. Refresh button verified by writing a file while the sheet was open and watching the child count go 1 → 2. Probe files cleaned up afterwards.
  • Frontend: tsc clean (only the pre-existing PdfViewer baseline), 57 workbench tests pass including 7 new ones for the inspector's summarizer (cyclic payloads included — the panel exists to diagnose broken plugins, so it must not break on one).
  • Template: npm install && npm run build actually run; output inspected and its manifest parsed.
  • Re-verified after rebasing onto develop.

🤖 Generated with Claude Code

haowei2000 and others added 3 commits July 19, 2026 19:50
…emplate for plugins

Writing a renderer plugin worked, but iterating on one did not. Three gaps, all
pure developer experience — the protocol, manifest and sandbox are untouched.

- Hot reload: "Watch file" keeps the picked .html handle and polls its mtime, so
  saving in your editor reloads the plugin in place instead of re-dragging it.
  File System Access API, so the button only appears on Chromium; drag-and-drop
  stays the universal path.

- Debugging: an opaque-origin sandbox swallows console output and uncaught
  errors, so a broken renderer showed up as a blank iframe with nothing in the
  host console. Session-loaded (temporary) plugins now get a "Dev" protocol
  inspector listing every cheers:* message in both directions, plus a new
  plugin -> host cheers:log that the SDK uses to forward uncaught errors and
  unhandled rejections. Installed plugins stay silent — this is a development
  affordance, not telemetry. Unknown to older hosts, which ignore it.

- Framework builds: the guide claimed "bundled framework code" was fine but gave
  no way to produce the required single file. Adds a React + TypeScript template
  built by vite-plugin-singlefile; verified to emit one 199 KB index.html with
  the embedded manifest intact, well under the 2 MiB cap.

Also fixes a documentation bug: the Chinese design original claimed cheers:render
is re-sent when the file changes externally. It is not — the code has exactly two
triggers (cheers:ready, and a conflicted save), which the English spec already
stated. That contract is now prominent in both, because "my board didn't refresh"
is correct behaviour and plugin authors have to design around it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eezes

Supersedes decision 2 of the 2026-07-06 mobile strategy (consolidate iOS and
Android onto one Expo codebase, then delete apps/ios and apps/android).

The decision did not survive contact with what we actually did. In the 13 days
after it was accepted the Expo scaffold was never started — there is still zero
Expo/React Native code in the repo — while apps/ios took three commits. The push
design was already revised on 2026-07-18 to direct APNs + relay precisely because
the shipping client is the native SwiftUI app. A document that contradicts the
work for two weeks is not a plan.

New direction:
- iOS = native SwiftUI (apps/ios), a long-term client invested in as such.
- Android = frozen: fixes only, no new features; Android users get the PWA.
- apps/mobile/ (Expo) will not be created.

The original cost argument was 2x maintenance for Swift *and* Kotlin. Freezing
Android removes that multiplier without a rewrite: one native client plus the PWA
is a smaller surface than either two native clients or a from-scratch port.

Accepted costs, recorded so they are not relitigated later: no native Android
feature velocity, and no React-team leverage on mobile.

The 2026-07-06 analysis is retained unedited — decision 1 (landing the three
branches) was carried out and is still accurate history. MOBILE_APP_DESIGN keeps
its product/UX and push sections, which remain valid; its Expo-specific mechanics
are banner-marked as superseded and will be replaced section by section rather
than deleted in one sweep.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Workbench menu entry resolved to a placeholder sheet — an icon, a title and
a blurb. It now browses the channel's file workspace for real.

The web Workbench renders a file through a bound renderer (a built-in lens or a
sandboxed HTML plugin) and falls back to "Raw". iOS has no sandbox to run plugin
bundles in, so this ships the Raw half: the tree, plus exact file bytes. Native
lenses are the follow-up; editing (fs.write + the if_version optimistic lock)
comes after that.

No new transport or server work — fs.ls / fs.read ride the resource_req RPC that
ChatSocket already implements for the ViewBoards.

Two things the on-device pass caught that the code review would not have:

- fs.ls returns a FLAT, recursive list of full paths and emits no is_dir rows at
  all, so the first cut rendered draft/paper.md as a root-level "paper.md" and
  lost every folder. The hierarchy is now derived client-side from the "/" in
  each path, mirroring the web buildTree, so one listing feeds the whole browser
  and drilling down costs no round trip. FsEntry deliberately no longer exposes a
  basename helper — reading only the last segment is what caused this.

- Long lines were silently clipped with no way to scroll to them: a
  maxWidth: .infinity inside a horizontal ScrollView pins content to the viewport.
  Truncating JSON invisibly is the worst kind of wrong, so the Text now takes its
  natural width.

Refresh is a header button, not pull-to-refresh: inside a presentationDetents
sheet a downward pan is claimed by the sheet's own resize gesture, so .refreshable
never fires — verified, not assumed.

Untrusted-content posture follows ViewBoards: plain inert Text, no markup
rendering and no tappable links, so bot-authored content cannot act on a tap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@haowei2000
haowei2000 merged commit 9385270 into develop Jul 20, 2026
6 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.

1 participant