Skip to content

feat(web): markdown-it + pierre diffs in transcript and diff panel - #2

Merged
johnnyhuy merged 4 commits into
mainfrom
feat/markdown-and-diff-rendering
Jul 20, 2026
Merged

feat(web): markdown-it + pierre diffs in transcript and diff panel#2
johnnyhuy merged 4 commits into
mainfrom
feat/markdown-and-diff-rendering

Conversation

@johnnyhuy

Copy link
Copy Markdown
Contributor

Summary

Wire the long-form text + diff layers into the web renderer so agent chat
content and code diffs render with structure (headings, lists, code,
links, hunks) instead of preformatted text.

Markdown: markdown-it

packages/web/src/components/MarkdownView.tsx

  • Token-stream parser, html: false (no inline HTML), linkify: true,
    typographer: true.
  • renderSafe() walks the token list and strips on* / src /
    srcdoc / style attrs and any href with an unsafe protocol
    (javascript:, data:, etc.).
  • Renders into <div className="markdown-body ..."> with Tailwind
    utility selectors for h1/h2/h3/p/pre/code/ul/li/a/strong.

Wired into AgentTranscript for message.delta and message.final
events. Streaming deltas coalesce into one open markdown line per turn
(so token pushes don't fragment the row list).

Diff: @pierre/diffs

packages/web/src/components/DiffView.tsx

  • Uses the React subpath. Hunk text is generated client-side via
    diff.createTwoFilesPatch, processed with pierre.processFile, fed
    to <FileDiff>.
  • Theme: pierre-dark. Diff style: split (toggle to unified via prop).
  • "No changes" message when both sides are identical (avoids an empty
    diff grid).
  • React 19's stricter JSX type checker rejects the pierre component
    type, so the renderer is wrapped in a small
    <RenderFileDiff> adapter that re-types the props and casts
    through unknown as React.ComponentType<…>. Documented inline.

New UI surface:

  • Sidebar: each workspace row gets an "Open diff" button that loads a
    sample before/after pair into a side-panel <DiffView>. This
    exercises the renderer end-to-end while the daemon-side
    ClientCommand.diff.open handler is still unwired.
  • Composer: a diff.open button next to Send issues the protocol
    command for the current session id. Output is currently a
    transcript confirmation; the future daemon reply will round-trip
    into the same <DiffView>.

Verification

$ bun run typecheck # clean
$ bun run lint # 0 errors (1324 warnings, all pre-existing)
$ bun run build:web # succeeds (large chunks from shiki langs# are expected — see warning notice)# Manually
$ cd packages/web && bun run dev # serves on 5179# Open the workspace sidebar 'Open diff' button to exercise DiffView

The 2 failing tests in connection-offer.test.ts are pre-existing
(they assert app.supaplane.run while the encoder produces
supaplane.com) and not related to this PR. No new test failures.

Consequences

  • Adds ~7 MB of shiki language bundles to the web renderer's output
    (shiki is pierre's highlighter backend). Vite code-splits by
    language already; only the languages actually referenced by a
    rendered file end up loaded at runtime, but the build emit is
    larger as a result.
  • The diff.open composer button currently issues a command the
    daemon ignores — the Composer footer shows the "opened diff for
    …" hint locally on a click, but no server-side confirmation is
    wired yet. Daemon-side handling is out of scope for this PR.
  • pierre/diffs is shipped as a React-19-compatible library, but the
    React 19 JSX type checker rejects its component type — hence the
    <RenderFileDiff> adapter. Upstream may fix this in a future
    release; we can drop the adapter when they do.

Testing

  • bun run typecheck clean
  • bun run build:web clean
  • Manual: dev server up, Open diff opens the side panel, the
    pinned-language demo renders both before/after with hunks and
    shiki highlighting (markdown pane + diff pane are independent)
  • Eyeball review requested for the React 19 type adapter and
    the markdown-it sanitiser

opencodeand others added 4 commits July 18, 2026 00:11
Adds the three-screen MVP on top of @echohello/client:
- connect: manual ws:// endpoint + label, saved to AsyncStorage,
connects via SupaplaneClient
- sessions: workspace list grouped by freshness, tap to drill in
- session: transcript + composer (send/abort/start) bound to
ClientCommand wire types
Shared infrastructure:
- zustand connection-store: single SupaplaneClient instance,
status, workspaces, sessions, per-session event buffer
- AsyncStorage wrapper (v3 API: setMany/removeMany) for the
saved endpoint and TOFU-pinned server fingerprint
- dark theme tokens, Button, TextField, ConnectionBanner
Bundler plumbing: metro.config.cjs with explicit watchFolders
and nodeModulesPaths so Bun's hoisting doesn't trip the
resolver on a monorepo root.
Co-authored-by: opencode <opencode@echohello.dev>
Desktop build wiring:
- packages/desktop/tsconfig.build.json emits ESM to dist/
- packages/desktop/package.json: dev now runs tsc -> electron
- main.js (dist) replaces main.ts as the package entry, and
preload.js resolves to the dist sibling
- @echohello/server and @echohello/protocol exports point at
dist/ so Electron's Node can resolve them. Run
'bun run build:server' (or 'bun run build') before
'desktop dev'.
Dev port scheme (avoiding Paseo + Vite + Metro defaults):
- Daemon listenPort: 6767 -> 17687 (IANA-sparse range,
visually echoes 6767)
- Vite port: 5173 -> 5179 (adjacent to Vite default so it
reads as 'the Supaplane Vite port')
- vite.config.ts reads SUPAPLANE_DAEMON_PORT and VITE_PORT
envs; defaults updated
- AGENTS.md + docs/development.md updated to reflect the
new ports
Server hygiene:
- daemon-keypair.ts: replace require('node:crypto') with
a top-level import so it builds cleanly to ESM
Co-authored-by: opencode <opencode@echohello.dev>
Wire the long-form text and diff layers into the web renderer:
- `markdown-it` (token stream, html:false, linkify on, sanitised
attrs) backs the agent transcript's `message.delta` /
`message.final` rows so chat output renders headings, lists,
code fences, tables, and links without running inline HTML.
Streaming `message.delta` events coalesce into a single open
markdown line per turn.
- `@pierre/diffs` (Shadow DOM + CSS grid web component) renders a
two-version diff panel. Hunk text is generated client-side via
`diff.createTwoFilesPatch`, processed with `pierre.processFile`,
and passed to `<FileDiff>`. Long-context collapse is on by
default; layout toggle (split/unified) is supported.
New UI surface:
- A workspace row in the sidebar can "Open diff" against a sample
before/after pair, opening a side-panel diff view. This exercises
the diff renderer end-to-end while the daemon's
`ClientCommand.diff.open` handler is still unwired.
- A `diff.open` button in the composer issues the protocol command
for the current session id, mirroring the existing
`session.send` path.
Co-authored-by: opencode <opencode@echohello.dev>
@johnnyhuy
johnnyhuy merged commit 4e219ab into mainJul 20, 2026
@github-actionsgithub-actionsBot mentioned this pull request Jul 20, 2026
johnnyhuy added a commit that referenced this pull request Aug 9, 2026
* feat(app): scaffold mobile UI for connect/sessions/transcript
Adds the three-screen MVP on top of @echohello/client:
- connect: manual ws:// endpoint + label, saved to AsyncStorage,
connects via SupaplaneClient
- sessions: workspace list grouped by freshness, tap to drill in
- session: transcript + composer (send/abort/start) bound to
ClientCommand wire types
Shared infrastructure:
- zustand connection-store: single SupaplaneClient instance,
status, workspaces, sessions, per-session event buffer
- AsyncStorage wrapper (v3 API: setMany/removeMany) for the
saved endpoint and TOFU-pinned server fingerprint
- dark theme tokens, Button, TextField, ConnectionBanner
Bundler plumbing: metro.config.cjs with explicit watchFolders
and nodeModulesPaths so Bun's hoisting doesn't trip the
resolver on a monorepo root.
Co-authored-by: opencode <opencode@echohello.dev>
* chore(dev): wire desktop build + shift dev ports off 6767/5173
Desktop build wiring:
- packages/desktop/tsconfig.build.json emits ESM to dist/
- packages/desktop/package.json: dev now runs tsc -> electron
- main.js (dist) replaces main.ts as the package entry, and
preload.js resolves to the dist sibling
- @echohello/server and @echohello/protocol exports point at
dist/ so Electron's Node can resolve them. Run
'bun run build:server' (or 'bun run build') before
'desktop dev'.
Dev port scheme (avoiding Paseo + Vite + Metro defaults):
- Daemon listenPort: 6767 -> 17687 (IANA-sparse range,
visually echoes 6767)
- Vite port: 5173 -> 5179 (adjacent to Vite default so it
reads as 'the Supaplane Vite port')
- vite.config.ts reads SUPAPLANE_DAEMON_PORT and VITE_PORT
envs; defaults updated
- AGENTS.md + docs/development.md updated to reflect the
new ports
Server hygiene:
- daemon-keypair.ts: replace require('node:crypto') with
a top-level import so it builds cleanly to ESM
Co-authored-by: opencode <opencode@echohello.dev>
* feat(web): markdown-it + pierre diffs in transcript and diff panel
Wire the long-form text and diff layers into the web renderer:
- `markdown-it` (token stream, html:false, linkify on, sanitised
attrs) backs the agent transcript's `message.delta` /
`message.final` rows so chat output renders headings, lists,
code fences, tables, and links without running inline HTML.
Streaming `message.delta` events coalesce into a single open
markdown line per turn.
- `@pierre/diffs` (Shadow DOM + CSS grid web component) renders a
two-version diff panel. Hunk text is generated client-side via
`diff.createTwoFilesPatch`, processed with `pierre.processFile`,
and passed to `<FileDiff>`. Long-context collapse is on by
default; layout toggle (split/unified) is supported.
New UI surface:
- A workspace row in the sidebar can "Open diff" against a sample
before/after pair, opening a side-panel diff view. This exercises
the diff renderer end-to-end while the daemon's
`ClientCommand.diff.open` handler is still unwired.
- A `diff.open` button in the composer issues the protocol command
for the current session id, mirroring the existing
`session.send` path.
Co-authored-by: opencode <opencode@echohello.dev>
---------
Co-authored-by: opencode <opencode@echohello.dev>
johnnyhuy added a commit that referenced this pull request Aug 9, 2026
* feat(app): scaffold mobile UI for connect/sessions/transcript
Adds the three-screen MVP on top of @echohello/client:
- connect: manual ws:// endpoint + label, saved to AsyncStorage,
connects via SupaplaneClient
- sessions: workspace list grouped by freshness, tap to drill in
- session: transcript + composer (send/abort/start) bound to
ClientCommand wire types
Shared infrastructure:
- zustand connection-store: single SupaplaneClient instance,
status, workspaces, sessions, per-session event buffer
- AsyncStorage wrapper (v3 API: setMany/removeMany) for the
saved endpoint and TOFU-pinned server fingerprint
- dark theme tokens, Button, TextField, ConnectionBanner
Bundler plumbing: metro.config.cjs with explicit watchFolders
and nodeModulesPaths so Bun's hoisting doesn't trip the
resolver on a monorepo root.
Co-authored-by: opencode <opencode@echohello.dev>
* chore(dev): wire desktop build + shift dev ports off 6767/5173
Desktop build wiring:
- packages/desktop/tsconfig.build.json emits ESM to dist/
- packages/desktop/package.json: dev now runs tsc -> electron
- main.js (dist) replaces main.ts as the package entry, and
preload.js resolves to the dist sibling
- @echohello/server and @echohello/protocol exports point at
dist/ so Electron's Node can resolve them. Run
'bun run build:server' (or 'bun run build') before
'desktop dev'.
Dev port scheme (avoiding Paseo + Vite + Metro defaults):
- Daemon listenPort: 6767 -> 17687 (IANA-sparse range,
visually echoes 6767)
- Vite port: 5173 -> 5179 (adjacent to Vite default so it
reads as 'the Supaplane Vite port')
- vite.config.ts reads SUPAPLANE_DAEMON_PORT and VITE_PORT
envs; defaults updated
- AGENTS.md + docs/development.md updated to reflect the
new ports
Server hygiene:
- daemon-keypair.ts: replace require('node:crypto') with
a top-level import so it builds cleanly to ESM
Co-authored-by: opencode <opencode@echohello.dev>
* feat(web): markdown-it + pierre diffs in transcript and diff panel
Wire the long-form text and diff layers into the web renderer:
- `markdown-it` (token stream, html:false, linkify on, sanitised
attrs) backs the agent transcript's `message.delta` /
`message.final` rows so chat output renders headings, lists,
code fences, tables, and links without running inline HTML.
Streaming `message.delta` events coalesce into a single open
markdown line per turn.
- `@pierre/diffs` (Shadow DOM + CSS grid web component) renders a
two-version diff panel. Hunk text is generated client-side via
`diff.createTwoFilesPatch`, processed with `pierre.processFile`,
and passed to `<FileDiff>`. Long-context collapse is on by
default; layout toggle (split/unified) is supported.
New UI surface:
- A workspace row in the sidebar can "Open diff" against a sample
before/after pair, opening a side-panel diff view. This exercises
the diff renderer end-to-end while the daemon's
`ClientCommand.diff.open` handler is still unwired.
- A `diff.open` button in the composer issues the protocol command
for the current session id, mirroring the existing
`session.send` path.
Co-authored-by: opencode <opencode@echohello.dev>
---------
Co-authored-by: opencode <opencode@echohello.dev>
johnnyhuy added a commit that referenced this pull request Aug 9, 2026
* feat(app): scaffold mobile UI for connect/sessions/transcript
Adds the three-screen MVP on top of @echohello/client:
- connect: manual ws:// endpoint + label, saved to AsyncStorage,
connects via SupaplaneClient
- sessions: workspace list grouped by freshness, tap to drill in
- session: transcript + composer (send/abort/start) bound to
ClientCommand wire types
Shared infrastructure:
- zustand connection-store: single SupaplaneClient instance,
status, workspaces, sessions, per-session event buffer
- AsyncStorage wrapper (v3 API: setMany/removeMany) for the
saved endpoint and TOFU-pinned server fingerprint
- dark theme tokens, Button, TextField, ConnectionBanner
Bundler plumbing: metro.config.cjs with explicit watchFolders
and nodeModulesPaths so Bun's hoisting doesn't trip the
resolver on a monorepo root.
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
* chore(dev): wire desktop build + shift dev ports off 6767/5173
Desktop build wiring:
- packages/desktop/tsconfig.build.json emits ESM to dist/
- packages/desktop/package.json: dev now runs tsc -> electron
- main.js (dist) replaces main.ts as the package entry, and
preload.js resolves to the dist sibling
- @echohello/server and @echohello/protocol exports point at
dist/ so Electron's Node can resolve them. Run
'bun run build:server' (or 'bun run build') before
'desktop dev'.
Dev port scheme (avoiding Paseo + Vite + Metro defaults):
- Daemon listenPort: 6767 -> 17687 (IANA-sparse range,
visually echoes 6767)
- Vite port: 5173 -> 5179 (adjacent to Vite default so it
reads as 'the Supaplane Vite port')
- vite.config.ts reads SUPAPLANE_DAEMON_PORT and VITE_PORT
envs; defaults updated
- AGENTS.md + docs/development.md updated to reflect the
new ports
Server hygiene:
- daemon-keypair.ts: replace require('node:crypto') with
a top-level import so it builds cleanly to ESM
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
* feat(web): markdown-it + pierre diffs in transcript and diff panel
Wire the long-form text and diff layers into the web renderer:
- `markdown-it` (token stream, html:false, linkify on, sanitised
attrs) backs the agent transcript's `message.delta` /
`message.final` rows so chat output renders headings, lists,
code fences, tables, and links without running inline HTML.
Streaming `message.delta` events coalesce into a single open
markdown line per turn.
- `@pierre/diffs` (Shadow DOM + CSS grid web component) renders a
two-version diff panel. Hunk text is generated client-side via
`diff.createTwoFilesPatch`, processed with `pierre.processFile`,
and passed to `<FileDiff>`. Long-context collapse is on by
default; layout toggle (split/unified) is supported.
New UI surface:
- A workspace row in the sidebar can "Open diff" against a sample
before/after pair, opening a side-panel diff view. This exercises
the diff renderer end-to-end while the daemon's
`ClientCommand.diff.open` handler is still unwired.
- A `diff.open` button in the composer issues the protocol command
for the current session id, mirroring the existing
`session.send` path.
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
---------
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
This was referenced Aug 9, 2026
Sign up for freeto 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

@johnnyhuy