diff --git a/.gitignore b/.gitignore index 6d6bb279b7..a85ff1fd8c 100644 --- a/.gitignore +++ b/.gitignore @@ -85,6 +85,8 @@ valknut-report.json/ packages/server/src/server/fixtures/dictation/dictation-debug-largest.wav packages/server/src/server/fixtures/dictation/dictation-debug-largest.transcript.txt packages/protocol/src/generated/validation/*.aot.ts +packages/protocol/src/generated/validation/ws-outbound-schema-metadata.ts +packages/protocol/codegen/generated/ /artifacts packages/desktop/.cache/ diff --git a/CHANGELOG.md b/CHANGELOG.md index cbd1d0551d..80dc5d17e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,139 @@ # Changelog +## 0.12.0-beta.1 - 2026-10-16 + +Background workspaces, a new Markdown renderer with selection across paragraphs on phones, a reworked phone layout, and close to 100 fixes. + +**Before upgrading:** `paseo run --background` is deprecated in favor of `--no-wait`, and `paseo permit allow` and `deny` now exit non-zero when the daemon doesn't confirm within 15 seconds + +### Added + +- Added background workspaces, hidden from the sidebar unless Show background is on ([#5039](https://github.com/getpaseo/paseo/pull/5039), [#7141](https://github.com/getpaseo/paseo/pull/7141) by [@gpambrozio](https://github.com/gpambrozio), [@marvin-ambrozio](https://github.com/marvin-ambrozio)) +- Added a Send feedback button to the diff pane, so review comments reach an agent without opening its chat ([#6455](https://github.com/getpaseo/paseo/pull/6455)) +- Added a fullscreen composer on phones ([#6531](https://github.com/getpaseo/paseo/pull/6531), [#6902](https://github.com/getpaseo/paseo/pull/6902), [#6945](https://github.com/getpaseo/paseo/pull/6945), [#6987](https://github.com/getpaseo/paseo/pull/6987), [#7130](https://github.com/getpaseo/paseo/pull/7130), [#7170](https://github.com/getpaseo/paseo/pull/7170)) +- Added the sender's name to messages one agent sends another ([#6373](https://github.com/getpaseo/paseo/pull/6373)) +- Added Claude Mythos 5.1 when the installed Claude Code advertises it ([#6403](https://github.com/getpaseo/paseo/pull/6403)) +- Added option previews to Claude question cards ([#6171](https://github.com/getpaseo/paseo/pull/6171) by [@gabrielgiordan](https://github.com/gabrielgiordan)) +- Added `paseo usage` to show account quotas, balances, and reset times from the CLI ([#6474](https://github.com/getpaseo/paseo/pull/6474)) + +### Improved + +- Changed text selection in replies on iOS and Android to cross paragraphs, headings, and lists ([#6973](https://github.com/getpaseo/paseo/pull/6973)) +- Added a copy button to code blocks on iOS and Android ([#6973](https://github.com/getpaseo/paseo/pull/6973)) +- Changed wide Markdown tables on iOS and Android to scroll sideways ([#6973](https://github.com/getpaseo/paseo/pull/6973)) +- Changed phone layouts to a floating header ([#6431](https://github.com/getpaseo/paseo/pull/6431)) +- Moved New agent and New terminal on phones into the tab switcher ([#6431](https://github.com/getpaseo/paseo/pull/6431)) +- Moved workspace scripts on phones into ⋯ → Scripts, which shows what is running ([#7122](https://github.com/getpaseo/paseo/pull/7122)) +- Changed phones to open files and subagents in a closable sheet, with Open as tab for subagents ([#6827](https://github.com/getpaseo/paseo/pull/6827), [#6885](https://github.com/getpaseo/paseo/pull/6885), [#6958](https://github.com/getpaseo/paseo/pull/6958)) +- Changed diff comments on phones to open in a sheet instead of an editor inside the code ([#6455](https://github.com/getpaseo/paseo/pull/6455)) +- Changed Settings on phones to large tiles grouped into App and Host ([#7129](https://github.com/getpaseo/paseo/pull/7129)) +- Reduced rendering work in long chats while a reply streams ([#7052](https://github.com/getpaseo/paseo/pull/7052) by [@colonelpanic8](https://github.com/colonelpanic8)) +- Made reopening the sidebar about 45% faster ([#7157](https://github.com/getpaseo/paseo/pull/7157)) +- Changed Gitea and Forgejo fork PR checkouts to include the fork owner in the branch name, such as `bob/patch-1` ([#6829](https://github.com/getpaseo/paseo/pull/6829) by [@alexanderadam](https://github.com/alexanderadam)) + +### Plugins + +- Fixed plugins that import Paseo protocol types failing to install with "Could not resolve type dependency" ([#7023](https://github.com/getpaseo/paseo/pull/7023)) +- Fixed `providers.waitForReady` throwing in plugins on the mobile app ([#6760](https://github.com/getpaseo/paseo/pull/6760) by [@GeorgeR](https://github.com/GeorgeR)) +- Fixed the chat outline not jumping to prompts rendered by a timeline plugin ([#6690](https://github.com/getpaseo/paseo/pull/6690) by [@lukeRRD](https://github.com/lukeRRD)) +- Fixed Escape stopping the running agent instead of first closing an open plugin preview ([#7110](https://github.com/getpaseo/paseo/pull/7110) by [@RRD-dev](https://github.com/RRD-dev), [@luoph](https://github.com/luoph)) + +### Fixed + +- Fixed iOS replies with list items that render blank, and text that keeps a stale width after a resize or rotation ([#6973](https://github.com/getpaseo/paseo/pull/6973)) +- Fixed plugin hosts, the CLI, and the desktop app growing to about 1 GB of memory after running for 30 to 75 minutes ([#6847](https://github.com/getpaseo/paseo/pull/6847) by [@babo-jeonguk](https://github.com/babo-jeonguk)) +- Fixed the daemon running out of memory during long OpenCode v2 reply or reasoning streams ([#6262](https://github.com/getpaseo/paseo/pull/6262)) +- Fixed archived ACP agents starting a provider process that kept running each time their timeline loaded ([#6766](https://github.com/getpaseo/paseo/pull/6766) by [@apple-ouyang](https://github.com/apple-ouyang)) +- Fixed oversized attachments disconnecting the host in a loop instead of failing with a size error ([#6285](https://github.com/getpaseo/paseo/pull/6285)) +- Fixed Download in Files failing on hosts connected over relay, SSH, or a socket ([#6595](https://github.com/getpaseo/paseo/pull/6595) by [@odoo-mvds](https://github.com/odoo-mvds), [@YASICJUNWOO](https://github.com/YASICJUNWOO), [@JrDw0](https://github.com/JrDw0)) +- Fixed Changes showing no diff lines with custom Git diff settings, such as `color.diff=always` or an external diff command ([#6536](https://github.com/getpaseo/paseo/pull/6536), [#6562](https://github.com/getpaseo/paseo/pull/6562) by [@Mar-garet](https://github.com/Mar-garet)) +- Fixed an incomplete font name in Appearance breaking the app's styling ([#6695](https://github.com/getpaseo/paseo/pull/6695)) +- Fixed the Linux desktop window never appearing when the hidden window renders no frames ([#6726](https://github.com/getpaseo/paseo/pull/6726) by [@EhEo](https://github.com/EhEo)) +- Fixed opening another agent or workspace stalling while a different agent on the same host is streaming ([#7074](https://github.com/getpaseo/paseo/pull/7074) by [@colonelpanic8](https://github.com/colonelpanic8)) +- Fixed agent creation showing an error and a retry draft when the provider rejected the first prompt ([#6379](https://github.com/getpaseo/paseo/pull/6379)) +- Fixed parent agents missing the finish notification when a child finished its first turn very quickly ([#6008](https://github.com/getpaseo/paseo/pull/6008) by [@ahmet-izgi](https://github.com/ahmet-izgi)) +- Fixed parent agents being told a child finished when its task was stopped or reloaded ([#6660](https://github.com/getpaseo/paseo/pull/6660) by [@Mar-garet](https://github.com/Mar-garet)) +- Fixed Codex chats containing U+2028 or U+2029 failing to reopen on Node 24 ([#5999](https://github.com/getpaseo/paseo/pull/5999) by [@grahameger](https://github.com/grahameger), [@jtsang4](https://github.com/jtsang4), [@kotusenka](https://github.com/kotusenka)) +- Fixed Add Project directory search hanging the daemon on macOS when it entered `~/Library` ([#5789](https://github.com/getpaseo/paseo/pull/5789) by [@lardissone](https://github.com/lardissone)) +- Fixed New workspace failing with "Invalid mode" when the saved mode isn't offered by the provider ([#5847](https://github.com/getpaseo/paseo/pull/5847) by [@mcowger](https://github.com/mcowger)) +- Fixed answering multi-select OpenCode v2 questions failing with "Invalid answer" ([#6149](https://github.com/getpaseo/paseo/pull/6149) by [@gammons](https://github.com/gammons)) +- Fixed browser screenshots failing the turn on OpenCode v2 agents ([#6330](https://github.com/getpaseo/paseo/pull/6330)) +- Fixed OpenCode v2 agents receiving "HTTP 500" instead of the Paseo tool error ([#6519](https://github.com/getpaseo/paseo/pull/6519)) +- Fixed rewinding a Claude conversation failing with "Session not found" when the provider sets `CLAUDE_CONFIG_DIR` ([#6325](https://github.com/getpaseo/paseo/pull/6325)) +- Fixed imported Claude Code sessions taking their first prompt as the name instead of their `/rename` title ([#6930](https://github.com/getpaseo/paseo/pull/6930) by [@platanus-kr](https://github.com/platanus-kr)) +- Fixed a wrongly shaped answer to a Claude question reporting success and discarding the question ([#6936](https://github.com/getpaseo/paseo/pull/6936) by [@thomasvan](https://github.com/thomasvan)) +- Fixed Cursor agents showing no plan usage in their context details ([#6353](https://github.com/getpaseo/paseo/pull/6353) by [@gengjiawen](https://github.com/gengjiawen)) +- Fixed the Claude context meter counting only output tokens when a proxy reports input usage at the end of the response ([#6703](https://github.com/getpaseo/paseo/pull/6703) by [@kywoo26](https://github.com/kywoo26)) +- Fixed the Claude context meter showing a 1M maximum after switching back to a 200k model ([#6814](https://github.com/getpaseo/paseo/pull/6814)) +- Fixed the context meter staying empty during the first turn on custom Claude `[1m]` models ([#6767](https://github.com/getpaseo/paseo/pull/6767)) +- Fixed a schedule never running again after one of its runs could not be recorded ([#6334](https://github.com/getpaseo/paseo/pull/6334)) +- Fixed schedules sharing a cron minute running a slot twice and skipping the next occurrence ([#6532](https://github.com/getpaseo/paseo/pull/6532) by [@keithpk](https://github.com/keithpk)) +- Fixed Committed showing no changes for worktrees created from an existing branch ([#6462](https://github.com/getpaseo/paseo/pull/6462) by [@cr-2takumax](https://github.com/cr-2takumax)) +- Fixed Discard and commit diffs in Changes for workspaces opened at a repository subdirectory ([#6777](https://github.com/getpaseo/paseo/pull/6777), [#6801](https://github.com/getpaseo/paseo/pull/6801), [#6882](https://github.com/getpaseo/paseo/pull/6882) by [@Mar-garet](https://github.com/Mar-garet)) +- Fixed renamed and edited files showing every line as added in Changes ([#6559](https://github.com/getpaseo/paseo/pull/6559)) +- Fixed renamed files opened from a commit showing as new files ([#6895](https://github.com/getpaseo/paseo/pull/6895)) +- Fixed Changes showing no diff for filenames containing quotes, backslashes, or tabs ([#6583](https://github.com/getpaseo/paseo/pull/6583) by [@Mar-garet](https://github.com/Mar-garet)) +- Fixed Changes dropping removed lines that start with `--`, such as SQL comments ([#6569](https://github.com/getpaseo/paseo/pull/6569) by [@Mar-garet](https://github.com/Mar-garet)) +- Fixed the Diff tab staying on a spinner after the first edit in a clean workspace ([#6745](https://github.com/getpaseo/paseo/pull/6745)) +- Fixed worktree creation failing after running `git init` in a project added before Git ([#6785](https://github.com/getpaseo/paseo/pull/6785)) +- Fixed new worktrees not opening the terminals configured in `paseo.json` after setup ([#6999](https://github.com/getpaseo/paseo/pull/6999) by [@davidyang](https://github.com/davidyang)) +- Fixed a service newly added to `paseo.json` failing to start until the daemon restarted ([#6542](https://github.com/getpaseo/paseo/pull/6542)) +- Fixed workspace service URLs returning 502 when the service listens only on IPv6 localhost ([#6753](https://github.com/getpaseo/paseo/pull/6753) by [@marcelocastrobarbosa](https://github.com/marcelocastrobarbosa)) +- Fixed terminals that could not be closed when their command was missing or exited immediately ([#6792](https://github.com/getpaseo/paseo/pull/6792) by [@KiraPC](https://github.com/KiraPC)) +- Fixed CJK terminal text gaining spaces between characters after returning to a workspace ([#6452](https://github.com/getpaseo/paseo/pull/6452) by [@jasonhnd](https://github.com/jasonhnd), [@liujin0506](https://github.com/liujin0506), [@seob717](https://github.com/seob717)) +- Fixed the self-hosted web app failing to connect through a reverse proxy on port 80 or 443 ([#6638](https://github.com/getpaseo/paseo/pull/6638) by [@kiawin](https://github.com/kiawin)) +- Fixed assistant file links with spaces or non-ASCII characters failing with ENOENT ([#6520](https://github.com/getpaseo/paseo/pull/6520) by [@apple-ouyang](https://github.com/apple-ouyang), [@sunshinehomeyu-blip](https://github.com/sunshinehomeyu-blip)) +- Fixed local images in agent replies failing to load when their path contains spaces or non-ASCII characters ([#6877](https://github.com/getpaseo/paseo/pull/6877)) +- Fixed Markdown file previews not showing images referenced by relative paths ([#6750](https://github.com/getpaseo/paseo/pull/6750) by [@apple-ouyang](https://github.com/apple-ouyang)) +- Fixed iOS sidebar rows staying highlighted and swallowing the next tap after a brief hold ([#7159](https://github.com/getpaseo/paseo/pull/7159) by [@kassamkhoja](https://github.com/kassamkhoja)) +- Fixed Android chats scrolling when you long-press to select text ([#7131](https://github.com/getpaseo/paseo/pull/7131) by [@RRD-dev](https://github.com/RRD-dev), [@luoph](https://github.com/luoph)) +- Fixed sidebar rows, footer buttons, and Settings items showing no feedback when tapped on touch devices ([#7150](https://github.com/getpaseo/paseo/pull/7150)) +- Fixed Page Up and Page Down not scrolling the chat after clicking a message ([#7033](https://github.com/getpaseo/paseo/pull/7033) by [@panta82](https://github.com/panta82)) +- Fixed Add Project keyboard selection moving to directories scrolled out of view ([#7081](https://github.com/getpaseo/paseo/pull/7081) by [@superheher](https://github.com/superheher)) +- Fixed Enter replacing an email address in the composer with a file mention ([#6471](https://github.com/getpaseo/paseo/pull/6471) by [@jasonhnd](https://github.com/jasonhnd)) +- Fixed letter shortcuts not running with non-Latin keyboard layouts such as Russian ([#6755](https://github.com/getpaseo/paseo/pull/6755)) +- Fixed shortcut recording saving the QWERTY key instead of the typed letter on layouts such as Dvorak ([#6652](https://github.com/getpaseo/paseo/pull/6652)) +- Fixed rebinding Jump to tab or Jump to workspace keeping only the pressed number instead of 1–9; record existing overrides again ([#7065](https://github.com/getpaseo/paseo/pull/7065) by [@dbunk903](https://github.com/dbunk903)) +- Fixed Previous and Next workspace shortcuts stopping at workspace 9 ([#6586](https://github.com/getpaseo/paseo/pull/6586) by [@IEatCodeDaily](https://github.com/IEatCodeDaily)) +- Fixed unreadable Android status bar icons in the Light theme and after the system theme changes ([#6707](https://github.com/getpaseo/paseo/pull/6707) by [@terayang](https://github.com/terayang)) +- Fixed the pairing QR code getting clipped on iOS and Android ([#6758](https://github.com/getpaseo/paseo/pull/6758) by [@fdkevin0](https://github.com/fdkevin0)) +- Fixed a `write EPIPE` error dialog on Linux desktop when the launcher stops reading console output ([#6639](https://github.com/getpaseo/paseo/pull/6639)) +- Fixed `paseo permit allow` and `deny` reporting success over SSH while the request stayed pending ([#6607](https://github.com/getpaseo/paseo/pull/6607) by [@budivoogt](https://github.com/budivoogt)) +- Fixed files and folders whose names start with two dots, like `..notes.txt`, being hidden and rejected ([#6604](https://github.com/getpaseo/paseo/pull/6604), [#6720](https://github.com/getpaseo/paseo/pull/6720) by [@Mar-garet](https://github.com/Mar-garet)) +- Fixed Markdown previews dropping table rows after a `<` that isn't HTML, such as `List` ([#6370](https://github.com/getpaseo/paseo/pull/6370) by [@gengjiawen](https://github.com/gengjiawen)) +- Fixed a literal heading tag appearing when an HTML heading contains a linked image ([#6650](https://github.com/getpaseo/paseo/pull/6650)) +- Fixed Astro diffs with an empty `{}` expression failing to render ([#6763](https://github.com/getpaseo/paseo/pull/6763)) +- Fixed slash command argument hints missing from autocomplete for ACP providers ([#6920](https://github.com/getpaseo/paseo/pull/6920) by [@bwestlund17](https://github.com/bwestlund17)) +- Fixed terminal profiles configured with the `agy` icon showing a generic bot instead of the Antigravity icon ([#6938](https://github.com/getpaseo/paseo/pull/6938) by [@noahg9](https://github.com/noahg9)) +- Fixed the Pi thinking level not updating when a Pi extension changes it ([#6741](https://github.com/getpaseo/paseo/pull/6741)) +- Fixed Pi subagents from `@tintinweb/pi-subagents` staying running after their turn limit ([#6594](https://github.com/getpaseo/paseo/pull/6594) by [@Chen-DaYi](https://github.com/Chen-DaYi)) +- Fixed manual `/compact` on OMP agents showing no progress and leaving the context meter stale ([#6106](https://github.com/getpaseo/paseo/pull/6106)) +- Fixed voice mode TTS rejecting model and voice names from OpenAI-compatible servers ([#6060](https://github.com/getpaseo/paseo/pull/6060) by [@khimaros](https://github.com/khimaros)) +- Fixed the xhigh thinking option showing "Extra high" instead of its configured label ([#6005](https://github.com/getpaseo/paseo/pull/6005) by [@EliorFureraj](https://github.com/EliorFureraj)) +- Fixed the Remove button going off screen for long custom model names in provider settings ([#6507](https://github.com/getpaseo/paseo/pull/6507)) +- Fixed the starting-ref picker not finding GitLab merge requests and issues by number ([#6525](https://github.com/getpaseo/paseo/pull/6525)) +- Fixed the starting-ref picker hiding change requests that match only in their description ([#6558](https://github.com/getpaseo/paseo/pull/6558)) +- Fixed @-mentions not finding plan files under `.cursor/plans` ([#6641](https://github.com/getpaseo/paseo/pull/6641) by [@chyendongnhanh338](https://github.com/chyendongnhanh338)) +- Fixed Paseo replacing a symlinked `config.json` with a regular file when saving settings ([#6023](https://github.com/getpaseo/paseo/pull/6023)) +- Fixed the published config schema rejecting valid settings such as `agentProfiles` and `plugins` ([#6621](https://github.com/getpaseo/paseo/pull/6621) by [@ragokan](https://github.com/ragokan)) +- Fixed duplicating a folder turning its relative symlinks into absolute links to the original ([#6771](https://github.com/getpaseo/paseo/pull/6771) by [@Mar-garet](https://github.com/Mar-garet)) +- Fixed renaming an untracked file like `[id].tsx` failing when a tracked file matches it as a pattern ([#6573](https://github.com/getpaseo/paseo/pull/6573) by [@Mar-garet](https://github.com/Mar-garet)) +- Fixed saving workspace files with long names failing with `ENAMETOOLONG` ([#6658](https://github.com/getpaseo/paseo/pull/6658) by [@Mar-garet](https://github.com/Mar-garet)) +- Fixed worktree creation and Changes failing for projects whose folder name ends in a space ([#6686](https://github.com/getpaseo/paseo/pull/6686)) +- Fixed merging a worktree when the base branch's worktree path contains a newline or ends in a space ([#6668](https://github.com/getpaseo/paseo/pull/6668) by [@Mar-garet](https://github.com/Mar-garet)) +- Fixed `paseo agent ls` listing only the first 200 agents ([#6033](https://github.com/getpaseo/paseo/pull/6033) by [@hteo1337](https://github.com/hteo1337)) +- Fixed `paseo worktree ls` and `paseo worktree archive` failing with "cwd or repoRoot is required" ([#6467](https://github.com/getpaseo/paseo/pull/6467) by [@jasonhnd](https://github.com/jasonhnd)) +- Fixed `paseo worktree ls` listing an archived worktree for up to 15 seconds ([#6545](https://github.com/getpaseo/paseo/pull/6545) by [@Mar-garet](https://github.com/Mar-garet)) +- Fixed `paseo logs --since` printing activity from before the requested timestamp ([#6514](https://github.com/getpaseo/paseo/pull/6514) by [@Mar-garet](https://github.com/Mar-garet)) +- Fixed `paseo logs --follow` splitting words across lines as text streams in ([#6528](https://github.com/getpaseo/paseo/pull/6528) by [@fiorelorenzo](https://github.com/fiorelorenzo)) +- Fixed `paseo logs` printing reasoning after the reply that followed it ([#6546](https://github.com/getpaseo/paseo/pull/6546)) +- Fixed the daemon keeping files open after a canceled HTTP download ([#6567](https://github.com/getpaseo/paseo/pull/6567) by [@Mar-garet](https://github.com/Mar-garet)) +- Fixed Linux desktop windows showing a generic icon instead of the Paseo icon ([#6645](https://github.com/getpaseo/paseo/pull/6645) by [@whme](https://github.com/whme), [@panta82](https://github.com/panta82)) +- Fixed Google Play listing Android devices without a camera as incompatible ([#6339](https://github.com/getpaseo/paseo/pull/6339)) +- Fixed `nix build .#desktop` running out of memory on Macs with less than 15 GB of RAM ([#6537](https://github.com/getpaseo/paseo/pull/6537) by [@truelecter](https://github.com/truelecter)) +- Fixed usage providers changing places after Refresh all ([#6947](https://github.com/getpaseo/paseo/pull/6947) by [@sleroq](https://github.com/sleroq)) +- Fixed the OpenCode Go usage icon being nearly invisible on dark themes ([#6362](https://github.com/getpaseo/paseo/pull/6362) by [@az33zy](https://github.com/az33zy)) + ## 0.11.4 - 2026-10-15 [Hoplite](https://hoplite.sh) joins Paseo as our first sponsor through the company sponsorship program. Hoplite builds cloud coding agents that turn tasks from GitHub, Linear, Slack and more into pull requests. Thank you to the Hoplite team for supporting Paseo. diff --git a/CLAUDE.md b/CLAUDE.md index 7076471ff9..fc431da2c5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,6 +49,8 @@ At the start of non-trivial work, list `docs/` and skim anything relevant to the | [docs/permissions.md](docs/permissions.md) | Semantic daemon permissions, principals, credentials, pairing invitations, and Hub authority | | [docs/terminal-performance.md](docs/terminal-performance.md) | Terminal latency pipeline, coalescing/backpressure invariants, benchmark + perf spec usage | | [docs/agent-stream-performance.md](docs/agent-stream-performance.md) | Assistant text pipeline — coalescing window, paced reveal, why arrival lumps are smoothed at render | +| [docs/markdown-renderer.md](docs/markdown-renderer.md) | Shared Markdown engine, native selection, links, images and library constraints | +| [docs/native-text-selection.md](docs/native-text-selection.md) | Native diagram grouping, selection boundaries and leading inline image limitations | | [docs/file-observation.md](docs/file-observation.md) | Recursive watcher ownership, Linux constraints, teardown invariants, and Parcel comparison | | [docs/testing.md](docs/testing.md) | TDD workflow, determinism, real dependencies over mocks, test organization | | [docs/qa.md](docs/qa.md) | QA evidence bar for pull requests — platform matrix, version drift, performance, UI proof | diff --git a/docs/agent-stream-performance.md b/docs/agent-stream-performance.md index e62f155fd5..ec506ee99b 100644 --- a/docs/agent-stream-performance.md +++ b/docs/agent-stream-performance.md @@ -12,7 +12,7 @@ provider deltas (every provider streams incrementally) → recordTimeline: one canonical row per flushed item → agent_stream ws message → reducer queue (app, one commit per frame) → session store - → source-item plugin transforms → native Markdown blocks / tool grouping + → source-item plugin transforms → Markdown presentation / tool grouping → paced reveal (app, per displayed item) → paint ``` @@ -29,23 +29,8 @@ So arrival sets a _target_ and the reveal rate is derived from the backlog inste - **The coalescer is leading + trailing.** The first delta after an idle window flushes synchronously; only the rest of the burst waits for the trailing timer. Reverting to trailing-only adds a full window to the first character of every turn. Same shape and the same reason as `TerminalOutputCoalescer`. - **The leading flush adds a canonical row, and that is fine.** A burst's first chunk lands as its own timeline row. `mergeAssistantChunks` / `mergeReasoningChunks` in `timeline-projection.ts` join contiguous same-turn rows, and clients read the projected timeline, so history is unaffected. Tests that assert on raw rows have to account for the extra row; tests that assert on what a client sees do not. - **The store holds the full text; only the rendered slice is paced.** Copy, selection, the chat outline, and scroll geometry all read the same string the user can see. Pacing the store instead would leave the bottom anchor chasing a content height that is ahead of the reveal. -- **Markdown blocks belong to presentation, and every assistant message is a block group.** - `agent-stream/presentation.ts` splits a message into one display row per Markdown block, the same - way whether it arrived streaming or as fetched history. Splitting in the reducer instead would - discard paragraph separators and expose fragments to plugin callbacks, which need the whole source - text. Live-head work stays cheap by parsing only the growing last block during append, retaining - completed blocks by object identity, and caching the split per source item so a tail change does - not re-split history. History was once left whole to keep cross-block Markdown context; that gave - one message two shapes depending on how it arrived, and message-addressed features only worked on - one of them. -- **A row id is never a message id.** Block rows are `${messageId}:block:${n}` with - `blockGroupId = messageId`, including single-block messages, so nothing can come to rely on the two - being equal. `getStreamItemMessageId` in `presentation.ts` is the only way to go from a row to its - message, and web rows carry it as `data-message-id` alongside `data-history-row-id`. Anything - addressing a message — chat find, scroll-to-message, history reveal, the find expansion that lifts - the render cap — uses the message id and must expect several rows to answer to it. Row ids stay for - React keys, virtualizer measurement, scroll anchors, and per-row caches like assistant image - occurrence keys. +- **One source assistant message is one display row.** Presentation projects plugin output and caches unchanged history. The shared [Markdown renderer](markdown-renderer.md) owns rendering on every platform. Chat paces the visible source and applies a 32,000-character render cap on web; native keeps the complete message. The native renderer keeps closed blocks in a stable head and sends the paced suffix to a separate tail view during streaming; completion reunites them for selection. This stays inside the renderer, without promoting blocks into timeline history. +- **Message identity survives completion.** Assistant row ids are source message ids. Plugin rows can replace a source, so find, scroll-to-message and history reveal still use `getStreamItemMessageId`. Width-specific measurements describe whole rendered messages. - **First sight of a text is revealed whole.** Only growth is paced. This is what makes history hydration, timeline replay, a virtualized row remounting on scroll, and an already-finished message all render complete on first paint without a special case for each. - **Leaving `phase: "streaming"` snaps the reveal.** A completed turn must never be left holding characters. `layoutStream` sets the phase, so anything outside the live head with an active turn is already complete. - **The reducer queue commits on a frame, with a timer as the ceiling.** A frame callback never fires in a hidden tab, so a timer races it and wins when nothing is painting — the store has to keep advancing either way. @@ -54,7 +39,7 @@ So arrival sets a _target_ and the reveal rate is derived from the backlog inste memo boundary each coalesced tick re-rendered every mounted row (about 50 on a phone, 100–250 ms of JS per tick). `layoutStream` keeps a layout item's identity when nothing about it changed, `useRevisedHistoryRows` hands a fresh item identity to rows whose tool-call group, expanded state, - or breakpoint changed, and `HistoryStreamRow` memoizes on both. Every viewport runs its history + or breakpoint changed, and `StreamRow` memoizes on both. Every viewport runs its history through that hook; the web viewport once skipped it and history hosts of a live tool group went stale. A new field on `StreamLayoutItem` must be added to `areLayoutItemsEquivalent`, or sharing silently stops. @@ -63,6 +48,12 @@ So arrival sets a _target_ and the reveal rate is derived from the backlog inste while text arrives, and release obsolete window caches as the reader reveals more history; window caches must stay bounded while their source timeline remains loaded. +## Native parsing cost + +The native library reparses and measures its entire `markdown` prop on each update. During streaming, the renderer splits source at an mdast block boundary and memoizes the head, so per-frame updates parse only the tail. Boundary discovery runs when a source line completes, rather than on every reveal frame. The unfinished line and final parsed block stay in the tail, preserving list continuation, setext headings, tables and open fences. Reference-dependent source and HTML retain a larger tail because later source can change their meaning. A single long block therefore still carries its full parsing cost. + +Completion restores one native text view per Mermaid-delimited segment, so selection crosses prose paragraphs again. The head retains its trailing block margin while split. JS reveal remains the pacing source; native opacity animation stays disabled. + ## Measuring - **Smoothness (user-perceived):** `packages/app/e2e/browser/agent-stream-smoothness.spec.ts`, gated behind `PASEO_AGENT_STREAM_PERF_E2E=1`. Drives the mock provider's `bursty-stream` model and reports coefficient of variation of characters painted per frame (smoothness) plus p95 gap between visible updates (stalls). Both numbers are needed: a stalled stream is perfectly smooth. diff --git a/docs/android.md b/docs/android.md index 75908a8ae2..c90a27efec 100644 --- a/docs/android.md +++ b/docs/android.md @@ -205,12 +205,12 @@ adb exec-out screencap -p > screenshot.png Stable tag pushes like `v0.1.0` trigger: -- The EAS GitHub app on Expo servers (iOS + Android production builds + store submit). There is no workflow file in this repo for it. +- The EAS GitHub app on Expo servers, using `packages/app/.eas/workflows/release-mobile.yml` (iOS + Android production builds and distribution). - `.github/workflows/android-apk-release.yml` on GitHub Actions (APK asset on GitHub Release). -iOS auto-submits to App Store review via a Fastlane lane after EAS uploads to TestFlight. Android auto-submits to the Play Store via EAS-managed credentials. +iOS distributes the same build to external TestFlight testers and App Store review; see [Mobile builds (EAS)](release.md#mobile-builds-eas) for the review paths. Android auto-submits to the Play Store via EAS-managed credentials. -Beta tags like `v0.1.1-beta.1` only trigger the GitHub APK workflow. They publish a GitHub prerelease APK for testing and do not submit to the stores. +Beta tags like `v0.1.1-beta.1` trigger the GitHub APK workflow and EAS `Release iOS Beta`. They publish a GitHub prerelease APK and distribute iOS through TestFlight without submitting to production stores. `android-v*` tags also trigger only the GitHub APK workflow — useful when you want to ship an APK without going through stores. The GitHub APK workflow supports `workflow_dispatch` with an existing `tag` input so you can rebuild without cutting a new tag. diff --git a/docs/markdown-renderer.md b/docs/markdown-renderer.md new file mode 100644 index 0000000000..418095730a --- /dev/null +++ b/docs/markdown-renderer.md @@ -0,0 +1,31 @@ +# Markdown rendering + +Use `MarkdownRenderer` for chat replies, file previews, plans, pull requests and changelog content. It uses `react-native-enriched-markdown` on iOS, Android and web. Keep source adaptation and renderer configuration in `components/markdown`; consumers supply Markdown and, when needed, its file location. + +Native selection belongs to the library's text view. Keep an assistant message in one row so selection can cross paragraphs, headings and lists. Code blocks and tables use the library's native views and their selection behavior. Web uses browser selection over semantic HTML. Once its WASM parser is initialized, the library parses synchronously for the first commit of each mounted row; an empty interim row would corrupt virtualized measurements and move the reader. Chat owns paced reveal, the web render cap, message identity and list measurement. Native renders completed messages together. While streaming, it keeps closed blocks in one head view and updates a separate tail, then reunites the text on completion. See [native parsing cost](agent-stream-performance.md#native-parsing-cost) for the boundary and pacing constraints. + +## Links and images + +Workspace file links use the shared file-link resolver. The workspace supplies its client, root and open-file action once; chat can supply the agent's more specific context. URLs use the same link press path. Recognized inline-code paths stay tappable. The library callback carries the tapped occurrence’s syntax context. Preserve it when opening files: explicit Markdown links decode URL escapes, while recognized inline-code paths preserve literal percent characters and use suffix lookup. Identical URL strings in different occurrences must stay distinct. Explicit absolute workspace links receive a relative hover title; existing authored titles remain intact. + +Resolve workspace image paths over the existing session RPC, then give the library a cached native file URL or web blob URL. HTTP(S) images pass through unchanged. This preserves relay, SSH and local socket connections, which do not expose a daemon HTTP endpoint. Each mounted renderer owns its image acquisition identity, so a later reply reads a rewritten path again while the earlier mounted reply retains its snapshot. Image taps open the attachment lightbox. File previews resolve relative images against the Markdown file's directory. + +## Engine constraints + +The pinned nightly has the same React Native package and core source trees as upstream commit `73ac5085eb1fdc743973b3473d4dba65abad5261`. Compatibility was checked with React Native 0.81.5. Native dependencies must be rebuilt when changing the highlighting configuration; JavaScript refresh cannot enable compiled grammars. + +Math and video are disabled. Task lists and admonitions are enabled; task checkboxes are read-only. Source adaptation spaces adjacent empty GFM table delimiters before escaping prose spoiler delimiters and decodes entity text before the engine can discard it. Code spans and fences keep their literal source. Comment HTML is normalized to Markdown before parsing; details show their summary and body expanded. + +Mermaid fences use the existing isolated diagram runtime and fullscreen viewer. Web dispatches them through the library’s code-block override. Native has no block-renderer hook, so the renderer extracts only Mermaid fences and places the existing WebView host between library-rendered segments. This is the only block exception; see [native selection limitations](native-text-selection.md) for diagram grouping and leading inline images. Streaming keeps the last valid diagram visible while the next revision renders. + +Native code blocks use the library's copy control and compiled grammars. Web code blocks use `HighlightedCodeBlock` through the library's `renderers.codeBlock` override. The patch also preserves web list numbering and link titles, marks pending parses with `aria-busy` so Find waits for rendered text, and carries link syntax through native and web tap events. Web inline code inside a link inherits only the link colour; its code font, size and background stay intact. Keep link variants colour-only: native pills replace text runs and prevent paths from wrapping; Android pills also use the surrounding block’s font size. The package patch is tracked by [upstream PR #997](https://github.com/software-mansion/enriched-markdown/pull/997); remove the patch when the pinned release includes it. + +The package patch also keeps overflowing native horizontal blocks in control of their drags: Android claims parent interception at touch-down and releases it for vertical drags so the chat list can scroll; iOS keeps its scroll recognizer active for outward drags at the leading edge. This prevents compact sidebar pans from taking block scrolling without changing sidebar swipes on prose or non-overflowing blocks. Native changes require a client rebuild. + +Android measurements use a size-only, 512-entry LRU across view unmounts, matching the library’s existing iOS cache. Scrolling back through history can reuse layout sizes without parsing those rows again. Content, layout props and width identify each entry; font-scale changes and native layout invalidation discard affected sizes. The cache retains no views or spans. + +The library determines how incomplete Markdown appears during streaming. Completed text uses the same component and engine as history. Find and scroll-to-message address source message ids; selection-copy understands semantic HTML and the web code component's copy metadata. + +## Verification + +Follow [QA](qa.md) and [mobile testing](mobile-testing.md). Exercise chat, file preview, plan and PR content on all three platforms. Include cross-paragraph selection, progressive streaming, a file-link tap, code copy/highlighting, a table and a workspace image. On iPad, repeat preview selection after opening and closing Explorer and after rotation. Check web find, selection-copy and source-message navigation after DOM changes. diff --git a/docs/mobile-testing.md b/docs/mobile-testing.md index 93fa35aedd..375da30708 100644 --- a/docs/mobile-testing.md +++ b/docs/mobile-testing.md @@ -300,16 +300,33 @@ done ### Android audio focus interruptions -Voice mode uses the custom `expo-two-way-audio` Android module, so incoming calls and other system audio owners must be tested with emulator/system commands, not a JS-only test. To verify that voice resume handles denied audio focus without crashing: +Use native instrumentation for audio interruptions. JS-only tests cannot prove Android focus, +input silencing, or route recovery. The module's `android/src/androidTest` suite runs through +Gradle's `connectedDebugAndroidTest` task and uses real `AudioManager` and `AudioRecord` instances. +Run the microphone-privacy cases on Android 12 or newer. + +Verify these distinct outcomes: + +- Dictation continues when another media owner takes permanent, temporary, or ducking focus. +- Disabling microphone access pauses audio delivery without discarding the dictation. Enabling + access resumes it. Starting dictation while access is disabled waits for the same recovery. +- Returning from music controls resumes dictation without pausing music again. Cancelling a + backgrounded recording must prevent the lifecycle callback from restarting it. +- Two-way voice still stops coherently on focus loss. Keep the incoming-call check below: it + exercises denied focus during lifecycle resume, beyond an ordinary media takeover. ```bash -adb shell am start -n sh.paseo/.MainActivity -# Start voice mode in an existing composer, then background Paseo with Home. +# Start voice mode in an existing composer, then background Paseo. adb emu gsm call 5551234 -# Foreground Paseo while the call is still ringing. +# Foreground Paseo while the call is still ringing, then end the simulated call. +adb emu gsm cancel 5551234 ``` -Expected result: Paseo does not throw `RuntimeException: Audio focus request failed`; native audio reports an interruption and voice mode stops or pauses coherently. +Expected result: no `RuntimeException: Audio focus request failed`, and voice mode stops or pauses +coherently. Repeat with dictation: it must retain captured audio and recover microphone access. +Use browser/device automation to operate the app; the commands above only control emulator telephony. + +This fix requires a rebuilt native app. An OTA update on an older binary retains legacy capture. ### Releasing the audio session when idle diff --git a/docs/native-text-selection.md b/docs/native-text-selection.md new file mode 100644 index 0000000000..dd7edcd79c --- /dev/null +++ b/docs/native-text-selection.md @@ -0,0 +1,24 @@ +# Native text selection + +Keep completed assistant prose in one native text view so selection crosses paragraphs, headings and lists. The shared [Markdown renderer](markdown-renderer.md) owns that view. + +## Android inverted chat + +Keep selection visibility handling in `modules/paseo-scroll`. Android's visibility +rectangles ignore list inversion. React Native decomposes Android's `scale: -1` +into rotation with positive `scaleY`, so test the composed matrix when deciding +whether those requests should move the viewport. Native tests must apply the +list's transform through React Native's view manager; assigning `scaleY` directly +misses that decomposition. + +## Diagrams in lists and quotes + +Selection stops at a Mermaid diagram. Native extracts Mermaid fences into separate views because the library exposes no native block renderer override. A diagram inside a list or quote therefore appears outside that container, and its following prose starts a separate Markdown fragment. + +Reopening a list marker or quote prefix on the following fragment cannot place the diagram inside the original container or continue its quote rule. Preserving the group requires a native block renderer hook or a host that owns the whole group. Keep native diagrams at the top level until that exists; nested grouping remains a known limitation. + +## Leading inline images + +Native image decoders do not support SVG badges, so the renderer shows their alt text. Pending images share the web placeholder: a reserved image box with a small centred image glyph; confirmed acquisition or decoding failures show alt text. + +The native library classifies an image from the text preceding it. A raster image at the start of a paragraph uses block sizing even when text follows; the inline size setting only affects images the library recognizes as inline. Correcting that classification requires a native library change and new app builds. JavaScript-only updates retain that classification until native app builds include a library fix. diff --git a/docs/protocol-validation.md b/docs/protocol-validation.md index 5fc31a5fdf..4694f9e208 100644 --- a/docs/protocol-validation.md +++ b/docs/protocol-validation.md @@ -6,7 +6,9 @@ The reason is mobile performance. A captured 353 KB provider snapshot cost about ## Runtime Path -`packages/protocol/src/validation/ws-outbound.ts` is the shipped boundary. It calls the generated `WSOutboundMessageSchema.safeParse` and returns the validated data. It does not normalize, repair, or re-validate the generated result. +`packages/protocol/src/validation/ws-outbound.ts` is the shipped boundary. It validates the envelope with a generated validator that checks only each session message's `type` tag, then runs the generated validator for that one message type and returns the validated data. Issue paths from the message validator, including nested union branches, are prefixed with `message`, so callers see the same issues as when one validator covered the whole envelope. It does not normalize, repair, or re-validate the generated result. + +Keep one generated function per message type. zod-aot inlines every nested schema into the function it compiles, so compiling `WSOutboundMessageSchema` as a whole produced one 11 MB function. On V8 (daemon clients, plugin hosts, the CLI, and Electron), the Maglev tier compiled that function once it was hot, allocating about 800 MB and stalling for seconds, and compiled it again after each deoptimization. Hermes has no JIT, so mobile never showed it. `packages/protocol/tests/validation/ws-outbound.test.ts` fails if a generated function grows past 1 MB. Generated validators preserve unknown keys where Zod object parsing strips them. The client dispatch path uses known `type` and payload fields, so this passthrough behavior is accepted for inbound messages. The wire format is unchanged. @@ -16,11 +18,10 @@ Provider model normalization is a parser-side compatibility shim in the client c The protocol package owns generation. -- `packages/protocol/codegen/ws-outbound.compile.ts` is the build-time zod-aot discovery entry. -- `packages/protocol/scripts/generate-validation-aot.mjs` runs the exact-pinned compiler and applies the small local compiler patches before generation. +- `packages/protocol/src/validation/ws-outbound-envelope.ts` defines the tag-only envelope schema and exposes the session message schemas that get their own validators. +- `packages/protocol/scripts/generate-validation-aot.mjs` writes the zod-aot discovery entry and the runtime schema metadata, runs the exact-pinned compiler, and applies the small local compiler patches before generation. It also emits the `type` → validator map the boundary dispatches through. - `packages/protocol/scripts/watch-validation-aot.mjs` reruns generation while editing protocol sources. -- `packages/protocol/src/generated/validation/ws-outbound.aot.ts` is generated runtime code and is gitignored. -- `packages/protocol/src/validation/ws-outbound-schema-metadata.ts` is runtime schema metadata for zod-aot fallback/default references. +- `packages/protocol/codegen/generated/ws-outbound.compile.ts`, `packages/protocol/src/generated/validation/ws-outbound.aot.ts`, and `packages/protocol/src/generated/validation/ws-outbound-schema-metadata.ts` are generated and gitignored. The metadata module lets generated code reach schema fallback/default values. Generation runs from protocol-owned lifecycle hooks: `prebuild`, `pretypecheck`, `pretest`, and `watch`. Installs do not run generation: published packages consume protocol from prebuilt `dist`, and local build/typecheck/test flows generate the source file at the point it is actually needed. diff --git a/docs/qa-evidence/question-response-retry/after.png b/docs/qa-evidence/question-response-retry/after.png new file mode 100644 index 0000000000..27dddb4bf6 Binary files /dev/null and b/docs/qa-evidence/question-response-retry/after.png differ diff --git a/docs/qa-evidence/question-response-retry/before.png b/docs/qa-evidence/question-response-retry/before.png new file mode 100644 index 0000000000..651e5f54da Binary files /dev/null and b/docs/qa-evidence/question-response-retry/before.png differ diff --git a/docs/release.md b/docs/release.md index cdac451f23..2be4b008db 100644 --- a/docs/release.md +++ b/docs/release.md @@ -305,12 +305,14 @@ macOS 13 maps to Darwin 22. The two values use different version domains; do not iOS and Android store builds are not in `.github/workflows`. They are triggered by the EAS GitHub app the moment the `v*` tag is pushed: - **Android (Play Store)** — EAS builds with profile `production` and auto-submits to the Play Store via `eas submit` (EAS-managed credentials, no Fastlane). -- **iOS (TestFlight + App Store)** — EAS builds with profile `production`, uploads to TestFlight, and a Fastlane lane submits the build for App Store review. +- **iOS (TestFlight + App Store)** — EAS builds with profile `production`, uploads once through a `testflight` job, distributes the build to the `Paseo Beta` external group with Beta App Review submission, and a Fastlane lane submits that same build for App Store review. - **Android APK (GitHub Release asset)** — separate, via `.github/workflows/android-apk-release.yml`. This is the only Android-related workflow that lives in this repo. EAS uses the local app version source. `packages/app/app.config.js` derives the native version from the package version. Android `versionCode` is `major * 1_000_000 + minor * 1_000 + patch`. iOS reserves 1,000 build slots per app version: beta `N` uses slot `N`, and stable uses slot `999`. For example, `0.2.6-beta.2` appears in App Store Connect as version `0.2.6` build `2006002`; stable uses build `2006999`. Rebuilding the same tag produces the same native build number; if a store has already accepted a binary and you need a different binary, cut the next beta or patch instead of relying on EAS remote auto-increment. -Beta tags run `Release iOS Beta`. The workflow uploads the build to TestFlight, distributes it to the persistent `Paseo Beta` external group, and submits it for Beta App Review. Testers and the group are managed once in App Store Connect; releases require no dashboard action. +Stable tags run `Release Mobile`; beta tags run `Release iOS Beta`. Both distribute to the persistent `Paseo Beta` external group and submit for Beta App Review, so testers receive stable releases as well as betas. Testers, the group, and required TestFlight test information are managed once in App Store Connect. + +Stable iOS releases use one binary for two Apple review paths: Beta App Review for external TestFlight distribution and App Store review. [Apple says](https://developer.apple.com/help/app-store-connect/test-a-beta-version/invite-external-testers) the first submitted build requires a full TestFlight review; later builds for the same version might not. Do not assume a stable patch skips review. There is no mobile-release workflow under `.github/workflows`. The EAS GitHub app reads the workflows under `packages/app/.eas/workflows` and handles tag triggering directly. @@ -334,7 +336,7 @@ npx eas build:list --platform android --limit 5 --non-interactive --json # Inspect a specific build. npx eas build:view -# Inspect the full release workflow, including submit_ios, submit_android, +# Inspect the full release workflow, including distribute_ios, submit_android, # and submit_ios_for_review. npx eas workflow:view --json @@ -347,17 +349,17 @@ npx eas build:view --json | jq '.logFiles[]' A build's `gitCommitHash` must match the release tag commit. `status` walks through `NEW` → `IN_QUEUE` → `IN_PROGRESS` → `FINISHED` (or `ERRORED`/`CANCELED`). The EAS workflow run's `gitCommitHash` and `trigger` must also match the release tag. -Once a build is `FINISHED`, EAS still has release-critical work to do: Android must submit to the Play Store, and iOS must upload to TestFlight **and** submit the build for App Store review. The release is not done until all platforms are on their way through the stores. +Once a build is `FINISHED`, EAS still has release-critical work to do: Android must submit to the Play Store, and iOS must distribute to the external TestFlight group with Beta App Review submission **and** submit the same build for App Store review. The release is not done until all platforms are on their way through the stores. For the `Release Mobile` EAS workflow, these jobs must pass: - `build_ios` — iOS binary built -- `submit_ios` — iOS binary uploaded to App Store Connect/TestFlight +- `distribute_ios` — iOS binary uploaded once, added to `Paseo Beta`, and submitted for Beta App Review - `submit_ios_for_review` — iOS build submitted for App Store review via Fastlane - `build_android` — Android store binary built - `submit_android` — Android binary submitted to the Play Store -Do not treat `build_ios: SUCCESS` or `submit_ios: SUCCESS` as a completed iOS release. `submit_ios_for_review: FAILURE` means the iOS release is blocked even if the build is visible in TestFlight. +Do not treat `build_ios: SUCCESS` or `distribute_ios: SUCCESS` as a completed iOS release. `submit_ios_for_review: FAILURE` means the iOS release is blocked even if the build is visible in TestFlight. To confirm the submission landed, inspect the EAS workflow with `npx eas workflow:view --json`. App Store Connect (review state for the matching version/build) and the Play Console track are the final ground truth. @@ -380,8 +382,9 @@ Windows, and Android APK assets are present along with the channel manifests (`latest-mac.yml`, `latest-linux.yml`, and `latest.yml` for stable; `beta-mac.yml`, `beta-linux.yml`, and `beta.yml` for beta). -For stable releases, also confirm every required mobile build, upload, store -submission, and review-submission job for the release commit. For betas, confirm +For stable releases, also confirm every required mobile build, external +TestFlight distribution, Beta App Review submission, and store submission job +for the release commit. For betas, confirm the beta EAS workflow completed its TestFlight distribution and Beta App Review path. Delete the heartbeat only after every applicable checklist item passes, then report the release as shipped. @@ -396,7 +399,7 @@ Pattern: "timezone": "UTC", "maxRuns": 120, "expiresIn": "24h", - "prompt": "Resume the vX.Y.Z release babysit for commit . Check npm tags; every GitHub Actions run for the release branch and tag; the published GitHub Release body, expected desktop/APK assets, and channel manifests; the Docker image; and the matching EAS workflow. Completion requires every applicable checklist item. For stable, require build_ios, submit_ios, submit_ios_for_review, build_android, and submit_android to succeed. For beta, require the beta TestFlight distribution and Beta App Review path. If work is pending, wait for the next heartbeat. If a failure can be retried safely for the same version, follow the failed-release procedure; otherwise report the blocker. When every applicable completion-checklist item passes, delete THIS heartbeat, report shipped, and stop.", + "prompt": "Resume the vX.Y.Z release babysit for commit . Check npm tags; every GitHub Actions run for the release branch and tag; the published GitHub Release body, expected desktop/APK assets, and channel manifests; the Docker image; and the matching EAS workflow. Completion requires every applicable checklist item. For stable, require build_ios, distribute_ios (Paseo Beta external distribution and Beta App Review submission), submit_ios_for_review, build_android, and submit_android to succeed. For beta, require the beta TestFlight distribution and Beta App Review path. If work is pending, wait for the next heartbeat. If a failure can be retried safely for the same version, follow the failed-release procedure; otherwise report the blocker. When every applicable completion-checklist item passes, delete THIS heartbeat, report shipped, and stop.", } ``` @@ -680,7 +683,7 @@ Every entry covers `previous stable release → release source`: first beta, lat - [ ] GitHub `Release Notes Sync` is green and the release body matches the stable changelog entry - [ ] EAS `Release Mobile` workflow for the same tag is green - [ ] EAS iOS `build_ios` completes for the same tag -- [ ] EAS iOS `submit_ios` succeeds, uploading the build to App Store Connect/TestFlight +- [ ] EAS iOS `distribute_ios` succeeds, uploading the binary once, distributing it to `Paseo Beta`, and submitting it for Beta App Review - [ ] EAS iOS `submit_ios_for_review` succeeds, putting the build into App Store review - [ ] EAS Android `build_android` completes for the same tag - [ ] EAS Android `submit_android` succeeds, putting the build on its Play Store track diff --git a/nix/npm-deps.hash b/nix/npm-deps.hash index 8906abc66b..25fc44e53a 100644 --- a/nix/npm-deps.hash +++ b/nix/npm-deps.hash @@ -1 +1 @@ -sha256-CK96G7MWsmd/VDvkuaFJ23U04EGNmeiXdTYbmR8RDDQ= +sha256-1WmrHvE68Lzyil4On7xtsPMQTODXZ3Jb3/juUg3nHi4= diff --git a/package-lock.json b/package-lock.json index c31951b1b3..e91904fd01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "paseo", - "version": "0.11.2", + "version": "0.12.0-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "paseo", - "version": "0.11.2", + "version": "0.12.0-beta.1", "hasInstallScript": true, "license": "Apache-2.0", "workspaces": [ @@ -16265,15 +16265,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/camelize": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", - "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001777", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", @@ -17088,15 +17079,6 @@ "node": ">=8" } }, - "node_modules/css-color-keywords": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", - "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", - "license": "ISC", - "engines": { - "node": ">=4" - } - }, "node_modules/css-in-js-utils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", @@ -17122,17 +17104,6 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/css-to-react-native": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", - "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", - "license": "MIT", - "dependencies": { - "camelize": "^1.0.0", - "css-color-keywords": "^1.0.0", - "postcss-value-parser": "^4.0.2" - } - }, "node_modules/css-tree": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", @@ -30248,6 +30219,7 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -30259,6 +30231,7 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, "license": "MIT" }, "node_modules/proper-lockfile": { @@ -30905,15 +30878,6 @@ "react-native": "*" } }, - "node_modules/react-native-fit-image": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/react-native-fit-image/-/react-native-fit-image-1.5.5.tgz", - "integrity": "sha512-Wl3Vq2DQzxgsWKuW4USfck9zS7YzhvLNPpkwUUCF90bL32e1a0zOVQ3WsJILJOwzmPdHfzZmWasiiAUNBkhNkg==", - "license": "Beerware", - "dependencies": { - "prop-types": "^15.5.10" - } - }, "node_modules/react-native-gesture-handler": { "version": "2.28.0", "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.28.0.tgz", @@ -30939,22 +30903,6 @@ "react-native": "*" } }, - "node_modules/react-native-markdown-display": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/react-native-markdown-display/-/react-native-markdown-display-7.0.2.tgz", - "integrity": "sha512-Mn4wotMvMfLAwbX/huMLt202W5DsdpMO/kblk+6eUs55S57VVNni1gzZCh5qpznYLjIQELNh50VIozEfY6fvaQ==", - "license": "MIT", - "dependencies": { - "css-to-react-native": "^3.0.0", - "markdown-it": "^10.0.0", - "prop-types": "^15.7.2", - "react-native-fit-image": "^1.5.5" - }, - "peerDependencies": { - "react": ">=16.2.0", - "react-native": ">=0.50.4" - } - }, "node_modules/react-native-nitro-modules": { "version": "0.35.5", "resolved": "https://registry.npmjs.org/react-native-nitro-modules/-/react-native-nitro-modules-0.35.5.tgz", @@ -31020,18 +30968,6 @@ "react-native": "*" } }, - "node_modules/react-native-uitextview": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/react-native-uitextview/-/react-native-uitextview-2.2.0.tgz", - "integrity": "sha512-Vbv3cTAuyfkYrfsR2YKFsOd9OYgfys+IX5yvvYY7Wd6TrOd6FSGrX93xtQHjeLXV1ds6fDJcFJsuLi3S4Ypr8A==", - "workspaces": [ - "example" - ], - "peerDependencies": { - "react": "*", - "react-native": "*" - } - }, "node_modules/react-native-unistyles": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/react-native-unistyles/-/react-native-unistyles-3.2.4.tgz", @@ -36413,7 +36349,7 @@ }, "packages/app": { "name": "@getpaseo/app", - "version": "0.11.2", + "version": "0.12.0-beta.1", "dependencies": { "@codemirror/commands": "6.10.4", "@codemirror/language": "6.12.4", @@ -36480,7 +36416,11 @@ "i18next": "^26.3.0", "lucide-react-native": "^1.50.0", "markdown-it": "^10.0.0", + "mdast-util-from-markdown": "2.0.3", + "mdast-util-gfm-table": "^2.0.0", "mermaid": "^11.16.0", + "micromark-extension-gfm-table": "^2.1.2", + "micromark-util-decode-string": "2.0.1", "mnemonic-id": "^3.2.7", "qrcode": "^1.5.4", "react": "19.1.0", @@ -36490,15 +36430,14 @@ "react-native": "0.81.5", "react-native-draggable-flatlist": "^4.0.3", "react-native-edge-to-edge": "^1.7.0", + "react-native-enriched-markdown": "1.2.0-nightly-20261014-766fc0e67", "react-native-gesture-handler": "~2.28.0", "react-native-keyboard-controller": "1.21.12", - "react-native-markdown-display": "^7.0.2", "react-native-nitro-modules": "0.35.5", "react-native-reanimated": "~4.3.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", "react-native-svg": "^15.14.0", - "react-native-uitextview": "^2.2.0", "react-native-unistyles": "^3.2.4", "react-native-web": "~0.21.0", "react-native-webview": "^13.16.0", @@ -37350,6 +37289,23 @@ } } }, + "packages/app/node_modules/micromark-extension-gfm-table": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.2.tgz", + "integrity": "sha512-pRzm4kDTu0MjlmBkxmS9yYhw60nncfcEwu9NNdPFSQEFXS95ZKyIIyTSHu/o3ReBUrLKYEq+7YaXCRn/bPB4MA==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "packages/app/node_modules/miniflare": { "version": "4.20260625.0", "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260625.0.tgz", @@ -37378,6 +37334,23 @@ "dev": true, "license": "MIT" }, + "packages/app/node_modules/react-native-enriched-markdown": { + "version": "1.2.0-nightly-20261014-766fc0e67", + "resolved": "https://registry.npmjs.org/react-native-enriched-markdown/-/react-native-enriched-markdown-1.2.0-nightly-20261014-766fc0e67.tgz", + "integrity": "sha512-PyYAAVJAyjlcqHXgSnzxok13MDmyxs/nt46hHIOBdCc0prfaL2Ef6nYkZu7fFCeaETOJXd/xQr4n2/79I+gvdA==", + "hasInstallScript": true, + "license": "MIT", + "peerDependencies": { + "katex": ">=0.16.0", + "react": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "katex": { + "optional": true + } + } + }, "packages/app/node_modules/react-native-keyboard-controller": { "version": "1.21.12", "resolved": "https://registry.npmjs.org/react-native-keyboard-controller/-/react-native-keyboard-controller-1.21.12.tgz", @@ -37496,12 +37469,12 @@ }, "packages/cli": { "name": "@getpaseo/cli", - "version": "0.11.2", + "version": "0.12.0-beta.1", "dependencies": { "@clack/prompts": "^1.0.0", - "@getpaseo/client": "0.11.2", - "@getpaseo/protocol": "0.11.2", - "@getpaseo/server": "0.11.2", + "@getpaseo/client": "0.12.0-beta.1", + "@getpaseo/protocol": "0.12.0-beta.1", + "@getpaseo/server": "0.12.0-beta.1", "chalk": "^5.3.0", "commander": "^12.0.0", "mime-types": "^2.1.35", @@ -37748,10 +37721,10 @@ }, "packages/client": { "name": "@getpaseo/client", - "version": "0.11.2", + "version": "0.12.0-beta.1", "dependencies": { - "@getpaseo/protocol": "0.11.2", - "@getpaseo/relay": "0.11.2", + "@getpaseo/protocol": "0.12.0-beta.1", + "@getpaseo/relay": "0.12.0-beta.1", "zod": "^4.4.3" }, "devDependencies": { @@ -37762,7 +37735,7 @@ }, "packages/desktop": { "name": "@getpaseo/desktop", - "version": "0.11.2", + "version": "0.12.0-beta.1", "license": "Apache-2.0", "dependencies": { "@getpaseo/cli": "*", @@ -38006,7 +37979,7 @@ }, "packages/expo-two-way-audio": { "name": "@getpaseo/expo-two-way-audio", - "version": "0.11.2", + "version": "0.12.0-beta.1", "license": "MIT", "devDependencies": { "@types/jest": "^29.5.14", @@ -38902,7 +38875,7 @@ }, "packages/highlight": { "name": "@getpaseo/highlight", - "version": "0.11.2", + "version": "0.12.0-beta.1", "dependencies": { "@codemirror/language": "6.12.4", "@codemirror/legacy-modes": "^6.5.3", @@ -38931,14 +38904,14 @@ }, "packages/plugin": { "name": "@getpaseo/plugin", - "version": "0.11.2", + "version": "0.12.0-beta.1", "dependencies": { "@agentclientprotocol/sdk": "^1.4.0", "use-sync-external-store": "^1.6.0" }, "devDependencies": { - "@getpaseo/client": "0.11.2", - "@getpaseo/protocol": "0.11.2", + "@getpaseo/client": "0.12.0-beta.1", + "@getpaseo/protocol": "0.12.0-beta.1", "@types/node": "^20.9.0", "@types/react": "~19.2.0", "react": "19.1.0", @@ -38948,8 +38921,8 @@ "zod": "^4.4.3" }, "peerDependencies": { - "@getpaseo/client": "0.11.2", - "@getpaseo/protocol": "0.11.2", + "@getpaseo/client": "0.12.0-beta.1", + "@getpaseo/protocol": "0.12.0-beta.1", "react": "~19.1.0", "react-native": ">=0.81.5", "zod": "^4.4.3" @@ -38971,7 +38944,7 @@ }, "packages/protocol": { "name": "@getpaseo/protocol", - "version": "0.11.2", + "version": "0.12.0-beta.1", "dependencies": { "semver": "^7.8.5", "zod": "^4.4.3" @@ -38998,7 +38971,7 @@ }, "packages/relay": { "name": "@getpaseo/relay", - "version": "0.11.2", + "version": "0.12.0-beta.1", "dependencies": { "base64-js": "^1.5.1", "tweetnacl": "^1.0.3", @@ -39216,16 +39189,16 @@ }, "packages/server": { "name": "@getpaseo/server", - "version": "0.11.2", + "version": "0.12.0-beta.1", "dependencies": { "@agentclientprotocol/sdk": "^0.17.1", "@anthropic-ai/claude-agent-sdk": "0.3.246", "@anthropic-ai/sdk": "^0.104.2", - "@getpaseo/client": "0.11.2", - "@getpaseo/highlight": "0.11.2", - "@getpaseo/plugin": "0.11.2", - "@getpaseo/protocol": "0.11.2", - "@getpaseo/relay": "0.11.2", + "@getpaseo/client": "0.12.0-beta.1", + "@getpaseo/highlight": "0.12.0-beta.1", + "@getpaseo/plugin": "0.12.0-beta.1", + "@getpaseo/protocol": "0.12.0-beta.1", + "@getpaseo/relay": "0.12.0-beta.1", "@isaacs/ttlcache": "^2.1.4", "@modelcontextprotocol/sdk": "1.29.0", "@opencode-ai/sdk": "1.14.46", @@ -40146,7 +40119,7 @@ }, "packages/website": { "name": "@getpaseo/website", - "version": "0.11.2", + "version": "0.12.0-beta.1", "dependencies": { "@cloudflare/vite-plugin": "^1.29.1", "@cloudflare/workers-types": "^4.20260317.1", @@ -41754,7 +41727,7 @@ }, "plugins": { "name": "@getpaseo/builtin-plugins", - "version": "0.11.2", + "version": "0.12.0-beta.1", "dependencies": { "@getpaseo/plugin": "*", "zod": "^4.4.3" diff --git a/package.json b/package.json index ecbfd83891..edc25df8f9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "paseo", - "version": "0.11.2", + "version": "0.12.0-beta.1", "private": true, "description": "Paseo: voice-controlled development environment for local AI coding agents", "keywords": [ diff --git a/packages/app/.eas/workflows/release-mobile.yml b/packages/app/.eas/workflows/release-mobile.yml index 13968b8b55..26790ef22f 100644 --- a/packages/app/.eas/workflows/release-mobile.yml +++ b/packages/app/.eas/workflows/release-mobile.yml @@ -23,13 +23,16 @@ jobs: platform: android profile: production - submit_ios: - name: Submit iOS + distribute_ios: + name: Distribute iOS to TestFlight needs: [build_ios] - type: submit + type: testflight params: build_id: ${{ needs.build_ios.outputs.build_id }} profile: production + external_groups: + - Paseo Beta + submit_beta_review: true submit_android: name: Submit Android @@ -41,7 +44,7 @@ jobs: submit_ios_for_review: name: Submit iOS for App Store review - needs: [build_ios, submit_ios] + needs: [build_ios, distribute_ios] environment: production runs_on: macos-medium steps: diff --git a/packages/app/e2e/browser/agent-message-submission.spec.ts b/packages/app/e2e/browser/agent-message-submission.spec.ts index 3bfda6b4b0..38598f4e65 100644 --- a/packages/app/e2e/browser/agent-message-submission.spec.ts +++ b/packages/app/e2e/browser/agent-message-submission.spec.ts @@ -1154,17 +1154,15 @@ test.describe("Agent message submission", () => { page, page.getByTestId("assistant-message").last(), ); - const assistantMessageCount = await page.getByTestId("assistant-message").count(); - const toolCallCount = await page.getByTestId("tool-call-badge").count(); await composer.press("Enter"); const userMessage = page.getByTestId("user-message").filter({ hasText: prompt }).last(); await expect(userMessage).toBeVisible(); - await expect - .poll(async () => page.getByTestId("assistant-message").count()) - .toBeGreaterThan(assistantMessageCount); - await expect - .poll(async () => page.getByTestId("tool-call-badge").count()) - .toBeGreaterThan(toolCallCount); + await expect( + page.getByTestId("assistant-message").filter({ + hasText: "I'll take a look at the scroll anchor behavior you described.", + }), + ).toBeVisible(); + await awaitToolCall(page, "Read"); await finishTimelineRowStabilityCheck(); }); diff --git a/packages/app/e2e/browser/agent-tab-image-stability.spec.ts b/packages/app/e2e/browser/agent-tab-image-stability.spec.ts index 841314343f..97b1c8337c 100644 --- a/packages/app/e2e/browser/agent-tab-image-stability.spec.ts +++ b/packages/app/e2e/browser/agent-tab-image-stability.spec.ts @@ -1,3 +1,12 @@ +import type { Page } from "@playwright/test"; +import { createReadStream } from "node:fs"; +import { createServer } from "node:http"; +import { delayFileReadResponse } from "../support/helpers/file-read-gate"; +import { + expectCompletedDiagram, + openDiagramFullscreen, + expectFullscreenDiagram, +} from "../support/helpers/diagram"; import { expect, test as base } from "../support/fixtures"; import { appendSettledTimelineTurns, @@ -17,6 +26,10 @@ import { import { expectAgentReadyToInterrupt } from "../support/helpers/agent-stream"; import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client"; import path from "node:path"; +import { + rememberTimelineViewport, + scrollTimelinePromptIntoView, +} from "../support/helpers/timeline-pagination"; const test = base.extend<{ imageWorkspace: SeededWorkspace }>({ imageWorkspace: async ({ page: _page }, provide) => { @@ -97,11 +110,11 @@ test("opens a timeline image in a zoomable lightbox", async ({ await openAssistantImageTimeline(page, imageAgent); await expectAssistantImageRendered(page, image); - await page.getByRole("button", { name: "Open image attachment" }).click(); + await page.getByRole("button", { name: image.alt }).click(); const lightboxImage = page.getByTestId("attachment-lightbox-image"); const canvas = page.getByTestId("attachment-lightbox-canvas"); await expect(lightboxImage).toBeVisible(); - await canvas.hover(); + await page.getByTestId("zoomable-viewport-toolbar").hover(); const transformedContent = canvas.locator(":scope > div").first(); const initialBox = await transformedContent.boundingBox(); const canvasBox = await canvas.boundingBox(); @@ -133,7 +146,7 @@ test("opens a timeline image in a zoomable lightbox", async ({ await expectAgentReadyToInterrupt(page); }); -test("reloading a timeline anchors near-tail assistant image growth", async ({ +test("reloading a timeline reserves image height without a load-time scroll jump", async ({ imageWorkspace: workspace, page, }) => { @@ -143,119 +156,34 @@ test("reloading a timeline anchors near-tail assistant image growth", async ({ fileName: "reload-geometry-image.png", }); const imageAgent = await createSettledMockAgent(workspace, "Reload image geometry"); - await emitSettledAssistantImage(workspace.client, imageAgent, image); - await appendSettledTimelineTurns(workspace.client, imageAgent, 3); + const delivery = await serveDelayedImage(path.join(workspace.repoPath, image.relativePath)); + try { + delivery.release(); + await emitSettledAssistantImage(workspace.client, imageAgent, image, delivery.url); + await appendSettledTimelineTurns(workspace.client, imageAgent, 3); + await openAssistantImageTimeline(page, imageAgent); + await expectAssistantImageRendered(page, image); - await page.addInitScript( - ({ accessibleName }) => { - interface ImageGeometryEvent { - atMs: number; - complete: boolean; - height: number; - naturalHeight: number; - naturalWidth: number; - scrollTop: number; - width: number; - } - const state = { - accessibleName, - events: [] as ImageGeometryEvent[], - mutationObserver: null as MutationObserver | null, - resizeObserver: null as ResizeObserver | null, - target: null as Element | null, - }; - const record = () => { - const target = state.target; - if (!target) return; - const imageElement = - target instanceof HTMLImageElement ? target : target.querySelector("img"); - const scroll = target.closest('[data-testid="agent-chat-scroll"]'); - const rect = target.getBoundingClientRect(); - const event: ImageGeometryEvent = { - atMs: performance.now(), - complete: imageElement?.complete ?? false, - height: rect.height, - naturalHeight: imageElement?.naturalHeight ?? 0, - naturalWidth: imageElement?.naturalWidth ?? 0, - scrollTop: scroll instanceof HTMLElement ? scroll.scrollTop : -1, - width: rect.width, - }; - const previous = state.events.at(-1); - if ( - !previous || - previous.complete !== event.complete || - previous.height !== event.height || - previous.naturalHeight !== event.naturalHeight || - previous.naturalWidth !== event.naturalWidth || - previous.scrollTop !== event.scrollTop || - previous.width !== event.width - ) { - state.events.push(event); - } - }; - const inspect = () => { - const target = Array.from(document.querySelectorAll('[role="img"]')).find( - (element) => element.getAttribute("aria-label") === accessibleName, - ); - if (!target || target === state.target) { - record(); - return; - } - state.target = target; - state.resizeObserver?.disconnect(); - state.resizeObserver = new ResizeObserver(record); - state.resizeObserver.observe(target); - record(); - }; - const start = () => { - state.mutationObserver = new MutationObserver(inspect); - state.mutationObserver.observe(document.documentElement, { - attributes: true, - attributeFilter: ["src", "style"], - childList: true, - subtree: true, - }); - inspect(); - }; - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", start, { once: true }); - } else { - start(); - } - ( - window as unknown as { - __paseoReloadImageGeometry?: typeof state; - } - ).__paseoReloadImageGeometry = state; - }, - { accessibleName: image.alt }, - ); + delivery.holdNextRequest(); + await page.reload(); + await delivery.waitUntilRequested(); + const rendered = page.getByRole("button", { name: image.alt }).first(); + await expect(rendered).toHaveAttribute("src", /^data:image\/png/); + await scrollTimelinePromptIntoView( + page, + "image-history-turn-2: emit 1 coalesced agent stream updates", + ); + const beforeLoad = await rememberTimelineViewport(page); + const reservedHeight = (await rendered.boundingBox())?.height; + expect(reservedHeight).toBe(320); - await openAssistantImageTimeline(page, imageAgent); - await expectAssistantImageRendered(page, image); - await page.reload(); - await expectAssistantImageRendered(page, image); - await page.waitForTimeout(500); - - const events = await page.evaluate(() => { - const state = ( - window as unknown as { - __paseoReloadImageGeometry?: { events: Array }; - } - ).__paseoReloadImageGeometry; - if (!state) throw new Error("Reload image geometry observer was not installed"); - return state.events; - }); - const geometryEvents = events as Array<{ height: number; scrollTop: number }>; - const initialGeometry = geometryEvents.find((event) => event.height > 0); - if (!initialGeometry) - throw new Error(`Image geometry was never measurable: ${JSON.stringify(events)}`); - const expandedGeometry = geometryEvents.find((event) => event.height > initialGeometry.height); - if (!expandedGeometry) - throw new Error(`Image geometry never expanded: ${JSON.stringify(events)}`); - expect(expandedGeometry.scrollTop, JSON.stringify(events)).toBeGreaterThan( - initialGeometry.scrollTop, - ); + delivery.release(); + await expectAssistantImageRendered(page, image); + expect((await rendered.boundingBox())?.height).toBe(reservedHeight); + await expect.poll(() => rememberTimelineViewport(page)).toEqual(beforeLoad); + } finally { + await delivery.close(); + } }); test("a real assistant PNG remains reachable through pagination and remount", async ({ @@ -296,3 +224,222 @@ test("a near-10 MiB real assistant PNG renders and the app remains responsive", response: "stress-update-0", }); }); + +test("a later reply reads new bytes at the same workspace image path", async ({ + imageWorkspace: workspace, + page, +}) => { + const agent = await createSettledMockAgent(workspace, "Screenshot revisions"); + const first = await createSmallAssistantPng(workspace, { + alt: "Earlier screenshot", + fileName: "screenshot.png", + }); + await emitSettledAssistantImage(workspace.client, agent, first); + await openAssistantImageTimeline(page, agent); + await expectAssistantImageRendered(page, first); + const second = await createSmallAssistantPng(workspace, { + alt: "Updated screenshot", + fileName: "screenshot.png", + width: 2, + height: 3, + }); + await emitSettledAssistantImage(workspace.client, agent, second); + await expectAssistantImageRendered(page, second); + await expectAssistantImageRendered(page, first); +}); + +test("pending assistant images reserve their decoded height", async ({ + imageWorkspace: workspace, + page, +}, testInfo) => { + const image = await createSmallAssistantPng(workspace, { + alt: "Delayed remote image", + fileName: "delayed.png", + width: 32, + height: 24, + }); + const agent = await createSettledMockAgent(workspace, "Pending image geometry"); + const delivery = await serveDelayedImage(path.join(workspace.repoPath, image.relativePath)); + const { url } = delivery; + try { + await emitSettledAssistantImage(workspace.client, agent, image, url); + await openAssistantImageTimeline(page, agent); + const rendered = page.getByRole("button", { name: image.alt }).first(); + await expect(rendered).toHaveAttribute("src", /^data:image\/png/); + await delivery.waitUntilRequested(); + const pendingBox = await rendered.boundingBox(); + expect(pendingBox?.height).toBe(320); + await rendered.click(); + await expect(page.getByTestId("attachment-lightbox-image")).toHaveCount(0); + await page.screenshot({ path: testInfo.outputPath("pending-image.png") }); + delivery.release(); + await expect(rendered).toHaveAttribute("src", url); + expect((await rendered.boundingBox())?.height).toBe(pendingBox?.height); + } finally { + await delivery.close(); + } +}); + +/** Serve the fixture's real PNG bytes; release controls when the decoder can finish. */ +async function serveDelayedImage(file: string) { + let release!: () => void; + let ready: Promise; + let requested!: () => void; + let requestStarted: Promise; + const holdNextRequest = () => { + ready = new Promise((resolve) => { + release = resolve; + }); + requestStarted = new Promise((resolve) => { + requested = resolve; + }); + }; + holdNextRequest(); + const server = createServer((_request, response) => { + requested(); + void ready.then(() => { + response.writeHead(200, { "Content-Type": "image/png", "Cache-Control": "no-store" }); + return createReadStream(file).pipe(response); + }); + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("Image server did not listen"); + return { + url: `http://127.0.0.1:${address.port}/image.png`, + release: () => release(), + holdNextRequest, + waitUntilRequested: () => requestStarted, + close: async () => { + release(); + server.closeAllConnections(); + await new Promise((resolve, reject) => { + server.close((error) => { + if (error) { + reject(error); + return; + } + resolve(); + }); + }); + }, + }; +} + +test("late workspace image completion preserves selected prose", async ({ + imageWorkspace, + page, +}) => { + await withPendingWorkspaceImage(page, imageWorkspace, async (image, release) => { + const selected = await selectImageFixtureParagraphs(page); + release(); + await expectAssistantImageRendered(page, image); + await expectSelectedParagraphsRetained(page, selected); + }); +}); + +test("late workspace image completion keeps Mermaid fullscreen open", async ({ + imageWorkspace, + page, +}) => { + await withPendingWorkspaceImage(page, imageWorkspace, async (image, release) => { + await openDiagramFullscreen(page); + await expectFullscreenDiagram(page, ["Start", "Done"]); + release(); + await expectAssistantImageRendered(page, image); + await expectFullscreenDiagram(page, ["Start", "Done"]); + }); +}); + +async function withPendingWorkspaceImage( + page: Page, + workspace: SeededWorkspace, + run: ( + image: Awaited>, + release: () => void, + ) => Promise, +) { + const image = await createSmallAssistantPng(workspace, { + alt: "Late workspace image", + fileName: "late.png", + width: 32, + height: 24, + }); + const source = [ + "Paragraph one stays selected while the workspace image loads.", + "Paragraph two remains in this reply.", + "Paragraph three is the selection endpoint.", + "```mermaid\nflowchart LR\n Start --> Done\n```", + `![${image.alt}](${image.relativePath})`, + ].join("\n\n"); + const agent = await createSettledMockAgent(workspace, "Delayed workspace read", source); + await emitSettledAssistantImage(workspace.client, agent, image); + const gate = await delayFileReadResponse(page, image.relativePath); + let released = false; + const release = () => { + if (!released) { + released = true; + gate.release(); + } + }; + try { + await openAssistantImageTimeline(page, agent); + await gate.waitUntilHeld(); + await expectCompletedDiagram(page, ["Start", "Done"]); + await run(image, release); + } finally { + release(); + } +} + +type SelectionProofWindow = Window & { __lateImageSelection?: { start: Node; end: Node } }; + +async function selectImageFixtureParagraphs(page: Page): Promise { + const message = page.getByTestId("assistant-message"); + await message + .getByText("Paragraph one stays selected while the workspace image loads.", { exact: true }) + .scrollIntoViewIfNeeded(); + const bounds = await message.evaluate((root) => { + const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); + let start: Text | undefined; + let end: Text | undefined; + while (walker.nextNode()) { + const text = walker.currentNode as Text; + if (text.data.startsWith("Paragraph one stays")) start = text; + if (text.data.startsWith("Paragraph three is")) end = text; + } + if (!start || !end) throw new Error("Selection paragraphs are missing"); + (window as SelectionProofWindow).__lateImageSelection = { start, end }; + const first = document.createRange(); + first.selectNodeContents(start); + const last = document.createRange(); + last.selectNodeContents(end); + return { + first: first.getBoundingClientRect().toJSON(), + last: last.getBoundingClientRect().toJSON(), + }; + }); + await page.mouse.move(bounds.first.x + 1, bounds.first.y + 5); + await page.mouse.down(); + await page.mouse.move(bounds.last.right - 1, bounds.last.bottom - 2, { steps: 20 }); + await page.mouse.up(); + const selected = await page.evaluate(() => window.getSelection()?.toString() ?? ""); + expect(selected).toContain("Paragraph one"); + expect(selected).toContain("Paragraph three"); + return selected; +} + +async function expectSelectedParagraphsRetained(page: Page, selected: string) { + await expect + .poll(() => + page.evaluate(() => { + const proof = (window as SelectionProofWindow).__lateImageSelection; + return { + startConnected: proof?.start.isConnected, + endConnected: proof?.end.isConnected, + text: window.getSelection()?.toString(), + }; + }), + ) + .toEqual({ startConnected: true, endConnected: true, text: selected }); +} diff --git a/packages/app/e2e/browser/agent-timeline-scroll-smoothness.spec.ts b/packages/app/e2e/browser/agent-timeline-scroll-smoothness.spec.ts index 73c662124f..04286b85d3 100644 --- a/packages/app/e2e/browser/agent-timeline-scroll-smoothness.spec.ts +++ b/packages/app/e2e/browser/agent-timeline-scroll-smoothness.spec.ts @@ -10,6 +10,8 @@ import { reportScrollJumps, scrollCadences, withVariedTimeline, + withStreamingReader, + holdParagraphThroughCompletion, } from "../support/helpers/timeline-scroll-smoothness"; test("scroll detector distinguishes entered image growth from a simultaneous viewport jump", () => { @@ -202,3 +204,22 @@ for (const cadence of scrollCadences) { test("reserves image space before its response arrives", async ({ page }, testInfo) => { await expectImageSpaceReserved(page, testInfo); }); + +test("holds a paragraph still while a mixed response streams and completes", async ({ page }) => { + await withStreamingReader(async (agent) => { + const reading = await holdParagraphThroughCompletion(page, agent); + expect( + reading.textGrowth, + "the held reader must observe an actively growing response", + ).toBeGreaterThan(1000); + expect( + reading.maxFollowGap, + "follow-bottom must catch each streamed commit before the next frame", + ).toBeLessThanOrEqual(1); + expect( + reading.maxDrift, + "streaming must preserve the held paragraph's viewport position", + ).toBeLessThanOrEqual(1); + expect(reading.retained, "completion must retain the paragraph's DOM node").toBe(true); + }); +}); diff --git a/packages/app/e2e/browser/assistant-message-render-limit.spec.ts b/packages/app/e2e/browser/assistant-message-render-limit.spec.ts index 0502d1375b..b6fae53e9a 100644 --- a/packages/app/e2e/browser/assistant-message-render-limit.spec.ts +++ b/packages/app/e2e/browser/assistant-message-render-limit.spec.ts @@ -2,7 +2,9 @@ import { expect, test } from "../support/fixtures"; import { getUtf8ByteLength } from "../../src/components/assistant-message-render-limit"; import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; -test("caps a newline-heavy assistant message before rendering markdown", async ({ page }) => { +test("caps a newline-heavy assistant message on web before rendering markdown", async ({ + page, +}) => { const hiddenTail = "HIDDEN_OVERSIZED_MESSAGE_TAIL"; const response = `${"x\n".repeat(16_000)}${hiddenTail}`; const agent = await seedMockAgentWorkspace({ diff --git a/packages/app/e2e/browser/assistant-selection-copy.spec.ts b/packages/app/e2e/browser/assistant-selection-copy.spec.ts index bf8e455c88..7daa11c7d6 100644 --- a/packages/app/e2e/browser/assistant-selection-copy.spec.ts +++ b/packages/app/e2e/browser/assistant-selection-copy.spec.ts @@ -73,9 +73,14 @@ const ASSISTANT_MARKDOWN = [ ].join("\n"); const EXPECTED_WHOLE_SELECTION_MARKDOWN = ASSISTANT_MARKDOWN.replace( - `${CROSS_STRUCTURE_MARKDOWN}\n\n`, - "", + "www.example.com stays", + "[www.example.com](http://www.example.com) stays", ) + .replace( + "`https://example.com/generated` stays", + "[`https://example.com/generated`](https://example.com/generated) stays", + ) + .replace(`${CROSS_STRUCTURE_MARKDOWN}\n\n`, "") .replace( "", "[https://autolink.example.com](https://autolink.example.com)", @@ -98,8 +103,7 @@ async function allowRichClipboard(context: BrowserContext): Promise { } /** - * The rows of one assistant message. A message renders one row per Markdown block, so - * every locator and every text walk below covers the group, not a single block. + * Locate assistant rows by source-message identity. */ function assistantMessageBlocks(page: Page): Locator { return page.locator("[data-message-id]").filter({ has: page.getByTestId("assistant-message") }); @@ -173,7 +177,7 @@ async function dragFromBelowImageTo(page: Page, text: string): Promise { const blocks = assistantMessageBlocks(page); const target = blocks.getByText(text); await target.scrollIntoViewIfNeeded(); - const frame = await blocks.locator('[data-paseo-markdown-tag="img"]').boundingBox(); + const frame = await blocks.locator("img").boundingBox(); const end = await target.evaluate((element, selectedText) => { const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT); while (walker.nextNode()) { @@ -192,7 +196,8 @@ async function dragFromBelowImageTo(page: Page, text: string): Promise { if (!frame) { throw new Error("Expected a rendered image"); } - await page.mouse.move(end.left + 5, frame.y + frame.height + 4); + await page.evaluate(() => window.getSelection()?.removeAllRanges()); + await page.mouse.move(end.left + 1, end.y - 14); await page.mouse.down(); await page.mouse.move(end.x, end.y, { steps: 10 }); await page.mouse.up(); @@ -210,18 +215,12 @@ async function selectAssistantListItemFromMarker( ): Promise { const assistantMessage = assistantMessageBlocks(page); const item = assistantMessage - .locator(`[data-paseo-markdown-tag="${listTag}"]`) + .locator(listTag) .filter({ hasText: itemText }) - .locator(':scope > [data-paseo-markdown-tag="li"]') + .locator(":scope > li") .filter({ hasText: itemText }); await item.evaluate((element, selectedEndText) => { - const marker = element.querySelector(':scope > [data-paseo-markdown-list-marker="true"]'); - const markerText = marker?.firstChild; - if (!(markerText instanceof Text)) { - throw new Error("Expected rendered list marker text"); - } - const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT); let endNode: Text | null = null; let endOffset = -1; @@ -239,7 +238,7 @@ async function selectAssistantListItemFromMarker( } const range = document.createRange(); - range.setStart(markerText, 0); + range.setStart(element, 0); range.setEnd(endNode, endOffset); const selection = window.getSelection(); selection?.removeAllRanges(); @@ -254,7 +253,7 @@ async function doubleClickAssistantMarkdownText( ): Promise { const assistantMessage = assistantMessageBlocks(page); await assistantMessage - .locator(`[data-paseo-markdown-tag="${tag}"]`) + .locator(tag) .filter({ hasText: text }) .dblclick({ position: { x: 5, y: 5 } }); } @@ -276,39 +275,23 @@ async function selectAssistantTextRange( found = blockWalker.nextNode(); } } - let cursor = 0; - const walker = { nextNode: () => nodes[cursor++] ?? null }; - let startNode: Node | null = null; - let startOffset = -1; - let endNode: Node | null = null; - let endOffset = -1; - let textNode = walker.nextNode(); - while (textNode) { - if (!startNode) { - const offset = textNode.textContent?.indexOf(selectedRange.startText) ?? -1; - if (offset >= 0) { - startNode = textNode; - startOffset = offset; - } - } - if (startNode) { - const offset = textNode.textContent?.indexOf(selectedRange.endText) ?? -1; - if (offset >= 0) { - endNode = textNode; - endOffset = offset + selectedRange.endText.length; - break; - } - } - textNode = walker.nextNode(); - } - if (!startNode || !endNode) { + const text = nodes.map((node) => node.data).join(""); + const start = text.indexOf(selectedRange.startText); + const end = text.indexOf(selectedRange.endText, start); + if (start < 0 || end < 0) throw new Error( `Could not find assistant text range: ${selectedRange.startText} — ${selectedRange.endText}`, ); - } + const boundary = (offset: number, isEnd = false): [Text, number] => { + for (const node of nodes) { + if (offset < node.length || (isEnd && offset === node.length)) return [node, offset]; + offset -= node.length; + } + throw new Error("Selection extends beyond message"); + }; const range = document.createRange(); - range.setStart(startNode, startOffset); - range.setEnd(endNode, endOffset); + range.setStart(...boundary(start)); + range.setEnd(...boundary(end + selectedRange.endText.length, true)); const selection = window.getSelection(); selection?.removeAllRanges(); selection?.addRange(range); @@ -428,16 +411,12 @@ test("copying an assistant selection preserves Markdown structure and links", as ["em", "emphasized prose"], ["s", "struck prose"], ]) { - const formattedProse = assistantMessage - .locator(`[data-paseo-markdown-tag="${tag}"]`) - .filter({ hasText: text }); + const formattedProse = assistantMessage.locator(tag).filter({ hasText: text }); await expect(formattedProse).toHaveCSS("font-family", "serif"); await expect(formattedProse).not.toHaveAttribute("data-pmono"); } - const inlineCode = assistantMessage - .locator('[data-paseo-markdown-tag="code"]') - .filter({ hasText: "apply_patch" }); - await expect(inlineCode).toHaveAttribute("data-pmono", ""); + const inlineCode = assistantMessage.locator("code").filter({ hasText: "apply_patch" }); + await expect(inlineCode).toHaveCSS("font-family", /monospace|Mono/); await selectStructuredClipboardFixture(page); await copySelection(page); @@ -466,7 +445,7 @@ test("copying an assistant selection preserves Markdown structure and links", as 'workspace file', ); expect(clipboard.html).toContain("https://example.com/generated"); - expect(clipboard.html).not.toContain( + expect(clipboard.html).toContain( 'https://example.com/generated', ); expect(clipboard.html).toContain(''); diff --git a/packages/app/e2e/browser/chat-find.spec.ts b/packages/app/e2e/browser/chat-find.spec.ts index 0ce6c561ed..b29a5f5b89 100644 --- a/packages/app/e2e/browser/chat-find.spec.ts +++ b/packages/app/e2e/browser/chat-find.spec.ts @@ -33,9 +33,10 @@ const STREAMED_RESPONSE = [ const RESPONSE = 'İ😀hello **world**; hello *world*.\n\n[hello & world](https://example.com/hidden-destination)\n\n```ts\nconst a = "a.b";\n```\n\n- first cell\n- second cell'; -// One message far taller than the viewport, so its first and last paragraph rows are -// never in the virtualizer's window at the same time. +// One message taller than the viewport, exercising navigation between distant +// matches inside the same rendered row. const PARAGRAPH_PADDING = Array.from({ length: 40 }, (_, index) => `padding${index}`).join(" "); +const VIRTUALIZED_TURN_COUNT = 60; const TALL_RESPONSE = Array.from({ length: 30 }, (_, index) => { if (index === 0) return `Opening paragraph with span-needle. ${PARAGRAPH_PADDING}`; if (index === 29) return `Closing paragraph with span-needle. ${PARAGRAPH_PADDING}`; @@ -322,11 +323,11 @@ test("finds a hit in a message that streamed while the chat was open", async ({ const chat = await openChatStreamingMultiBlockReply(page); try { await openChatFind(page); - // The only hit is in the first paragraph, which is no longer the row that grows. + // The first-paragraph hit must remain searchable while the same message grows. await expectFindStatus(page, "alpha-needle", "1 of 1"); await chat.replyFinished(); await expectFindStatus(page, "beta-needle", "1 of 1"); - // One message, two Markdown blocks, one count across both. + // One message, two paragraphs, one count across both. await expectFindStatus(page, "pair-needle", "1 of 2"); await expectNextMatch(page, "2 of 2", "pair-needle"); await closeFind(page); @@ -346,12 +347,14 @@ async function openChatWithVirtualizedTallMessage(page: Page): Promise { @@ -370,9 +372,9 @@ test("counts every occurrence of a virtualized message and steps between them", const chat = await openChatWithVirtualizedTallMessage(page); try { await openChatFind(page); - await expectFindStatus(page, "span-needle", "1 of 12"); + await expectFindStatus(page, "span-needle", `1 of ${VIRTUALIZED_TURN_COUNT * 2}`); await expectHighlight(page, "span-needle"); - await expectNextMatch(page, "2 of 12", "span-needle"); + await expectNextMatch(page, `2 of ${VIRTUALIZED_TURN_COUNT * 2}`, "span-needle"); await closeFind(page); } finally { await chat.cleanup(); diff --git a/packages/app/e2e/browser/composer-diff-stat.spec.ts b/packages/app/e2e/browser/composer-diff-stat.spec.ts index 00705faccd..b3d1b56c12 100644 --- a/packages/app/e2e/browser/composer-diff-stat.spec.ts +++ b/packages/app/e2e/browser/composer-diff-stat.spec.ts @@ -310,7 +310,7 @@ async function sendFeedback(page: Page, count: number) { } async function expectFeedbackSent(page: Page, recipient: string) { - await expect(page.getByText(`Feedback sent to ${recipient}`, { exact: true })).toBeVisible(); + await expect(page.getByTestId("app-toast")).toHaveText(`Feedback sent to ${recipient}`); await expect(page.getByTestId("working-diff-panel")).toBeVisible(); } @@ -340,6 +340,9 @@ test.describe("send review feedback", () => { await expectFeedbackSent(page, "Composer diff stat"); await expectReviewAbsent(page, "Please simplify this."); await captureFeedback(page, "single"); + await expect( + page.getByText("Feedback sent to Composer diff stat", { exact: true }), + ).toHaveCount(0); }); await test.step("Return to the untouched composer draft", async () => { await showWideLayout(page); @@ -375,11 +378,7 @@ test.describe("send review feedback", () => { ).toBeDisabled(); expect(gate.requestCount()).toBe(1); gate.accept(); - await expect( - compactReviewSurface(page).getByText("Feedback sent to Composer diff stat", { - exact: true, - }), - ).toBeVisible(); + await expectFeedbackSent(page, "Composer diff stat"); }); } finally { await workspace.cleanup(); diff --git a/packages/app/e2e/browser/file-editing.spec.ts b/packages/app/e2e/browser/file-editing.spec.ts index 7726c581f3..da729381d2 100644 --- a/packages/app/e2e/browser/file-editing.spec.ts +++ b/packages/app/e2e/browser/file-editing.spec.ts @@ -72,10 +72,10 @@ async function selectFileView(page: Page, view: "Preview" | "Source"): Promise { - const image = page.getByRole("img", { name, exact: true }); + const image = page.getByRole("button", { name, exact: true }); await expect(image).toBeVisible(); await expect - .poll(() => image.locator("img").evaluate((element: HTMLImageElement) => element.naturalWidth)) + .poll(() => image.evaluate((element: HTMLImageElement) => element.naturalWidth)) .toBeGreaterThan(0); } @@ -221,7 +221,7 @@ test.describe("CodeMirror workspace file editing", () => { const fileLink = page.getByRole("link", { name: "Open target" }).first(); await expect(fileLink).toBeVisible({ timeout: 15_000 }); await fileLink.hover(); - await expect(page.getByText("src/target.ts:42-45", { exact: true })).toBeVisible(); + await expect(fileLink).toHaveAttribute("title", "src/target.ts:42-45"); } finally { await workspace.cleanup(); } diff --git a/packages/app/e2e/browser/folder-links.spec.ts b/packages/app/e2e/browser/folder-links.spec.ts new file mode 100644 index 0000000000..7e1bfac1ae --- /dev/null +++ b/packages/app/e2e/browser/folder-links.spec.ts @@ -0,0 +1,45 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { expect, test, type Page } from "../support/fixtures"; +import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; + +async function openReportedFolderLink(page: Page, name: string) { + const link = page.getByTestId("assistant-message").getByRole("link", { name, exact: true }); + await link.click(); +} + +async function expectFolderContents(page: Page) { + await expect(page.getByText("Requested path is not a file", { exact: true })).toHaveCount(0); + await expect(page.getByTestId("file-explorer-tree-scroll")).toBeVisible(); + await expect( + page.getByTestId("file-explorer-tree-scroll").getByText("guide.md", { exact: true }), + ).toBeVisible(); +} + +for (const folder of ["docs/guides", "docs/guides/", "docs/reports.v1", ".github/workflows"]) { + test(`chat folder link opens folder contents: ${folder}`, async ({ page }) => { + const session = await seedMockAgentWorkspace({ + repoPrefix: "folder-link-repro-", + title: "Folder link reproduction", + initialPrompt: "Show the folder link", + featureValues: { mockAssistantResponse: `[Open folder](./${folder})` }, + }); + try { + await mkdir(path.join(session.cwd, folder), { recursive: true }); + await writeFile(path.join(session.cwd, folder, "guide.md"), "# Folder link destination\n"); + await session.client.waitForFinish(session.agentId, 30_000); + await page.addInitScript(() => { + localStorage.setItem( + "panel-state", + JSON.stringify({ state: { explorerShowHiddenFiles: false }, version: 0 }), + ); + }); + await openAgentRoute(page, session); + await openReportedFolderLink(page, "Open folder"); + await expectFolderContents(page); + } finally { + await page.screenshot({ path: test.info().outputPath("folder-link-result.png") }); + await session.cleanup(); + } + }); +} diff --git a/packages/app/e2e/browser/open-project-scroll.spec.ts b/packages/app/e2e/browser/open-project-scroll.spec.ts new file mode 100644 index 0000000000..b0b9e9c9c4 --- /dev/null +++ b/packages/app/e2e/browser/open-project-scroll.spec.ts @@ -0,0 +1,42 @@ +import { type Page } from "@playwright/test"; +import { test, expect } from "../support/fixtures"; + +async function openProjectHome(page: Page) { + await page.goto("/open-project"); + await expect(page.getByText("Add a project", { exact: true })).toBeVisible(); +} + +async function scrollToHomeFooter(page: Page) { + await page.mouse.move(160, 350); + await page.mouse.wheel(0, 1500); +} + +async function expectHomeActionsReachable(page: Page) { + const providers = page.getByTestId("open-project-setup-providers"); + const community = page.getByRole("button", { name: "Community", exact: true }); + await expect(providers).toBeInViewport({ ratio: 1 }); + await expect(community).toBeInViewport({ ratio: 1 }); + const providerBounds = await providers.boundingBox(); + const communityBounds = await community.boundingBox(); + expect(providerBounds).not.toBeNull(); + expect(communityBounds).not.toBeNull(); + expect(communityBounds!.y).toBeGreaterThanOrEqual(providerBounds!.y + providerBounds!.height); +} + +test("home actions remain reachable on a short phone screen", async ({ page }, testInfo) => { + await page.setViewportSize({ width: 320, height: 568 }); + await openProjectHome(page); + await scrollToHomeFooter(page); + await expectHomeActionsReachable(page); + await page.screenshot({ path: testInfo.outputPath("home-scrolled.png") }); +}); + +test("home actions fit on a desktop screen without overlap", async ({ page }) => { + await page.setViewportSize({ width: 1280, height: 900 }); + await openProjectHome(page); + await expectHomeActionsReachable(page); + await expect(page.getByTestId("open-project-setup-providers")).toHaveCSS( + "-webkit-app-region", + "none", + ); +}); diff --git a/packages/app/e2e/browser/plan-card-markdown.spec.ts b/packages/app/e2e/browser/plan-card-markdown.spec.ts index b5d7cfe1e7..5dcf2a2cec 100644 --- a/packages/app/e2e/browser/plan-card-markdown.spec.ts +++ b/packages/app/e2e/browser/plan-card-markdown.spec.ts @@ -1,15 +1,9 @@ +import type { Locator, Page } from "@playwright/test"; import { expect, test } from "../support/fixtures"; import { waitForPermissionPrompt } from "../support/helpers/permissions"; import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; -// The plan card has to pass its own markdown parser. Drop that prop and -// react-native-markdown-display silently supplies one with `typographer: true` -// (node_modules/react-native-markdown-display/src/index.js:139-141), which -// turns (c) into ©, curls straight quotes, and rewrites --- as an em dash. -// No unit test can catch that, because the defect lives in the JSX. -// -// Nothing here is provider-specific: the mock provider drives it, and the -// behavior is the same for every agent. +// Literal shell commands and copyright markers must survive Markdown rendering. test.describe("Plan card markdown", () => { test("renders plan text verbatim", async ({ page }) => { test.setTimeout(180_000); @@ -25,6 +19,7 @@ test.describe("Plan card markdown", () => { await waitForPermissionPrompt(page, 120_000); const planCard = page.getByTestId("permission-plan-card"); + await expect(planCard.locator(".enrm-text")).toBeVisible(); await expect(planCard).toContainText("(c)"); await expect(planCard).toContainText('--name="my repo"'); await expect(planCard).toContainText("---buzz"); @@ -35,3 +30,161 @@ test.describe("Plan card markdown", () => { } }); }); + +test("chat and file preview share the library renderer and resolve file links", async ({ + page, +}, testInfo) => { + await withSharedMarkdownFiles(page, async () => { + const message = page.getByTestId("assistant-message"); + await expect(message).toHaveCount(1); + await expect(message.locator(".enrm-text > p").first()).toContainText("First paragraph."); + await expect(message.locator(".enrm-text > p").nth(1)).toContainText("Second paragraph."); + await expect(message.locator("blockquote")).toContainText("A quote"); + await expect(message.getByRole("table")).toContainText("42"); + await expect(message.getByRole("cell").nth(0)).toHaveText("Answer"); + await expect(message.getByRole("cell").nth(1)).toHaveText(""); + await expect(message.getByRole("cell").nth(2)).toHaveText("42"); + await expect(message.locator("p div")).toHaveCount(0); + await openSharedMarkdownFile(page, "README.md"); + const preview = page.getByTestId("markdown-preview-frame"); + await expect(preview.locator(".enrm-text")).toContainText("Second paragraph."); + await expect(preview.getByRole("table")).toContainText("42"); + await returnToSharedMarkdownChat(page); + await expectFileLinkTooltip( + page, + message.getByRole("link", { name: "src/说明.md:2-3", exact: true }), + "packages/app/src/说明.md:2-3", + ); + await openSharedMarkdownFile(page, "src/说明.md:2-3"); + await expect(page.getByText("Unicode suffix lookup succeeded.", { exact: true })).toBeVisible(); + await openSharedMarkdownFile(page, "packages/app/src/说明.md:2-3"); + await expect(page.getByText("Unicode suffix lookup succeeded.", { exact: true })).toBeVisible(); + await openSharedMarkdownFile(page, "Space file"); + await expect( + page.getByText("Explicit link opened the space filename.", { exact: true }), + ).toBeVisible(); + await openSharedMarkdownFile(page, "my%20file.ts"); + await expect( + page.getByText("Inline code opened the literal percent filename.", { exact: true }), + ).toBeVisible(); + await testInfo.attach("shared-renderer", { + body: await page.screenshot(), + contentType: "image/png", + }); + }); +}); +async function expectFileLinkTooltip(page: Page, link: Locator, path: string) { + await link.hover(); + await expect(page.getByRole("tooltip")).toHaveText(path); +} + +async function withSharedMarkdownFiles(page: Page, run: () => Promise) { + const { writeFile, mkdir } = await import("node:fs/promises"); + const source = + "First paragraph.\n\nSecond paragraph.\n\n```ts\nconst answer = 42;\n```\n\n> A quote\n\n| Key || Value |\n| --- | --- | --- |\n| Answer || 42 |\n\nOpen `README.md` or `src/说明.md:2-3`, or packages/app/src/说明.md:2-3.\n\n[Space file](my%20file.ts) and `my%20file.ts`."; + const session = await seedMockAgentWorkspace({ + repoPrefix: "shared-markdown-", + title: "Shared Markdown", + featureValues: { mockAssistantResponse: source }, + initialPrompt: "Show Markdown", + }); + try { + await writeFile(`${session.cwd}/README.md`, source); + await writeFile(`${session.cwd}/my file.ts`, "Explicit link opened the space filename."); + await writeFile( + `${session.cwd}/my%20file.ts`, + "Inline code opened the literal percent filename.", + ); + await mkdir(`${session.cwd}/packages/app/src`, { recursive: true }); + await writeFile( + `${session.cwd}/packages/app/src/说明.md`, + "# Nested file\n\nUnicode suffix lookup succeeded.", + ); + await session.client.waitForFinish(session.agentId, 30_000); + // A cold render must work even when a separate parser chunk cannot be fetched. + await page.route("**/wasm/md4c.bundle?**", (route) => route.abort("failed")); + await openAgentRoute(page, session); + await run(); + } finally { + await session.cleanup(); + } +} + +async function returnToSharedMarkdownChat(page: Page) { + await page.getByRole("button", { name: "Shared Markdown", exact: true }).click(); +} + +async function openSharedMarkdownFile(page: Page, name: string) { + await returnToSharedMarkdownChat(page); + await page.getByTestId("assistant-message").getByRole("link", { name, exact: true }).click(); +} + +test("linked inline code keeps code typography and wraps long paths", async ({ + page, +}, testInfo) => { + const longPath = + "packages/app/src/components/markdown/renderer/very-long-directory-name/another-long-directory-name/file.ts:12"; + const session = await seedMockAgentWorkspace({ + repoPrefix: "inline-code-style-", + title: "Inline code style", + featureValues: { + mockAssistantResponse: + "Compare `ordinary_token`, `file.ts:12`, `https://example.com`. At the end of this line open `" + + longPath + + "`.", + }, + initialPrompt: "Show inline code", + }); + try { + await session.client.waitForFinish(session.agentId, 30_000); + await openAgentRoute(page, session); + const message = page.getByTestId("assistant-message"); + await expect(message.getByRole("link", { name: longPath, exact: true })).toBeVisible(); + const measurements = await measureInlineCode(message); + await testInfo.attach("inline-code-measurements", { + body: JSON.stringify(measurements, null, 2), + contentType: "application/json", + }); + expect(measurements.codes).toHaveLength(4); + const [plain, file, url, long] = measurements.codes; + for (const linked of [file, url, long]) { + expect(linked.fontSize).toBe(plain.fontSize); + expect(linked.fontFamily).toBe(plain.fontFamily); + expect(linked.backgroundColor).toBe(plain.backgroundColor); + expect(linked.color).toBe(linked.anchorColor); + expect(linked.color).not.toBe(plain.color); + } + expect(long.lines).toBeGreaterThan(1); + expect(long.right).toBeLessThanOrEqual(measurements.right + 1); + } finally { + await session.cleanup(); + } +}); + +async function measureInlineCode(message: Locator) { + return message.evaluate((element) => { + // Constrain the paragraph to exercise an inline path at a line boundary. + const paragraph = element.querySelector("p")!; + paragraph.style.width = "300px"; + const codes = Array.from(paragraph.querySelectorAll("code"), (code) => { + const style = getComputedStyle(code); + const range = document.createRange(); + range.selectNodeContents(code); + const rects = Array.from(range.getClientRects()); + return { + text: code.textContent, + fontSize: style.fontSize, + fontFamily: style.fontFamily, + backgroundColor: style.backgroundColor, + color: style.color, + anchorColor: code.closest("a") ? getComputedStyle(code.closest("a")!).color : null, + whiteSpace: style.whiteSpace, + overflowWrap: style.overflowWrap, + rects: rects.map((rect) => ({ left: rect.left, right: rect.right, top: rect.top })), + lines: new Set(rects.map((rect) => rect.top)).size, + right: Math.max(...rects.map((rect) => rect.right)), + }; + }); + return { codes, right: paragraph.getBoundingClientRect().right }; + }); +} diff --git a/packages/app/e2e/browser/question-prompt-pagination.spec.ts b/packages/app/e2e/browser/question-prompt-pagination.spec.ts index 1d75ae2839..b444a8cddf 100644 --- a/packages/app/e2e/browser/question-prompt-pagination.spec.ts +++ b/packages/app/e2e/browser/question-prompt-pagination.spec.ts @@ -1,5 +1,16 @@ +import { fork } from "node:child_process"; +import path from "node:path"; +import { expect } from "@playwright/test"; +import { z } from "zod"; import { test } from "../support/fixtures"; -import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; +import { expectVisibleAgentSurfacesIdle } from "../support/helpers/agent-stream"; +import { buildSeededHost } from "../support/helpers/daemon-registry"; +import { killProcessTree } from "../support/helpers/spawn-node"; +import { + buildAgentRoute, + openAgentRoute, + seedMockAgentWorkspace, +} from "../support/helpers/mock-agent"; import { chooseQuestionOption, continueToNextQuestion, @@ -130,3 +141,204 @@ test.describe("Question prompt pagination", () => { } }); }); + +const questionDaemonMessage = z.discriminatedUnion("type", [ + z.object({ type: z.literal("ready"), port: z.number(), serverId: z.string() }), + z.object({ + type: z.literal("attempt"), + requestId: z.string(), + response: z.unknown(), + sessionId: z.string(), + }), + z.object({ + type: z.literal("resolved"), + requestId: z.string(), + response: z.unknown(), + sessionId: z.string(), + pendingRequestIds: z.array(z.string()), + }), + z.object({ type: z.literal("error"), error: z.string() }), +]); + +type QuestionResponseAttempt = Extract, { type: "attempt" }>; + +type QuestionResolution = Extract, { type: "resolved" }>; + +interface QuestionRetryControl { + attempts: QuestionResponseAttempt[]; + resolutions: QuestionResolution[]; + releaseFirstResponse(): void; +} + +const retryTest = test.extend<{ + questionClient: QuestionRetryControl; +}>({ + questionClient: async ({ page }, provide, testInfo) => { + const serverDir = path.resolve(__dirname, "../../../server"); + const child = fork(path.join(serverDir, "src/server/test-utils/question-response-daemon.ts"), { + cwd: serverDir, + execArgv: ["--import", "tsx"], + stdio: ["ignore", "pipe", "pipe", "ipc"], + }); + const output: string[] = []; + child.stdout?.on("data", (data: Buffer) => output.push(data.toString())); + child.stderr?.on("data", (data: Buffer) => output.push(data.toString())); + const ready = Promise.withResolvers<{ port: number; serverId: string }>(); + const attempts: QuestionResponseAttempt[] = []; + const resolutions: QuestionResolution[] = []; + child.on("message", (message: unknown) => { + const parsed = questionDaemonMessage.parse(message); + switch (parsed.type) { + case "ready": + ready.resolve(parsed); + break; + case "attempt": + attempts.push(parsed); + break; + case "resolved": + resolutions.push(parsed); + break; + case "error": + ready.reject(new Error(parsed.error)); + break; + } + }); + child.once("exit", (code) => ready.reject(new Error(`Question daemon exited with ${code}`))); + const timeout = setTimeout( + () => ready.reject(new Error("Question daemon startup timed out")), + 20_000, + ); + try { + const daemon = await ready.promise; + clearTimeout(timeout); + const session = await seedMockAgentWorkspace({ + port: daemon.port, + repoPrefix: "question-retry-", + title: "Question response retry", + initialPrompt: "Emit synthetic questions.", + }); + try { + await page.goto("/", { waitUntil: "commit" }); + const serverId = daemon.serverId; + const host = buildSeededHost({ + serverId, + endpoint: `127.0.0.1:${daemon.port}`, + label: "Question retry host", + nowIso: new Date().toISOString(), + }); + await page.evaluate((extraHost) => { + localStorage.setItem("@paseo:e2e-extra-hosts", JSON.stringify([extraHost])); + }, host); + await page.goto(buildAgentRoute(session.workspaceId, session.agentId, serverId), { + waitUntil: "commit", + }); + await waitForQuestionPrompt(page, 120_000); + await provide({ attempts, resolutions, releaseFirstResponse: () => child.send("release") }); + } finally { + await session.cleanup(); + } + } finally { + clearTimeout(timeout); + await killProcessTree(child); + if (testInfo.status !== testInfo.expectedStatus) { + await testInfo.attach("question-daemon", { + body: output.join(""), + contentType: "text/plain", + }); + } + } + }, +}); + +retryTest( + "failed question submission preserves answers and allows retry", + async ({ page, questionClient }, testInfo) => { + retryTest.setTimeout(180_000); + const card = page.getByTestId("question-form-card"); + await chooseQuestionOption(page, "App"); + await chooseQuestionOption(page, "Behind feature flag"); + await fillQuestionAnswer(page, { + question: SUCCESS_QUESTION, + answer: "Preserve my answers after a failed submission.", + }); + await card.getByRole("button", { name: "Submit" }).click(); + await expect(card.getByRole("progressbar")).toBeVisible(); + await expectQuestionPrimaryActionDisabled(page, "Submit"); + await expect(card.getByRole("button", { name: "Dismiss" })).toBeDisabled(); + await expect(card.getByRole("tab").first()).toBeDisabled(); + await expect(card.getByRole("textbox")).not.toBeEditable(); + questionClient.releaseFirstResponse(); + await expectQuestionPrimaryActionEnabled(page, "Submit"); + await card.screenshot({ path: testInfo.outputPath("submission-failed.png") }); + await expect(card.getByRole("alert")).toHaveText("Could not send your response. Try again."); + await expect(card.getByRole("progressbar")).toHaveCount(0); + await expect(card.getByRole("button", { name: "Submit" })).toHaveText("Submit"); + await expectQuestionDismissEnabled(page); + await openQuestion(page, { index: 1, total: TOTAL_QUESTIONS }); + await expectQuestionOptionSelected(page, "App"); + await openQuestion(page, { index: 2, total: TOTAL_QUESTIONS }); + await expectQuestionOptionSelected(page, "Behind feature flag"); + await openQuestion(page, { index: 3, total: TOTAL_QUESTIONS }); + await expect(card.getByRole("textbox")).toHaveValue( + "Preserve my answers after a failed submission.", + ); + await submitQuestionAnswers(page); + await expectVisibleAgentSurfacesIdle(page); + await expect.poll(() => questionClient.attempts.length).toBe(2); + expect(questionClient.attempts[1]).toEqual(questionClient.attempts[0]); + await expect + .poll(() => questionClient.resolutions) + .toEqual([{ ...questionClient.attempts[1], type: "resolved", pendingRequestIds: [] }]); + expect(questionClient.attempts[1].response).toMatchObject({ + behavior: "allow", + updatedInput: { + answers: { + surface: "App", + rollout: "Behind feature flag", + success: "Preserve my answers after a failed submission.", + }, + }, + }); + await page.screenshot({ path: testInfo.outputPath("submission-retried.png") }); + }, +); + +retryTest( + "failed question dismissal allows an immediate retry", + async ({ page, questionClient }, testInfo) => { + retryTest.setTimeout(180_000); + const card = page.getByTestId("question-form-card"); + questionClient.releaseFirstResponse(); + await chooseQuestionOption(page, "App"); + await chooseQuestionOption(page, "Behind feature flag"); + await fillQuestionAnswer(page, { + question: SUCCESS_QUESTION, + answer: "Start with the internal team.", + }); + await card.getByRole("button", { name: "Dismiss" }).click(); + await expect(card.getByRole("alert")).toHaveText("Could not send your response. Try again."); + await expect(card.getByRole("progressbar")).toHaveCount(0); + await expect(card.getByRole("button", { name: "Dismiss" })).toHaveText("Dismiss"); + await expectQuestionDismissEnabled(page); + await expectQuestionPrimaryActionEnabled(page, "Submit"); + await card.screenshot({ path: testInfo.outputPath("dismissal-failed.png") }); + await openQuestion(page, { index: 1, total: TOTAL_QUESTIONS }); + await expectQuestionOptionSelected(page, "App"); + await openQuestion(page, { index: 3, total: TOTAL_QUESTIONS }); + await expect(card.getByRole("textbox")).toHaveValue("Start with the internal team."); + await expect(card.getByRole("alert")).toBeVisible(); + await card.getByRole("button", { name: "Dismiss" }).click(); + await expect(card).toHaveCount(0); + await expectVisibleAgentSurfacesIdle(page); + await expect.poll(() => questionClient.attempts.length).toBe(2); + expect(questionClient.attempts[1]).toEqual(questionClient.attempts[0]); + await expect + .poll(() => questionClient.resolutions) + .toEqual([{ ...questionClient.attempts[1], type: "resolved", pendingRequestIds: [] }]); + expect(questionClient.attempts[1].response).toEqual({ + behavior: "deny", + message: "Dismissed by user", + }); + await page.screenshot({ path: testInfo.outputPath("dismissal-retried.png") }); + }, +); diff --git a/packages/app/e2e/browser/sidebar-mobile-menu-sheets.spec.ts b/packages/app/e2e/browser/sidebar-mobile-menu-sheets.spec.ts index cf2f448913..7b4c36bf97 100644 --- a/packages/app/e2e/browser/sidebar-mobile-menu-sheets.spec.ts +++ b/packages/app/e2e/browser/sidebar-mobile-menu-sheets.spec.ts @@ -21,6 +21,15 @@ test("project and workspace kebabs open action sheets on compact layouts", async await page.getByRole("button", { name: "Open menu", exact: true }).click(); await waitForSidebarHydration(page); + await page.getByTestId("sidebar-display-preferences-menu").click(); + await expect(page.getByRole("menuitem")).toHaveText([ + /^Grouping/, + /^Title/, + "Show", + "Show background", + ]); + await closeMenuSheet(page); + await page .getByTestId(`sidebar-project-kebab-${projectEquivalenceViewKey(seeded.projectKey)}`) .click(); diff --git a/packages/app/e2e/browser/sidebar-project-filter.spec.ts b/packages/app/e2e/browser/sidebar-project-filter.spec.ts index df9b9afddf..a2f8d14348 100644 --- a/packages/app/e2e/browser/sidebar-project-filter.spec.ts +++ b/packages/app/e2e/browser/sidebar-project-filter.spec.ts @@ -29,6 +29,16 @@ test.describe("Sidebar project filter", () => { await expect(alphaRow).toBeVisible({ timeout: 30_000 }); await expect(betaRow).toBeVisible({ timeout: 30_000 }); + await page.getByTestId("sidebar-display-preferences-menu").click(); + await expect(page.getByRole("menuitem")).toHaveText([ + /^Grouping/, + /^Title/, + "Show", + "Project", + "Show background", + ]); + await closeSidebarDisplayPreferences(page); + await openSidebarProjectFilter(page); await expect(page.getByTestId("sidebar-project-filter-all")).toBeVisible(); await toggleProjectFilter(page, alpha.projectKey); diff --git a/packages/app/e2e/browser/terminal-profile-icons.spec.ts b/packages/app/e2e/browser/terminal-profile-icons.spec.ts new file mode 100644 index 0000000000..29d786cfd3 --- /dev/null +++ b/packages/app/e2e/browser/terminal-profile-icons.spec.ts @@ -0,0 +1,71 @@ +import { ACP_PROVIDER_ICON_SVGS } from "../../src/assets/acp-provider-icons"; +import { expect, test, type Page } from "../support/fixtures"; +import { gotoAppShell } from "../support/helpers/app"; +import { openNewWorkspaceComposer } from "../support/helpers/new-workspace"; +import { openLaunchMenu } from "../support/helpers/new-workspace-launch"; +import { seedWorkspace } from "../support/helpers/seed-client"; +import { waitForSidebarHydration } from "../support/helpers/workspace-ui"; + +// Harmless commands: this journey opens the profile menu without launching them. +test.use({ + e2eDaemonConfig: { + version: 1, + daemon: { + terminalProfiles: [ + { id: "antigravity", name: "Antigravity artwork", command: "/bin/sh", icon: "agy" }, + { id: "cursor", name: "Cursor artwork", command: "/bin/sh", icon: "cursor" }, + { id: "unknown", name: "Unknown artwork", command: "/bin/sh", icon: "unknown-icon" }, + ], + }, + }, +}); + +function profileIcon(page: Page, name: string) { + return page.getByRole("menuitem", { name: new RegExp(`^${name} artwork`) }).locator("svg"); +} + +async function expectBundledArtwork(page: Page, name: string, artwork: string) { + const icon = profileIcon(page, name); + await expect(icon).toBeVisible(); + const expectedPaths = await page.evaluate((xml) => { + const document = new DOMParser().parseFromString(xml, "image/svg+xml"); + return Array.from(document.querySelectorAll("path"), (path) => path.getAttribute("d")!); + }, artwork); + await expect(icon.locator("path")).toHaveCount(expectedPaths.length); + for (const [index, path] of expectedPaths.entries()) { + await expect(icon.locator("path").nth(index)).toHaveAttribute("d", path); + } + await expect(icon).toHaveAttribute("width", "14"); + await expect(icon).toHaveAttribute("height", "14"); + const terminalColor = await page + .getByRole("menuitem", { name: "Terminal", exact: true }) + .locator("svg") + .evaluate((element) => getComputedStyle(element).stroke); + await expect(icon.locator("path").first()).toHaveCSS("fill", terminalColor); +} + +async function expectUnknownArtworkFallback(page: Page) { + const icon = profileIcon(page, "Unknown"); + await expect(icon).toBeVisible(); + await expect(icon.locator('path[d="M12 8V4H8"]')).toHaveCount(1); +} + +test("terminal profiles show bundled artwork while unknown icons keep the Bot fallback", async ({ + page, +}) => { + const workspace = await seedWorkspace({ repoPrefix: "terminal-icons-" }); + try { + await gotoAppShell(page); + await waitForSidebarHydration(page); + await openNewWorkspaceComposer(page, { + projectKey: workspace.projectKey, + projectDisplayName: workspace.projectDisplayName, + }); + await openLaunchMenu(page); + await expectBundledArtwork(page, "Antigravity", ACP_PROVIDER_ICON_SVGS.agy); + await expectBundledArtwork(page, "Cursor", ACP_PROVIDER_ICON_SVGS.cursor); + await expectUnknownArtworkFallback(page); + } finally { + await workspace.cleanup(); + } +}); diff --git a/packages/app/e2e/mobile/agent-device/native-composer-fullscreen.ios.ad b/packages/app/e2e/mobile/agent-device/native-composer-fullscreen.ios.ad new file mode 100644 index 0000000000..d991b227b7 --- /dev/null +++ b/packages/app/e2e/mobile/agent-device/native-composer-fullscreen.ios.ad @@ -0,0 +1,18 @@ +context platform=ios timeout=60000 retries=0 +# Start with an empty composer in an idle chat or new-agent draft on an iPhone with the software keyboard enabled. +# Expand a long draft with the keyboard open; Exit fullscreen must remain reachable. +open sh.paseo.debug +wait "editable=true" 60000 +fill "editable=true" "iOS fullscreen composer smoke.\n\nLine 1: Check that the composer keeps this draft when expanded and collapsed.\nLine 2: Check that the composer keeps this draft when expanded and collapsed.\nLine 3: Check that the composer keeps this draft when expanded and collapsed.\nLine 4: Check that the composer keeps this draft when expanded and collapsed.\nLine 5: Check that the composer keeps this draft when expanded and collapsed.\nLine 6: Check that the composer keeps this draft when expanded and collapsed.\nLine 7: Check that the composer keeps this draft when expanded and collapsed.\nLine 8: Check that the composer keeps this draft when expanded and collapsed.\nLine 9: Check that the composer keeps this draft when expanded and collapsed.\nLine 10: Check that the composer keeps this draft when expanded and collapsed.\nLine 11: Check that the composer keeps this draft when expanded and collapsed.\nLine 12: Check that the composer keeps this draft when expanded and collapsed.\nLine 13: Check that the composer keeps this draft when expanded and collapsed.\nLine 14: Check that the composer keeps this draft when expanded and collapsed." +press "label=\"Fullscreen\"" +wait "label=\"Exit fullscreen\" role=button" 5000 +is visible "label=\"Exit fullscreen\" role=button" +press "label=\"Exit fullscreen\" role=button" +wait "label=\"Fullscreen\" role=button" 5000 +is visible "label=\"Fullscreen\" role=button" +press "label=\"Fullscreen\"" +wait "label=\"Exit fullscreen\" role=button" 5000 +is visible "label=\"Exit fullscreen\" role=button" +press "label=\"Exit fullscreen\" role=button" +wait "label=\"Fullscreen\" role=button" 5000 +is visible "label=\"Fullscreen\" role=button" diff --git a/packages/app/e2e/mobile/agent-device/native-sidebar-drag-release.ios.ad b/packages/app/e2e/mobile/agent-device/native-sidebar-drag-release.ios.ad new file mode 100644 index 0000000000..c94e24914d --- /dev/null +++ b/packages/app/e2e/mobile/agent-device/native-sidebar-drag-release.ios.ad @@ -0,0 +1,13 @@ +context platform=ios timeout=60000 retries=0 +env APP_ID=sh.paseo.debug + +# Start with the sidebar closed. WORKSPACE_ROW is the testID of a visible, +# unselected workspace row. Hold past drag arming (180ms), below menu opening (450ms). +open "${APP_ID}" +press "label=\"Open menu\"" +wait "id=\"${WORKSPACE_ROW}\"" 10000 +longpress "id=\"${WORKSPACE_ROW}\"" 250 +press "id=\"${WORKSPACE_ROW}\"" +# A released drag must not swallow the next tap: navigation closes the sidebar. +wait "label=\"Open menu\"" 5000 +close diff --git a/packages/app/e2e/support/helpers/acp-streaming-markdown.ts b/packages/app/e2e/support/helpers/acp-streaming-markdown.ts index 0ebe9c2b3d..d7ca2ee183 100644 --- a/packages/app/e2e/support/helpers/acp-streaming-markdown.ts +++ b/packages/app/e2e/support/helpers/acp-streaming-markdown.ts @@ -52,13 +52,11 @@ export async function expectAcpMarkdown(page: Page, count: number): Promise boolean): Promise => { + while (!done(forwardedText)) { + const frame = pending.shift(); + if (!frame) { + await new Promise((resolve) => { + notifyFrame = resolve; + }); + continue; + } + forwardedText += frame.text; + frame.forward(); + } + }; + return { waitForInitialTimeline: () => initialTimeline, + showUntil: (text: string) => forwardUntil((forwarded) => forwarded.includes(text)), async showThrough(prefix: string): Promise { - while (forwardedText.length < prefix.length) { - const frame = pending.shift(); - if (!frame) { - await new Promise((resolve) => { - notifyFrame = resolve; - }); - continue; - } - forwardedText += frame.text; - frame.forward(); - } + await forwardUntil((text) => text.length >= prefix.length); if (forwardedText !== prefix) { throw new Error( `Stream did not stop at ${JSON.stringify(prefix)}: ${JSON.stringify(forwardedText)}`, diff --git a/packages/app/e2e/support/helpers/assistant-images.ts b/packages/app/e2e/support/helpers/assistant-images.ts index c4200c7c75..3d930b41a6 100644 --- a/packages/app/e2e/support/helpers/assistant-images.ts +++ b/packages/app/e2e/support/helpers/assistant-images.ts @@ -26,16 +26,13 @@ export interface AssistantImageFixture { export async function createSmallAssistantPng( workspace: SeededWorkspace, - input: { alt: string; fileName: string }, + input: { alt: string; fileName: string; width?: number; height?: number }, ): Promise { - await writeFile(path.join(workspace.repoPath, input.fileName), SMALL_PNG); - return { - alt: input.alt, - height: 1, - relativePath: input.fileName, - size: SMALL_PNG.byteLength, - width: 1, - }; + const width = input.width ?? 1; + const height = input.height ?? 1; + const bytes = width === 1 && height === 1 ? SMALL_PNG : encodeDeterministicPng(width, height); + await writeFile(path.join(workspace.repoPath, input.fileName), bytes); + return { alt: input.alt, height, relativePath: input.fileName, size: bytes.byteLength, width }; } export async function createNearTenMegabyteAssistantPng( @@ -61,6 +58,7 @@ export async function createNearTenMegabyteAssistantPng( export async function createSettledMockAgent( workspace: SeededWorkspace, title: string, + markdown?: string, ): Promise { const agent = await workspace.client.createAgent({ provider: "mock", @@ -69,6 +67,7 @@ export async function createSettledMockAgent( cwd: workspace.repoPath, workspaceId: workspace.workspaceId, title, + featureValues: markdown === undefined ? undefined : { mockAssistantResponse: markdown }, }); await workspace.client.waitForAgentUpsert( agent.id, @@ -124,7 +123,7 @@ export async function expectAssistantImageRendered( page: Page, image: AssistantImageFixture, ): Promise { - const rendered = page.getByRole("img", { name: image.alt }).first(); + const rendered = page.getByRole("button", { name: image.alt }).first(); await expect(rendered).toBeVisible({ timeout: 30_000 }); await expect .poll(async () => @@ -172,14 +171,14 @@ export async function expectAssistantImageNotMounted( page: Page, image: AssistantImageFixture, ): Promise { - await expect(page.getByRole("img", { name: image.alt })).toHaveCount(0); + await expect(page.getByRole("button", { name: image.alt })).toHaveCount(0); } export async function userPagesUntilAssistantImageRenders( page: Page, image: AssistantImageFixture, ): Promise { - const rendered = page.getByRole("img", { name: image.alt }); + const rendered = page.getByRole("button", { name: image.alt }); for (let attempt = 0; attempt < 10; attempt += 1) { if ((await rendered.count()) > 0) { await expectAssistantImageRendered(page, image); @@ -253,8 +252,8 @@ async function beginVisibleImageStabilityObservation( ); const imageTab = document.querySelector(`[data-testid="${tabTestId}"]`); if (imageTab?.getAttribute("aria-selected") !== "true") return; - const imageVisible = Array.from(document.querySelectorAll('[role="img"]')).some( - (element) => element.getAttribute("aria-label") === accessibleName && isVisible(element), + const imageVisible = Array.from(document.querySelectorAll("img[alt]")).some( + (element) => element.getAttribute("alt") === accessibleName && isVisible(element), ); record.missingSeen ||= !imageVisible; }; diff --git a/packages/app/e2e/support/helpers/streaming-markdown.ts b/packages/app/e2e/support/helpers/streaming-markdown.ts index e628d80471..91bf9be172 100644 --- a/packages/app/e2e/support/helpers/streaming-markdown.ts +++ b/packages/app/e2e/support/helpers/streaming-markdown.ts @@ -45,11 +45,8 @@ export async function requestStreamingMarkdown(agent: StreamingMarkdownAgent): P export async function expectUnfinishedBold(page: Page): Promise { const message = page.getByTestId("assistant-message").last(); - const bold = message.locator('[data-paseo-markdown-tag="strong"]'); - await expect(bold).toContainText("Bold"); + await expect(message.locator(".enrm-text")).toContainText("Bold"); await expect(message).not.toContainText("stays bold"); - await expect(bold).toHaveCSS("font-weight", "500"); - await expect(message).not.toContainText("*"); } export async function expectUnfinishedLink( @@ -61,7 +58,6 @@ export async function expectUnfinishedLink( await agent.stream.showThrough("**Bold text stays bold** and [Paseo docs"); await expect(message).toContainText("Paseo docs"); await expect(message.getByRole("link", { name: "Paseo docs" })).toHaveCount(0); - await expect(message).not.toContainText("["); await expect(message).not.toContainText("https:"); await captureMarkdown(page, testInfo, "unfinished-link"); } @@ -95,8 +91,5 @@ async function expectCompletedMarkdown(page: Page): Promise { await expect( message.getByRole("link", { name: "Paseo docs" }).and(message.locator("a")), ).toHaveAttribute("href", "https://example.com/documentation"); - await expect(message.locator('[data-paseo-markdown-tag="strong"]')).toHaveCSS( - "font-weight", - "500", - ); + await expect(message.locator("strong")).toHaveCSS("font-weight", "700"); } diff --git a/packages/app/e2e/support/helpers/timeline-scroll-smoothness.ts b/packages/app/e2e/support/helpers/timeline-scroll-smoothness.ts index f7b62fd953..41f8cf3279 100644 --- a/packages/app/e2e/support/helpers/timeline-scroll-smoothness.ts +++ b/packages/app/e2e/support/helpers/timeline-scroll-smoothness.ts @@ -1,8 +1,9 @@ import { writeFile } from "node:fs/promises"; import { createServer } from "node:http"; import { expect, type Page, type TestInfo } from "@playwright/test"; -import { seedMockAgentWorkspace, type MockAgentWorkspace } from "./mock-agent"; +import { seedMockAgentWorkspace, openAgentRoute, type MockAgentWorkspace } from "./mock-agent"; import { openAgentTimeline, expectTimelinePromptVisible } from "./timeline-pagination"; +import { holdAssistantStream } from "./agent-timeline-gate"; export const scrollCadences = [ { name: "slow", delta: 160, intervalMs: 50, steps: 1000 }, @@ -44,6 +45,120 @@ interface TimelinePage { at: number; } +export async function withStreamingReader(run: (agent: MockAgentWorkspace) => Promise) { + const section = "Read this paragraph while the response continues below. ".repeat(8); + const response = [ + "```mermaid\nflowchart LR\n Start --> Review --> Done\n```", + "```mermaid\nflowchart LR\n Nested --> Finished\n```", + ...Array.from({ length: 40 }, (_, index) => `## Long section ${index}\n\n${section}`), + "READER-STREAM-SETTLED", + ].join("\n\n"); + const agent = await seedMockAgentWorkspace({ + repoPrefix: "held-stream-reader-", + title: "Held streaming paragraph", + featureValues: { + mockStreamingAssistantResponse: response, + mockStreamingAssistantIntervalMs: 1, + }, + }); + try { + await run(agent); + } finally { + await agent.cleanup(); + } +} + +export async function holdParagraphThroughCompletion(page: Page, agent: MockAgentWorkspace) { + const stream = await holdAssistantStream(page, agent.agentId); + await openAgentRoute(page, agent); + await page.getByRole("textbox", { name: "Message agent...", exact: true }).waitFor(); + const scroll = page.getByTestId("agent-chat-scroll"); + const following = await scroll.evaluateHandle((viewport) => { + const result = { stopped: false, maxGap: 0 }; + const frame = () => { + if (result.stopped) return; + result.maxGap = Math.max( + result.maxGap, + viewport.scrollHeight - viewport.clientHeight - viewport.scrollTop, + ); + requestAnimationFrame(frame); + }; + requestAnimationFrame(frame); + return result; + }); + await agent.client.sendAgentMessage(agent.agentId, "Stream the mixed response."); + await stream.showUntil("Long section 12\n"); + await page + .getByRole("heading", { name: "Long section 12", exact: true }) + .waitFor({ state: "attached" }); + // Exercise a reader whose producer has already finished; delivery must be gated. + await agent.client.waitForFinish(agent.agentId, 30_000); + await expect(page.getByTestId("mermaid-viewport")).toHaveCount(2); + await expect(page.getByTestId("mermaid-viewport").nth(0)).toHaveCSS("opacity", "1"); + await expect(page.getByTestId("mermaid-viewport").nth(1)).toHaveCSS("opacity", "1"); + const maxFollowGap = await following.evaluate((result) => { + result.stopped = true; + return result.maxGap; + }); + const bounds = await scroll.boundingBox(); + if (!bounds) throw new Error("Reader viewport is missing"); + await page.mouse.move(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2); + await page.mouse.wheel(0, -1600); + await page.waitForTimeout(150); + const paragraph = page.getByRole("heading", { name: "Long section 3", exact: true }); + await paragraph.waitFor({ state: "attached" }); + const recording = await paragraph.evaluateHandle((heading) => { + if (!document.querySelector('[data-testid="turn-working-indicator"]')) { + throw new Error("The stream completed before the reader detached"); + } + const viewport = document.querySelector('[data-testid="agent-chat-scroll"]')!; + const clip = viewport.getBoundingClientRect(); + viewport.scrollTop += heading.getBoundingClientRect().y - (clip.top + clip.height / 2); + const baseline = heading.getBoundingClientRect().y; + const initialTextLength = heading.closest('[data-testid="assistant-message"]')!.textContent! + .length; + const result = { maxDrift: 0, retained: true, textGrowth: 0, done: false }; + let stableAt = performance.now(); + let signature = ""; + const frame = (now: number) => { + result.retained &&= heading.isConnected; + const current = [...document.querySelectorAll("h2")].find( + (n) => n.textContent === "Long section 3", + ); + const textLength = + current?.closest('[data-testid="assistant-message"]')?.textContent?.length ?? 0; + result.textGrowth = Math.max(result.textGrowth, textLength - initialTextLength); + if (current) + result.maxDrift = Math.max( + result.maxDrift, + Math.abs(current.getBoundingClientRect().y - baseline), + ); + const next = `${viewport.scrollHeight}:${viewport.scrollTop}:${current?.getBoundingClientRect().y}`; + if (next !== signature) { + signature = next; + stableAt = now; + } + const complete = + document.body.textContent?.includes("Worked for") && + document.body.textContent.includes("READER-STREAM-SETTLED") && + !document.querySelector('[data-testid="turn-working-indicator"]'); + if (complete && now - stableAt >= 750) { + result.done = true; + return; + } + requestAnimationFrame(frame); + }; + requestAnimationFrame(frame); + return result; + }); + await expect( + page.getByTestId("scroll-to-bottom-button").filter({ visible: true }).first(), + ).toBeVisible(); + stream.release(); + await page.waitForFunction((result) => result.done, recording); + return { ...(await recording.jsonValue()), maxFollowGap }; +} + /** Real HTTP image responses, with intrinsic dimensions unavailable until the response arrives. */ export async function withVariedTimeline( run: (agent: MockAgentWorkspace, newestPrompt: string) => Promise, @@ -495,7 +610,7 @@ export async function expectImageSpaceReserved(page: Page, testInfo: TestInfo): await agent.client.waitForFinish(agent.agentId, 15_000); await openAgentTimeline(page, agent); await expectTimelinePromptVisible(page, "Show the delayed image"); - const image = page.getByRole("img", { name: "Reserved image" }).first(); + const image = page.getByRole("button", { name: "Reserved image", exact: true }).first(); await expect(image).toBeVisible(); const before = await image.boundingBox(); await attachTimelineScreenshot(page, testInfo, "image-placeholder"); @@ -512,8 +627,9 @@ export async function expectImageSpaceReserved(page: Page, testInfo: TestInfo): .toBe(240); await expect .poll(async () => { - const box = await image.boundingBox(); - return box ? Math.round((box.width / box.height) * 100) : 0; + return image.evaluate((element: HTMLImageElement) => + Math.round((element.naturalWidth / element.naturalHeight) * 100), + ); }) .toBe(150); const after = await image.boundingBox(); diff --git a/packages/app/package.json b/packages/app/package.json index 84af04dee4..670e0d93ab 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/app", - "version": "0.11.2", + "version": "0.12.0-beta.1", "private": true, "main": "index.ts", "scripts": { @@ -104,7 +104,11 @@ "i18next": "^26.3.0", "lucide-react-native": "^1.50.0", "markdown-it": "^10.0.0", + "mdast-util-from-markdown": "2.0.3", + "mdast-util-gfm-table": "^2.0.0", "mermaid": "^11.16.0", + "micromark-extension-gfm-table": "^2.1.2", + "micromark-util-decode-string": "2.0.1", "mnemonic-id": "^3.2.7", "qrcode": "^1.5.4", "react": "19.1.0", @@ -114,15 +118,14 @@ "react-native": "0.81.5", "react-native-draggable-flatlist": "^4.0.3", "react-native-edge-to-edge": "^1.7.0", + "react-native-enriched-markdown": "1.2.0-nightly-20261014-766fc0e67", "react-native-gesture-handler": "~2.28.0", "react-native-keyboard-controller": "1.21.12", - "react-native-markdown-display": "^7.0.2", "react-native-nitro-modules": "0.35.5", "react-native-reanimated": "~4.3.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", "react-native-svg": "^15.14.0", - "react-native-uitextview": "^2.2.0", "react-native-unistyles": "^3.2.4", "react-native-web": "~0.21.0", "react-native-webview": "^13.16.0", @@ -164,6 +167,32 @@ "wrangler": "^4.105.0", "ws": "^8.20.0" }, + "enriched-markdown": { + "enableMath": false, + "enableCodeHighlight": true, + "codeHighlightLanguages": [ + "json", + "html", + "css", + "markdown", + "yaml", + "go", + "java", + "javascript", + "python", + "c", + "rust", + "bash", + "typescript", + "tsx", + "cpp", + "swift", + "php", + "ruby", + "c-sharp" + ], + "enableVideo": false + }, "reanimated": { "staticFeatureFlags": { "FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS": false diff --git a/packages/app/scripts/generate-markdown-image-placeholder.mjs b/packages/app/scripts/generate-markdown-image-placeholder.mjs new file mode 100644 index 0000000000..4e147161e4 --- /dev/null +++ b/packages/app/scripts/generate-markdown-image-placeholder.mjs @@ -0,0 +1,27 @@ +// Regenerate from the repo root: node packages/app/scripts/generate-markdown-image-placeholder.mjs +// sharp is already provided by the development dependency tree; it is not used at runtime. +import { readFile, writeFile } from "node:fs/promises"; +import sharp from "sharp"; + +// Lucide Image (ISC), lucide-react-native/dist/esm/icons/image.mjs. +const svg = ` + + + + + + + +`; +const png = await sharp(Buffer.from(svg)).png({ compressionLevel: 9 }).toBuffer(); +const uri = `data:image/png;base64,${png.toString("base64")}`; +const sourceFile = new URL("../src/components/markdown/source.ts", import.meta.url); +const source = await readFile(sourceFile, "utf8"); +await writeFile( + sourceFile, + source.replace( + /export const PENDING_MARKDOWN_IMAGE =\s*"[^"]*";/, + `export const PENDING_MARKDOWN_IMAGE =\n "${uri}";`, + ), +); +console.log(`Generated 640×640 Markdown image placeholder: ${png.length} bytes`); diff --git a/packages/app/src/agent-stream/chat-find/ranges.browser.test.ts b/packages/app/src/agent-stream/chat-find/ranges.browser.test.ts index c1d84c1b42..631b8f1d27 100644 --- a/packages/app/src/agent-stream/chat-find/ranges.browser.test.ts +++ b/packages/app/src/agent-stream/chat-find/ranges.browser.test.ts @@ -65,3 +65,14 @@ it("matches whitespace across text nodes without inspecting hidden link destinat "world\nline", ]); }); + +it("keeps semantic Markdown paragraphs separate while finding inline matches", () => { + const row = content( + "

İ😀hello world; hello world.

another paragraph

", + ); + expect(findRenderedMatches(row, "hello world").map((range) => range.toString())).toEqual([ + "hello world", + "hello world", + ]); + expect(findRenderedMatches(row, ".another")).toEqual([]); +}); diff --git a/packages/app/src/agent-stream/chat-find/ranges.web.ts b/packages/app/src/agent-stream/chat-find/ranges.web.ts index 91ed7bff6a..ea2e901c2d 100644 --- a/packages/app/src/agent-stream/chat-find/ranges.web.ts +++ b/packages/app/src/agent-stream/chat-find/ranges.web.ts @@ -1,10 +1,10 @@ const BLOCKS = ["p", "pre", "li", "td", "th", "h1", "h2", "h3", "h4", "h5", "h6"] - .map((tag) => `[data-paseo-markdown-tag="${tag}"]`) + .flatMap((tag) => [tag, `[data-paseo-markdown-tag="${tag}"]`]) .join(","); const IGNORED = '[data-paseo-markdown-ignore="true"], [aria-hidden="true"], button, [role="button"], svg, script, style'; -/** The rows of one message, in reading order. Each is one Markdown block. */ +/** Rendered rows carrying a source message identity, in reading order. */ export function findMessageRows(root: HTMLElement | null, messageId: string): HTMLElement[] { if (!root) return []; return Array.from( diff --git a/packages/app/src/agent-stream/chat-find/viewport.browser.test.ts b/packages/app/src/agent-stream/chat-find/viewport.browser.test.ts index 2bd1e291b4..a7cf29994c 100644 --- a/packages/app/src/agent-stream/chat-find/viewport.browser.test.ts +++ b/packages/app/src/agent-stream/chat-find/viewport.browser.test.ts @@ -98,3 +98,23 @@ function highlightedRow(): string | undefined { ); return row?.dataset.historyRowId; } + +it("waits for an asynchronous renderer before counting its matches", async () => { + const root = transcript("message-1", ["needle"]); + const content = root.querySelector('[data-message-text="true"]')!; + content.setAttribute("aria-busy", "true"); + let settled = false; + const reveal = viewport(root, {}) + .reveal("message-1", "needle", 0, new AbortController().signal) + .then((result) => { + settled = true; + return result; + }); + await new Promise((resolve) => + requestAnimationFrame(() => requestAnimationFrame(() => resolve())), + ); + expect(settled).toBe(false); + content.textContent = "needle and another needle"; + content.removeAttribute("aria-busy"); + expect(await reveal).toEqual({ occurrence: 0, count: 2 }); +}); diff --git a/packages/app/src/agent-stream/chat-find/viewport.web.ts b/packages/app/src/agent-stream/chat-find/viewport.web.ts index 6bbb3a704b..4fef1f6abc 100644 --- a/packages/app/src/agent-stream/chat-find/viewport.web.ts +++ b/packages/app/src/agent-stream/chat-find/viewport.web.ts @@ -21,8 +21,7 @@ export function createFindViewport({ clear() { CSS.highlights.delete(highlightName); }, - // A message renders as one row per Markdown block, so an occurrence can sit in - // any of them and the chosen one decides where the viewport lands. + // The source message owns navigation; the rendered occurrence owns its position. reveal(messageId, query, occurrence, signal) { return new Promise((resolve, reject) => { let frame = 0; @@ -61,6 +60,10 @@ export function createFindViewport({ frame = requestAnimationFrame(poll); return; } + if (rows.some((row) => row.querySelector('[aria-busy="true"]'))) { + frame = requestAnimationFrame(poll); + return; + } const ranges = findMessageMatches(getRoot(), messageId, query); const index = occurrence < 0 ? ranges.length - 1 : Math.min(occurrence, ranges.length - 1); diff --git a/packages/app/src/agent-stream/footer-spacing.test.ts b/packages/app/src/agent-stream/footer-spacing.test.ts index a2414fff5c..5fa3535bce 100644 --- a/packages/app/src/agent-stream/footer-spacing.test.ts +++ b/packages/app/src/agent-stream/footer-spacing.test.ts @@ -49,17 +49,6 @@ function assistantMessage(id: string): Extract { - return { - ...assistantMessage(id), - blockGroupId: "response", - blockIndex, - }; -} - function timingFor(assistantId: string): Map { return new Map([ [ @@ -109,30 +98,4 @@ describe("turn footer spacing", () => { expect(assistantLayout?.assistantSpacing).toBe("compactBottom"); }, ); - - it.each([ - ["web", false], - ["web", true], - ["native", false], - ["native", true], - ] as const)( - "compacts both edges of the final split paragraph on %s when active=%s", - (platform, isTurnActive) => { - const strategy = strategyFor(platform); - const first = assistantBlock("first", 0); - const last = assistantBlock("last", 1); - const layout = layoutStream({ - strategy, - isTurnActive, - history: strategy.orderTail([userMessage("user"), first]), - liveHead: strategy.orderHead([last]), - timingByAssistantId: timingFor(last.id), - }); - const firstLayout = layout.history.find((item) => item.item.id === first.id); - const lastLayout = layout.liveHead.find((item) => item.item.id === last.id); - - expect(firstLayout?.assistantSpacing).toBe("compactBottom"); - expect(lastLayout?.assistantSpacing).toBe("compactBoth"); - }, - ); }); diff --git a/packages/app/src/agent-stream/layout.test.ts b/packages/app/src/agent-stream/layout.test.ts index 3ab2a9f8f0..f10fde2cb1 100644 --- a/packages/app/src/agent-stream/layout.test.ts +++ b/packages/app/src/agent-stream/layout.test.ts @@ -25,7 +25,7 @@ function userMessage(id: string, seed: number): Extract { return { @@ -33,7 +33,6 @@ function assistantMessage( id, text: id, timestamp: timestamp(seed), - ...(block ? { blockGroupId: block.groupId, blockIndex: block.index } : {}), ...(turnId ? { turnId } : {}), }; } @@ -318,7 +317,7 @@ describe("layoutStream", () => { expect(assistantRow.frameOrder).toBe("content-then-footer"); }); - it("compacts assistant block spacing across the history and live-head boundary", () => { + it("keeps separate messages spaced across history and the live head", () => { const historyBlock = assistantMessage("turn:block:0", 2, { groupId: "turn", index: 0 }); const headBlock = assistantMessage("turn:head", 3, { groupId: "turn", index: 1 }); const layout = layoutFor({ @@ -328,8 +327,8 @@ describe("layoutStream", () => { timingIds: [historyBlock.id, headBlock.id], }); - expect(findLayoutItem(layout, historyBlock.id).assistantSpacing).toBe("compactBottom"); - expect(findLayoutItem(layout, headBlock.id).assistantSpacing).toBe("compactBoth"); + expect(findLayoutItem(layout, historyBlock.id).assistantSpacing).toBe("default"); + expect(findLayoutItem(layout, headBlock.id).assistantSpacing).toBe("compactBottom"); }); it.each(["web", "android"] as const)( diff --git a/packages/app/src/agent-stream/layout.ts b/packages/app/src/agent-stream/layout.ts index d460c1fa2e..82ef98db54 100644 --- a/packages/app/src/agent-stream/layout.ts +++ b/packages/app/src/agent-stream/layout.ts @@ -1,6 +1,6 @@ import type { TurnTiming } from "@/timeline/turn-time"; import type { StreamItem } from "@/types/stream"; -import { getAssistantBlockSpacing, getGapBetweenStreamItems } from "./spacing"; +import { getAssistantMessageSpacing, getGapBetweenStreamItems } from "./spacing"; import type { StreamFrameChildOrder, StreamStrategy } from "./strategy"; import { continuesResponse, continuesTurn, isResponseBoundary } from "./turn-membership"; @@ -311,10 +311,8 @@ function layoutSegmentItem( boundaryAboveItems: input.boundaryAboveItems, boundaryAboveIndex: input.boundaryAboveIndex, }); - const assistantSpacing = getAssistantBlockSpacing({ + const assistantSpacing = getAssistantMessageSpacing({ item, - aboveItem, - belowItem, hasFooterBelow: completedFooter !== null || (input.hasAuxiliaryFooter && belowItem === null), }); diff --git a/packages/app/src/agent-stream/presentation.test.ts b/packages/app/src/agent-stream/presentation.test.ts index 70ed8862ae..dcb52fd24b 100644 --- a/packages/app/src/agent-stream/presentation.test.ts +++ b/packages/app/src/agent-stream/presentation.test.ts @@ -1,7 +1,5 @@ -import { QueryClient } from "@tanstack/react-query"; -import { describe, expect, it } from "vitest"; -import type { AgentStreamEventPayload } from "@getpaseo/protocol/messages"; import { runPluginClientBundle, type PluginClientRuntime } from "@/plugins/evaluate"; +import { transformTimelineItem, type TimelineItemTransform } from "@/plugins/timeline/model"; import type { InstalledPlugin } from "@/plugins/types"; import { applyStreamEvent, @@ -10,9 +8,11 @@ import { type ToolCallItem, type UserMessageItem, } from "@/types/stream"; -import { transformTimelineItem, type TimelineItemTransform } from "@/plugins/timeline/model"; -import { createStreamPresentation, getStreamItemMessageId } from "./presentation"; +import type { AgentStreamEventPayload } from "@getpaseo/protocol/messages"; +import { QueryClient } from "@tanstack/react-query"; +import { describe, expect, it } from "vitest"; import { buildAgentStreamRenderModel } from "./model"; +import { createStreamPresentation, getStreamItemMessageId } from "./presentation"; const runtime = { paseo: {}, @@ -123,6 +123,25 @@ function rows(result: { tail: StreamItem[]; head: StreamItem[] }): StreamItem[] return [...result.tail, ...result.head]; } +describe("whole-message Markdown presentation", () => { + it("keeps paragraphs, fences and quotes in one progressively updated row", () => { + const present = createStreamPresentation(); + const text = "First paragraph.\n\nSecond paragraph.\n\n```ts\nconst n = 1;\n```\n\n> Quote"; + for (const length of [10, 30, text.length]) { + const source = hydrateStreamState([ + { event: assistant(text.slice(0, length)), timestamp: new Date(1000) }, + ]); + const result = present({ + ...presentationOptions, + tail: [], + head: source, + transform: undefined, + }); + expect(rows(result)).toEqual(source); + } + }); +}); + describe("stream presentation through installed plugins", () => { it("offers every source tool call to an installed transformer in Overview mode", () => { const calls = [toolCall("call-1", "bash"), toolCall("call-2", "read")]; @@ -198,26 +217,17 @@ describe("stream presentation through installed plugins", () => { { text: "Header\n\nBody", phase: "streaming" }, ]); expect(rows(harness.render(transform))).toHaveLength(1); - expect(rows(harness.render()).map((item) => item.kind)).toEqual([ - "assistant_message", - "assistant_message", - ]); + expect(rows(harness.render()).map((item) => item.kind)).toEqual(["assistant_message"]); }); - it("keeps completed native blocks stable while the last block grows and finishes", () => { + it("retains the complete message when streaming finishes", () => { const harness = streamHarness(); - const first = harness.send(assistant("Intro\n\n![Image](image.png)")); - const growing = harness.send(assistant("\n\nAfter\n")); - const last = harness.send(assistant("- item")); + harness.send(assistant("Intro\n\n![Image](image.png)")); + const last = harness.send(assistant("\n\nAfter\n- item")); const completed = harness.send({ type: "turn_completed", provider: "claude" }); - expect(first.tail).toMatchObject([{ text: "Intro", blockIndex: 0 }]); - expect(first.head).toMatchObject([{ text: "![Image](image.png)", blockIndex: 1 }]); - expect(growing.tail[0]).toBe(first.tail[0]); - expect(growing.tail[1]).toBe(first.head[0]); - expect(last.tail).toBe(growing.tail); - expect(last.head).toMatchObject([{ text: "After\n- item", blockIndex: 2 }]); - expect(completed.tail).toEqual(rows(last)); - expect(completed.tail[2]).toBe(last.head[0]); + expect(last.tail).toEqual([]); + expect(last.head).toMatchObject([{ text: "Intro\n\n![Image](image.png)\n\nAfter\n- item" }]); + expect(completed.tail).toEqual(last.head); expect(completed.head).toEqual([]); }); @@ -225,8 +235,8 @@ describe("stream presentation through installed plugins", () => { const harness = streamHarness(); harness.send(assistant("Intro\n\n```ts\nconst a = 1;")); const result = harness.send(assistant("\n\nconst b = 2;")); - expect(result.tail).toMatchObject([{ text: "Intro" }]); - expect(result.head).toMatchObject([{ text: "```ts\nconst a = 1;\n\nconst b = 2;" }]); + expect(result.tail).toEqual([]); + expect(result.head).toMatchObject([{ text: "Intro\n\n```ts\nconst a = 1;\n\nconst b = 2;" }]); }); // Codex streams a line break and the next line's indent as their own chunks. @@ -244,14 +254,13 @@ describe("stream presentation through installed plugins", () => { // which is what lets find, scroll-to-message and history reveal address it by id. // A link reference definition stays with the paragraph that uses it, so the split // never leaves an empty row behind and the reference still resolves. - it("splits an assistant message into the same blocks through history and through the live head", () => { + it("preserves source identity through history and the live head", () => { const source = hydrateStreamState([ { event: assistant("[Link][docs]\n\n[docs]: https://example.com\n\nClosing paragraph."), timestamp: new Date(1000), }, ]); - const messageId = source[0]!.id; const present = (placement: "tail" | "head") => rows( createStreamPresentation()({ @@ -261,20 +270,7 @@ describe("stream presentation through installed plugins", () => { transform: undefined, }), ); - expect(present("tail")).toMatchObject([ - { - id: `${messageId}:block:0`, - blockGroupId: messageId, - blockIndex: 0, - text: "[Link][docs]\n\n[docs]: https://example.com", - }, - { - id: `${messageId}:block:1`, - blockGroupId: messageId, - blockIndex: 1, - text: "Closing paragraph.", - }, - ]); + expect(present("tail")).toEqual(source); const blocks = (items: StreamItem[]) => items.map((item) => [item.id, item.kind === "assistant_message" ? item.text : null]); expect(blocks(present("head"))).toEqual(blocks(present("tail"))); @@ -282,7 +278,7 @@ describe("stream presentation through installed plugins", () => { // Rows are addressed by id from outside presentation, so the same text has to land on // the same ids whether the reader watched it arrive or opened the chat afterwards. - it("gives a streamed message the block ids its hydrated text would have had", () => { + it("gives a streamed message the same id as its hydrated text", () => { const text = "Intro paragraph.\n\n# Heading\n\n- first\n- second\n\nClosing paragraph."; const harness = streamHarness(); for (let end = 8; end < text.length; end += 8) { @@ -304,7 +300,7 @@ describe("stream presentation through installed plugins", () => { ); }); - it("repoints a block at the canonical source that replaced the streamed one", () => { + it("uses the canonical source that replaced the streamed message", () => { const harness = streamHarness(); harness.send(assistant("Streamed answer")); const streamed = rows(harness.render()); @@ -338,15 +334,8 @@ describe("stream presentation through installed plugins", () => { head: [], transform: undefined, }); - expect(result.tail).toMatchObject([ - { - id: `${messageId}:block:0`, - blockGroupId: messageId, - blockIndex: 0, - text: "Only one paragraph.", - }, - ]); - expect(result.tail[0]!.id).not.toBe(messageId); + expect(result.tail).toEqual(source); + expect(result.tail[0]!.id).toBe(messageId); }); it("continues to stream inline reasoning with a stable plugin row", () => { @@ -563,13 +552,7 @@ describe("timeline presentation", () => { text: "ExampleExplain this XML.", }, ]; - expect(projectTimelineItems(items)).toEqual( - items.map((item) => - item.kind === "assistant_message" - ? { ...item, id: `${item.id}:block:0`, blockGroupId: item.id, blockIndex: 0 } - : item, - ), - ); + expect(projectTimelineItems(items)).toEqual(items); expect(projectTimelineItems(items)[0]).toBe(items[0]); }); diff --git a/packages/app/src/agent-stream/presentation.ts b/packages/app/src/agent-stream/presentation.ts index b89f91f6c0..4459486a7b 100644 --- a/packages/app/src/agent-stream/presentation.ts +++ b/packages/app/src/agent-stream/presentation.ts @@ -1,13 +1,12 @@ -import type { AssistantMessageItem, StreamItem, UserMessageItem } from "@/types/stream"; import type { TimelineItemTransform } from "@/plugins/timeline/model"; import { projectPluginTimelineItems } from "@/plugins/timeline/projection"; -import { splitMarkdownBlocks } from "@/utils/split-markdown-blocks"; import { prepareToolCallHistory, projectToolCallDetailLevel, type PreparedToolCallHistory, type ToolCallDetailLevel, } from "@/tool-calls/detail-level/projection"; +import type { StreamItem, UserMessageItem } from "@/types/stream"; interface PresentationInput { tail: StreamItem[]; @@ -17,42 +16,13 @@ interface PresentationInput { isTurnActive: boolean; } -function retainItems(previous: StreamItem[], next: StreamItem[]): StreamItem[] { - return previous.length === next.length && previous.every((item, index) => item === next[index]) - ? previous - : next; -} - -/** - * The source message a display row belongs to. Every assistant message is split into - * Markdown blocks, so an assistant row id never equals its message id; anything that - * addresses a message — find, scroll-to-message, history reveal — asks for this. A - * plugin row a timeline transformer projected answers for the item it replaced, whose - * id is what the chat outline and find hold. - */ +/** Find and scrolling address a plugin row by the source it replaces. */ export function getStreamItemMessageId(item: StreamItem): string { - if (item.kind === "assistant_message") return item.blockGroupId ?? item.id; if (item.kind === "plugin") return item.sourceId ?? item.id; return item.id; } -/** - * A block is reusable only when it still stands for the same source state. Text and - * turn are what the reader sees; cursor and timestamp are what the timeline reads back - * through the row — a reused block carrying a pre-canonical cursor reports a stale - * reading position and weakens fork-boundary resolution. - */ -function isReusableBlock(block: AssistantMessageItem, source: AssistantMessageItem, id: string) { - return ( - block.id === id && - block.turnId === source.turnId && - block.timestamp.getTime() === source.timestamp.getTime() && - block.timelineCursor?.epoch === source.timelineCursor?.epoch && - block.timelineCursor?.seq === source.timelineCursor?.seq - ); -} - -/** Source messages reach plugins before any Markdown splitting or Overview grouping. */ +/** Source messages reach plugins before display projection. */ export function createStreamPresentation() { const userMessageCache = new WeakMap(); @@ -71,115 +41,39 @@ export function createStreamPresentation() { return projected; } - const blocksBySource = new WeakMap(); - let liveSources = new Map(); let historySource: StreamItem[] | undefined; let historyTransform: TimelineItemTransform | undefined; let historyRows: StreamItem[] = []; - let displayTail: StreamItem[] = []; - let displayHistory: StreamItem[] | undefined; - let promotedRows: StreamItem[] = []; let preparedTail: StreamItem[] | undefined; let preparedLevel: ToolCallDetailLevel | undefined; let preparedHistory: PreparedToolCallHistory | null = null; - /** - * One display row per Markdown block. Each block renders on its own, so a construct - * that needs context from another block does not resolve: `splitMarkdownBlocks` keeps - * link reference definitions with the block that uses them, but a reference pointing - * at a definition several blocks away renders as literal text. Streamed messages - * always behaved this way; history now matches them. - */ - function nativeBlocks(item: StreamItem): StreamItem[] { - if (item.kind === "user_message") return [presentUserMessage(item)]; - if (item.kind !== "assistant_message") return [item]; - const cached = blocksBySource.get(item); - if (cached) return cached; - const previousSource = liveSources.get(item.id); - const previous = previousSource && blocksBySource.get(previousSource); - // Parse only the growing last block on append, as the old live reducer did. - // Canonical text replacements are parsed afresh instead of joining fragments. - const isAppend = previousSource && previous && item.text.startsWith(previousSource.text); - let prefix: AssistantMessageItem[] = []; - let growingText = item.text; - if (isAppend) { - prefix = previous.slice(0, -1); - growingText = - previous[previous.length - 1]!.text + item.text.slice(previousSource.text.length); - } - const parsed = splitMarkdownBlocks(growingText); - // Whitespace-only text has no block, and a message still owns exactly one row. - const textBlocks = parsed.length > 0 || prefix.length > 0 ? parsed : [""]; - - const blocks = [...prefix]; - for (const [offset, text] of textBlocks.entries()) { - const index = prefix.length + offset; - let blockText = text; - if (offset === textBlocks.length - 1) { - // The split drops trailing blank lines, including a line break followed only - // by the next line's indent. Keep them so the next append continues that line. - const trailingBlankLines = /\n\s*$/.exec(item.text)?.[0] ?? ""; - blockText += trailingBlankLines; - } - const existing = previous?.[index]; - const id = `${item.id}:block:${index}`; - if (existing?.text === blockText && isReusableBlock(existing, item, id)) { - blocks.push(existing); - continue; - } - blocks.push({ - ...item, - id, - blockGroupId: item.id, - blockIndex: index, - text: blockText, - }); - } - blocksBySource.set(item, blocks); - return blocks; + function presentRows(item: StreamItem): StreamItem[] { + return [item.kind === "user_message" ? presentUserMessage(item) : item]; } return (input: PresentationInput) => { // Retained history is not reprojected or regrouped on each live text update. if (historySource !== input.tail || historyTransform !== input.transform) { - // One rendering path: history splits the same way the live head does, and - // `blocksBySource` keeps both the live block identities and the split cost - // from being paid again when only the tail's array identity changed. historyRows = projectPluginTimelineItems(input.tail, input.transform).flatMap( - nativeBlocks, + (item) => presentRows(item), ); historySource = input.tail; historyTransform = input.transform; } const head: StreamItem[] = []; - const promoted: StreamItem[] = []; - const nextLiveSources = new Map(); for (const item of projectPluginTimelineItems(input.head, input.transform, "streaming")) { - const blocks = nativeBlocks(item); - if (item.kind === "assistant_message") nextLiveSources.set(item.id, item); - if (item.kind === "assistant_message" && blocks.length > 1) { - promoted.push(...blocks.slice(0, -1)); - head.push(blocks[blocks.length - 1]!); - } else { - head.push(...blocks); - } + const blocks = presentRows(item); + head.push(...blocks); } - liveSources = nextLiveSources; - const nextPromoted = retainItems(promotedRows, promoted); - if (displayHistory !== historyRows || nextPromoted !== promotedRows) { - displayTail = historyRows; - if (nextPromoted.length > 0) displayTail = [...historyRows, ...nextPromoted]; - displayHistory = historyRows; - promotedRows = nextPromoted; - } - if (preparedTail !== displayTail || preparedLevel !== input.level) { - preparedHistory = prepareToolCallHistory(input.level, displayTail); - preparedTail = displayTail; + if (preparedTail !== historyRows || preparedLevel !== input.level) { + preparedHistory = prepareToolCallHistory(input.level, historyRows); + preparedTail = historyRows; preparedLevel = input.level; } return projectToolCallDetailLevel({ level: input.level, - tail: displayTail, + tail: historyRows, head, preparedHistory, isTurnActive: input.isTurnActive, diff --git a/packages/app/src/agent-stream/spacing.test.ts b/packages/app/src/agent-stream/spacing.test.ts index dcf8a36844..07e9f185ff 100644 --- a/packages/app/src/agent-stream/spacing.test.ts +++ b/packages/app/src/agent-stream/spacing.test.ts @@ -1,135 +1,20 @@ import { describe, expect, it } from "vitest"; +import { getAssistantMessageSpacing, getGapBetweenStreamItems } from "./spacing"; import type { StreamItem } from "@/types/stream"; -import { getAssistantBlockSpacing, isSameAssistantBlockGroup } from "./spacing"; - -function assistantBlock(params: { - id: string; - blockGroupId: string; - blockIndex: number; - text?: string; -}): Extract { - return { - kind: "assistant_message", - id: params.id, - blockGroupId: params.blockGroupId, - blockIndex: params.blockIndex, - text: params.text ?? "", - timestamp: new Date("2026-05-01T00:00:00.000Z"), - }; -} - -function toolCallBlock(id: string): Extract { - return { - kind: "tool_call", - id, - timestamp: new Date("2026-05-01T00:00:00.000Z"), - payload: { - source: "orchestrator", - data: { - toolCallId: id, - toolName: "bash", - arguments: "cmd", - result: null, - status: "executing", - }, - }, - }; -} - -describe("isSameAssistantBlockGroup", () => { - it("returns true for two assistant blocks with the same blockGroupId", () => { - const a = assistantBlock({ id: "a", blockGroupId: "group-1", blockIndex: 0 }); - const b = assistantBlock({ id: "b", blockGroupId: "group-1", blockIndex: 1 }); - expect(isSameAssistantBlockGroup({ item: a, other: b })).toBe(true); - }); - - it("returns false for blocks from different groups", () => { - const a = assistantBlock({ id: "a", blockGroupId: "group-1", blockIndex: 0 }); - const b = assistantBlock({ id: "b", blockGroupId: "group-2", blockIndex: 0 }); - expect(isSameAssistantBlockGroup({ item: a, other: b })).toBe(false); - }); - - it("returns false when one item is not an assistant_message", () => { - const a = assistantBlock({ id: "a", blockGroupId: "group-1", blockIndex: 0 }); - const tc = toolCallBlock("tc-1"); - expect(isSameAssistantBlockGroup({ item: a, other: tc })).toBe(false); - }); - - it("returns false for null neighbors", () => { - const a = assistantBlock({ id: "a", blockGroupId: "group-1", blockIndex: 0 }); - expect(isSameAssistantBlockGroup({ item: a, other: null })).toBe(false); - }); -}); - -describe("getAssistantBlockSpacing", () => { - it("returns default for non-assistant items", () => { - const tc = toolCallBlock("tc-1"); - expect(getAssistantBlockSpacing({ item: tc, aboveItem: null, belowItem: null })).toBe( - "default", - ); - }); - - it("returns default when no same-group neighbors exist", () => { - const a = assistantBlock({ id: "a", blockGroupId: "group-1", blockIndex: 0 }); - expect(getAssistantBlockSpacing({ item: a, aboveItem: null, belowItem: null })).toBe("default"); - }); - - it("compacts the bottom edge when the turn footer follows the assistant block", () => { - const a = assistantBlock({ id: "a", blockGroupId: "group-1", blockIndex: 0 }); - expect( - getAssistantBlockSpacing({ - item: a, - aboveItem: null, - belowItem: null, - hasFooterBelow: true, - }), - ).toBe("compactBottom"); - }); - - it("returns compactTop when the item above is in the same block group", () => { - const above = assistantBlock({ id: "above", blockGroupId: "group-1", blockIndex: 0 }); - const item = assistantBlock({ id: "item", blockGroupId: "group-1", blockIndex: 1 }); - expect(getAssistantBlockSpacing({ item, aboveItem: above, belowItem: null })).toBe( - "compactTop", - ); - }); - - it("returns compactBottom when the item below is in the same block group", () => { - const item = assistantBlock({ id: "item", blockGroupId: "group-1", blockIndex: 0 }); - const below = assistantBlock({ id: "below", blockGroupId: "group-1", blockIndex: 1 }); - expect(getAssistantBlockSpacing({ item, aboveItem: null, belowItem: below })).toBe( +const message: StreamItem = { + kind: "assistant_message", + id: "m", + text: "First\n\nSecond", + timestamp: new Date(0), +}; +describe("whole-message spacing", () => { + it("compacts only the gap to its turn footer", () => { + expect(getAssistantMessageSpacing({ item: message })).toBe("default"); + expect(getAssistantMessageSpacing({ item: message, hasFooterBelow: true })).toBe( "compactBottom", ); }); - - it("returns compactBoth when both neighbors are in the same block group", () => { - const above = assistantBlock({ id: "above", blockGroupId: "group-1", blockIndex: 0 }); - const item = assistantBlock({ id: "item", blockGroupId: "group-1", blockIndex: 1 }); - const below = assistantBlock({ id: "below", blockGroupId: "group-1", blockIndex: 2 }); - expect(getAssistantBlockSpacing({ item, aboveItem: above, belowItem: below })).toBe( - "compactBoth", - ); - }); - - it("spans the history/live-head boundary: tail gets compactBottom, head gets compactTop", () => { - const tailBlock = assistantBlock({ - id: "group-1:block:0", - blockGroupId: "group-1", - blockIndex: 0, - text: "First paragraph", - }); - const headBlock = assistantBlock({ - id: "group-1:head", - blockGroupId: "group-1", - blockIndex: 1, - text: "Second paragraph", - }); - - expect( - getAssistantBlockSpacing({ item: tailBlock, aboveItem: null, belowItem: headBlock }), - ).toBe("compactBottom"); - expect( - getAssistantBlockSpacing({ item: headBlock, aboveItem: tailBlock, belowItem: null }), - ).toBe("compactTop"); + it("keeps separate messages separated", () => { + expect(getGapBetweenStreamItems(message, { ...message, id: "next" })).toBeGreaterThan(0); }); }); diff --git a/packages/app/src/agent-stream/spacing.ts b/packages/app/src/agent-stream/spacing.ts index 87bdf75f88..55761f71f3 100644 --- a/packages/app/src/agent-stream/spacing.ts +++ b/packages/app/src/agent-stream/spacing.ts @@ -1,35 +1,13 @@ import type { StreamItem } from "@/types/stream"; import { SPACING } from "@/styles/theme"; -export function isSameAssistantBlockGroup(params: { - item: StreamItem | null | undefined; - other: StreamItem | null | undefined; -}): boolean { - return ( - params.item?.kind === "assistant_message" && - params.other?.kind === "assistant_message" && - params.item.blockGroupId !== undefined && - params.item.blockGroupId === params.other.blockGroupId - ); -} - -export function getAssistantBlockSpacing(params: { +export function getAssistantMessageSpacing(params: { item: StreamItem; - aboveItem: StreamItem | null | undefined; - belowItem: StreamItem | null | undefined; hasFooterBelow?: boolean; -}): "default" | "compactTop" | "compactBottom" | "compactBoth" { - if (params.item.kind !== "assistant_message") { - return "default"; - } - const compactTop = isSameAssistantBlockGroup({ item: params.item, other: params.aboveItem }); - const compactBottom = - params.hasFooterBelow || - isSameAssistantBlockGroup({ item: params.item, other: params.belowItem }); - if (compactTop && compactBottom) return "compactBoth"; - if (compactTop) return "compactTop"; - if (compactBottom) return "compactBottom"; - return "default"; +}): "default" | "compactBottom" { + return params.item.kind === "assistant_message" && params.hasFooterBelow + ? "compactBottom" + : "default"; } const isUserMessageItem = (item?: StreamItem | null) => item?.kind === "user_message"; @@ -62,8 +40,5 @@ export function getGapBetweenStreamItems( if (isToolSequenceItem(item) && belowItem.kind === "assistant_message") { return SPACING[1]; } - if (isSameAssistantBlockGroup({ item, other: belowItem })) { - return SPACING[3]; - } return SPACING[4]; } diff --git a/packages/app/src/agent-stream/strategy-native.tsx b/packages/app/src/agent-stream/strategy-native.tsx index 65f535db11..66409f23e1 100644 --- a/packages/app/src/agent-stream/strategy-native.tsx +++ b/packages/app/src/agent-stream/strategy-native.tsx @@ -591,8 +591,11 @@ function NativeStreamViewport(props: StreamRenderInput & { strategy: StreamStrat scrollEventThrottle={16} onContentSizeChange={handleContentSizeChange} maintainVisibleContentPosition={maintainVisibleContentPosition} - initialNumToRender={12} - windowSize={10} + // A history row can contain an entire long reply. Keep synchronous native + // Markdown measurement bounded while FlatList fills the surrounding window. + initialNumToRender={2} + maxToRenderPerBatch={2} + windowSize={5} removeClippedSubviews={false} scrollEnabled={scrollEnabled} showsVerticalScrollIndicator diff --git a/packages/app/src/agent-stream/strategy-web.tsx b/packages/app/src/agent-stream/strategy-web.tsx index f1c806fdca..97490c2a17 100644 --- a/packages/app/src/agent-stream/strategy-web.tsx +++ b/packages/app/src/agent-stream/strategy-web.tsx @@ -307,7 +307,6 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool olderHistoryProgressKey, scrollEnabled, contentMaxWidth, - imageContext, isMobileBreakpoint, } = props; const historyVirtualized = useRevisedHistoryRows( @@ -455,7 +454,7 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool const row = segments.historyVirtualized[index]; const measured = row && measuredRowHeights.current.get(row.id); if (measured && measured.width === contentWidth) return measured.height; - return row ? estimateStreamItemHeight({ item: row, contentMaxWidth, imageContext }) : 120; + return row ? estimateStreamItemHeight({ item: row, contentMaxWidth }) : 120; }, observeElementOffset: observeVirtualOffset, rangeExtractor, @@ -979,14 +978,25 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool if (!followOutputRef.current) { return; } - scheduleStickToBottom(); + // ResizeObserver runs before paint. Following in the next frame exposes + // the newly grown row at the old offset for one painted frame. + forceStickToBottom(); }); observer.observe(scrollContainer); if (contentNode) { observer.observe(contentNode); } + // Paced reveal commits inside a message without rerendering this viewport. + // Observe those commits too, so the next frame sees the grown content at the + // bottom instead of waiting for the resize notification after animation frames. + const contentChanges = new MutationObserver(() => { + if (isActiveRef.current && followOutputRef.current) forceStickToBottom(); + }); + if (contentNode) + contentChanges.observe(contentNode, { childList: true, characterData: true, subtree: true }); return () => { observer.disconnect(); + contentChanges.disconnect(); }; }, [ reconcileReadingPosition, @@ -994,7 +1004,7 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool isActive, reportReadingPosition, scheduleHistoryStartPrependSettle, - scheduleStickToBottom, + forceStickToBottom, updateScrollMetrics, measureVirtualizedRows, ]); diff --git a/packages/app/src/agent-stream/strategy.ts b/packages/app/src/agent-stream/strategy.ts index 3b77f6b831..5f43f8f6b1 100644 --- a/packages/app/src/agent-stream/strategy.ts +++ b/packages/app/src/agent-stream/strategy.ts @@ -1,4 +1,3 @@ -import type { AssistantImageContext } from "@/utils/assistant-image-metadata"; import type { ComponentType, ReactElement, ReactNode, RefObject } from "react"; import type { StyleProp, ViewStyle } from "react-native"; import type { StreamItem } from "@/types/stream"; @@ -95,7 +94,6 @@ export interface StreamRenderInput { forwardListContentContainerStyle: StyleProp; // The chat column's max width. The web viewport estimates unmeasured row heights with it. contentMaxWidth: number; - imageContext?: AssistantImageContext; } export interface ResolveStreamRenderStrategyInput { diff --git a/packages/app/src/agent-stream/view.tsx b/packages/app/src/agent-stream/view.tsx index 6526e7352b..c9b44ed679 100644 --- a/packages/app/src/agent-stream/view.tsx +++ b/packages/app/src/agent-stream/view.tsx @@ -87,13 +87,10 @@ import { } from "./bottom-anchor-controller"; import { createAssistantImageOccurrenceKey } from "@/assistant-image/acquisition-cache"; import { AssistantSelectionCopySurface } from "@/assistant-selection-copy/surface"; -import { - AssistantFileLinkResolverProvider, - normalizeInlinePathTarget, -} from "@/assistant-file-links"; +import { AssistantFileLinkResolverProvider, createInlinePathOpener } from "@/assistant-file-links"; import { createWorkspaceFileTabTarget, - normalizeWorkspaceFileLocation, + type WorkspaceFileLocation, type OpenFileDisposition, type WorkspaceFileOpenRequest, } from "@/workspace/file-open"; @@ -218,12 +215,13 @@ function renderListEmptyComponent(input: { ); } -// History rows sit inside FlatList cells that rerender on every data change (RN recreates each -// CellRenderer with a fresh ref and, in a newest-first list, a shifted index). This boundary is +// Live and retained rows use the same boundary so completion preserves the message +// subtree, including measured diagrams and selections. FlatList recreates history +// cells with fresh refs and shifted indexes on data changes. This boundary is // what stops that churn: a row renders again only when its stream item identity, its layout item // identity, or the renderer itself changes. Item identity is the revision signal the strategy // already uses (`useRevisedHistoryRows` clones items whose content or display state changed). -const HistoryStreamRow = memo(function HistoryStreamRow({ +const StreamRow = memo(function StreamRow({ layoutItem, renderStreamItem, }: { @@ -234,7 +232,7 @@ const HistoryStreamRow = memo(function HistoryStreamRow({ return <>{renderStreamItem(layoutItem)}; }); -function renderHistoryStreamItem(input: { +function renderStreamRow(input: { item: StreamItem; layoutItemById: Map; renderStreamItem: (layoutItem: StreamLayoutItem) => ReactNode; @@ -244,7 +242,7 @@ function renderHistoryStreamItem(input: { return null; } return ( - ; - renderStreamItem: (layoutItem: StreamLayoutItem) => ReactNode; -}): ReactNode { - const layoutItem = input.layoutItemById.get(input.item.id); - if (!layoutItem) { - return null; - } - return input.renderStreamItem(layoutItem); -} - export interface AgentStreamViewHandle { scrollToBottom(reason?: BottomAnchorLocalRequest["reason"]): void; prepareForViewportChange(): void; @@ -398,7 +384,7 @@ const AgentStreamViewComponent = forwardRef { + if (onOpenWorkspaceFile) { + onOpenWorkspaceFile({ location, disposition }); + } else if (context.workspaceId) { + navigateToWorkspace({ + serverId: resolvedServerId, + workspaceId: context.workspaceId, + target: createWorkspaceFileTabTarget(location), + }); + } + }, + ); + const openInlineDirectory = useStableEvent((directoryPath: string) => { + void revealDirectory(directoryPath); + openExplorerSidebarView({ + isCompact: isMobile, + workspaceKey: buildWorkspaceTabPersistenceKey({ + serverId: resolvedServerId, + workspaceId: context.workspaceId ?? "", + }), + checkout: { + serverId: resolvedServerId, + cwd: context.cwd, + isGit: context.projectPlacement?.checkout?.isGit ?? true, + }, + view: "files", + }); + }); + const inlinePathOpener = useMemo( + () => + createInlinePathOpener({ + workspaceRoot, + client, + openFile: openInlineFile, + openDirectory: openInlineDirectory, + }), + [workspaceRoot, client, openInlineFile, openInlineDirectory], + ); + useEffect(() => { + if (!isActive) inlinePathOpener.cancel(); + return () => inlinePathOpener.cancel(); + }, [inlinePathOpener, isActive, agentId]); const handleInlinePathPress = useStableEvent( (target: InlinePathTarget, disposition: OpenFileDisposition) => { - if (!target.path) { - return; - } - - const normalized = normalizeInlinePathTarget(target.path, context.cwd); - if (!normalized) { - return; - } - - if (normalized.file) { - const location = normalizeWorkspaceFileLocation({ - path: normalized.file, - lineStart: target.lineStart, - lineEnd: target.lineEnd, - }); - if (!location) { - return; - } - - if (onOpenWorkspaceFile) { - onOpenWorkspaceFile({ - location, - disposition, - }); - return; - } - - if (context.workspaceId) { - navigateToWorkspace({ - serverId: resolvedServerId, - workspaceId: context.workspaceId, - target: createWorkspaceFileTabTarget(location), - }); - } - return; - } - - void requestDirectoryListing(normalized.directory, { - recordHistory: false, - setCurrentPath: false, - }); - - openExplorerSidebarView({ - isCompact: isMobile, - workspaceKey: buildWorkspaceTabPersistenceKey({ - serverId: resolvedServerId, - workspaceId: context.workspaceId ?? "", - }), - checkout: { - serverId: resolvedServerId, - cwd: context.cwd, - isGit: context.projectPlacement?.checkout?.isGit ?? true, - }, - view: "files", - }); + if (isActive) void inlinePathOpener.open(target, disposition); }, ); @@ -530,7 +506,6 @@ const AgentStreamViewComponent = forwardRef - renderHistoryStreamItem({ + renderStreamRow({ item, layoutItemById: layoutHistoryItemById, renderStreamItem, @@ -1051,7 +1026,7 @@ const AgentStreamViewComponent = forwardRef - renderLiveHeadStreamItem({ + renderStreamRow({ item, layoutItemById: layoutLiveHeadItemById, renderStreamItem, @@ -1137,7 +1112,6 @@ const AgentStreamViewComponent = forwardRef ); } diff --git a/packages/app/src/agent-stream/web-virtualization.test.ts b/packages/app/src/agent-stream/web-virtualization.test.ts index 602925faa0..31107789e2 100644 --- a/packages/app/src/agent-stream/web-virtualization.test.ts +++ b/packages/app/src/agent-stream/web-virtualization.test.ts @@ -2,9 +2,9 @@ import { describe, expect, it } from "vitest"; import { DEFAULT_CONTENT_MAX_WIDTH } from "@/styles/theme"; import type { StreamItem } from "@/types/stream"; import { - clearAssistantImageMetadataCache, - setAssistantImageMetadata, -} from "@/utils/assistant-image-metadata"; + setAssistantMessageHeight, + clearAssistantMessageHeightEstimateCache, +} from "@/utils/assistant-message-height-estimate"; import { DEFAULT_WEB_MOUNTED_RECENT_STREAM_ITEMS, DEFAULT_WEB_PARTIAL_VIRTUALIZATION_THRESHOLD, @@ -161,56 +161,18 @@ describe("estimateStreamItemHeight", () => { ); }); - it("uses local image dimensions only from the row's server and workspace", () => { - clearAssistantImageMetadataCache(); - const imageContext = { serverId: "server-one", workspaceRoot: "/workspace/one" }; - setAssistantImageMetadata( - { source: "portrait.png", ...imageContext }, - { width: 800, height: 1600 }, - ); + it("uses the renderer's whole-message measurement at the measured width", () => { + clearAssistantMessageHeightEstimateCache(); const item: StreamItem = { kind: "assistant_message", - id: "local-image", - text: "![Portrait](portrait.png)", + id: "measured", + text: "First paragraph\n\n![Image](image.png)", timestamp: createTimestamp(2), }; - - expect(estimateStreamItemHeight({ item, contentMaxWidth: 608, imageContext })).toBe(1264); - expect( - estimateStreamItemHeight({ - item, - contentMaxWidth: 608, - imageContext: { ...imageContext, workspaceRoot: "/workspace/two" }, - }), - ).toBe(464); - expect( - estimateStreamItemHeight({ - item, - contentMaxWidth: 608, - imageContext: { ...imageContext, serverId: "server-two" }, - }), - ).toBe(464); - }); - - it("uses cached assistant image metadata when available", () => { - clearAssistantImageMetadataCache(); - setAssistantImageMetadata( - { - source: "https://example.com/tall.png", - }, - { width: 800, height: 1600 }, - ); - - const item: StreamItem = { - kind: "assistant_message", - id: "a-image", - text: "Look at this\n\n![Screenshot](https://example.com/tall.png)", - timestamp: createTimestamp(2), - }; - - expect( - estimateStreamItemHeight({ item, contentMaxWidth: DEFAULT_CONTENT_MAX_WIDTH }), - ).toBeGreaterThan(220); + setAssistantMessageHeight({ markdown: item.text, width: 608, height: 640 }); + expect(estimateStreamItemHeight({ item, contentMaxWidth: 608 })).toBe(640); + expect(estimateStreamItemHeight({ item, contentMaxWidth: 320 })).toBe(220); + clearAssistantMessageHeightEstimateCache(); }); }); diff --git a/packages/app/src/agent-stream/web-virtualization.ts b/packages/app/src/agent-stream/web-virtualization.ts index 0c7f756959..daf01af558 100644 --- a/packages/app/src/agent-stream/web-virtualization.ts +++ b/packages/app/src/agent-stream/web-virtualization.ts @@ -1,4 +1,3 @@ -import type { AssistantImageContext } from "@/utils/assistant-image-metadata"; import type { StreamItem } from "@/types/stream"; import { estimateAssistantMessageHeightFromCache } from "@/utils/assistant-message-height-estimate"; import { @@ -47,13 +46,11 @@ export interface WebVirtualizedHistoryWindow { interface StreamItemHeightEstimateInput { item: StreamItem; contentMaxWidth: number; - imageContext?: AssistantImageContext; } export function estimateStreamItemHeight({ item, contentMaxWidth, - imageContext, }: StreamItemHeightEstimateInput): number { switch (item.kind) { case "user_message": @@ -63,7 +60,6 @@ export function estimateStreamItemHeight({ estimateAssistantMessageHeightFromCache({ markdown: item.text, contentMaxWidth, - imageContext, }) ?? 220 ); case "tool_call": diff --git a/packages/app/src/assistant-file-links/extensions.ts b/packages/app/src/assistant-file-links/extensions.ts new file mode 100644 index 0000000000..b32bf6aad9 --- /dev/null +++ b/packages/app/src/assistant-file-links/extensions.ts @@ -0,0 +1,57 @@ +export const ASSISTANT_FILE_EXTENSIONS: ReadonlySet = new Set([ + "astro", + "bash", + "c", + "cc", + "cjs", + "cpp", + "cs", + "css", + "cts", + "cxx", + "env", + "fish", + "go", + "gql", + "gradle", + "graphql", + "h", + "hpp", + "htm", + "html", + "ini", + "java", + "js", + "json", + "jsonc", + "jsx", + "kt", + "kts", + "less", + "lock", + "lua", + "md", + "mdx", + "mjs", + "mts", + "php", + "proto", + "py", + "rb", + "rs", + "sass", + "scss", + "sh", + "sql", + "svelte", + "swift", + "toml", + "ts", + "tsx", + "txt", + "vue", + "xml", + "yaml", + "yml", + "zsh", +]); diff --git a/packages/app/src/assistant-file-links/index.ts b/packages/app/src/assistant-file-links/index.ts index 723945409f..9b66e99c18 100644 --- a/packages/app/src/assistant-file-links/index.ts +++ b/packages/app/src/assistant-file-links/index.ts @@ -1,9 +1,3 @@ -export { - AssistantInlineCodePathLink, - AssistantMarkdownCodeLink, - AssistantMarkdownLink, -} from "./link"; -export { type AssistantLinkPress, useAssistantLinkPress } from "./link-press-context"; export { classifyAssistantFileLink, normalizeInlinePathTarget, @@ -11,7 +5,10 @@ export { } from "./parse"; export { AssistantFileLinkResolverProvider, + useAssistantFileLinkEnvironment, type AssistantFileLinkResolverProviderProps, } from "./provider"; export type { AssistantFileLinkSource } from "./resolver"; -export { useAssistantFileLinkActions } from "./use-file-link"; +export { useAssistantFileLinkActions, useFileLink } from "./use-file-link"; + +export { createInlinePathOpener } from "./path-open"; diff --git a/packages/app/src/assistant-file-links/link-press-context.ts b/packages/app/src/assistant-file-links/link-press-context.ts deleted file mode 100644 index 2d6ae19639..0000000000 --- a/packages/app/src/assistant-file-links/link-press-context.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createContext, useContext } from "react"; -import type { TextProps } from "react-native"; - -// Carries a link's press handler down to the leaf text spans that render its -// label. On iOS an assistant link is a nested UITextView span, and -// react-native-uitextview only attaches onPress to the *string* children it -// converts into RNUITextViewChild nodes (src/Text.tsx) — element children (the -// MarkdownInheritedText spans markdown produces for link text) pass through -// untouched, so an onPress placed on the wrapping span never reaches a tappable -// native node. Threading the handler through context lets each leaf span hand -// onPress to its own string children, where the native tap recognizer can find -// it. Provided only on iOS (Android/web links tap fine via their own paths). -export interface AssistantLinkPress { - onPress: () => void; - accessibilityRole?: TextProps["accessibilityRole"]; -} - -const AssistantLinkPressContext = createContext(null); - -export const AssistantLinkPressProvider = AssistantLinkPressContext.Provider; - -export function useAssistantLinkPress(): AssistantLinkPress | null { - return useContext(AssistantLinkPressContext); -} diff --git a/packages/app/src/assistant-file-links/link.tsx b/packages/app/src/assistant-file-links/link.tsx deleted file mode 100644 index 4b8e944ee3..0000000000 --- a/packages/app/src/assistant-file-links/link.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import { useMemo, type CSSProperties, type MouseEvent, type ReactNode } from "react"; -import { Platform, Text, View, type StyleProp, type TextStyle, type ViewStyle } from "react-native"; -import { StyleSheet } from "react-native-unistyles"; -import { isNative, isWeb } from "@/constants/platform"; -import { MarkdownTextSpan } from "@/components/markdown-text"; -import { MarkdownLinkText } from "@/components/markdown/link-text"; -import { AssistantLinkPressProvider, type AssistantLinkPress } from "./link-press-context"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; -import { CODE_SURFACE_DATASET } from "@/styles/code-surface"; -import { markdownCopyDataSet } from "@/assistant-selection-copy/markup"; -import { useAssistantFileLinkResolverContext } from "./provider"; -import type { AssistantFileLinkSource } from "./resolver"; -import { formatFileLinkTooltipPath } from "./tooltip-path"; -import { useFileLink } from "./use-file-link"; - -interface AssistantMarkdownLinkProps { - source: AssistantFileLinkSource; - style: StyleProp; - monoSurface?: boolean; - children: ReactNode; -} - -const MARKDOWN_CODE_LINK_DATASET = { - ...CODE_SURFACE_DATASET, - ...markdownCopyDataSet.code, -} as const; - -export function AssistantMarkdownLink({ - source, - style, - monoSurface, - children, -}: AssistantMarkdownLinkProps) { - const { target, onHoverIn, onPress } = useFileLink(source); - const { configRef } = useAssistantFileLinkResolverContext(); - const workspaceRoot = configRef.current.workspaceRoot; - const tooltipPath = useMemo( - () => (target ? formatFileLinkTooltipPath({ target, workspaceRoot }) : null), - [target, workspaceRoot], - ); - const linkPress = useMemo( - () => ({ onPress, accessibilityRole: "link" }), - [onPress], - ); - const unwrapForMarkdownCopy = source.sourceType === "inline-code" || source.markup === "linkify"; - - if (isNative) { - // Must be a MarkdownTextSpan, not a plain : on iOS the link renders - // inside the paragraph's native UITextView, and a plain nested there - // is not hoisted into a UITextViewChild, so its text is silently dropped - // (the link disappears). The span composes correctly and stays selectable. - // - // Tap-to-open: react-native-uitextview only wires onPress onto the *string* - // children it turns into RNUITextViewChild nodes — the element children that - // markdown emits for link text pass through untouched, so an onPress placed - // here never reaches a tappable native node. We thread it down through - // AssistantLinkPressProvider so each leaf text span re-attaches it to its - // own string children, where the native tap recognizer can find it. iOS - // only: Android forwards onPress through nested already, and web uses - // the path below. - const span = ( - - {children} - - ); - return ( - - {Platform.OS === "ios" ? ( - {span} - ) : ( - span - )} - - ); - } - - const anchor = ( - - - {children} - - - ); - - return {anchor}; -} - -interface AssistantMarkdownCodeLinkProps { - source: AssistantFileLinkSource; - inheritedStyles: TextStyle; - codeInlineStyle: TextStyle; - linkStyle: TextStyle; - children: ReactNode; -} - -export function AssistantMarkdownCodeLink({ - source, - inheritedStyles, - codeInlineStyle, - linkStyle, - children, -}: AssistantMarkdownCodeLinkProps) { - const style = useMemo( - () => [inheritedStyles, codeInlineStyle, linkStyle], - [inheritedStyles, codeInlineStyle, linkStyle], - ); - return ( - - {children} - - ); -} - -interface AssistantInlineCodePathLinkProps { - content: string; - inheritedStyles: TextStyle; - codeInlineStyle: TextStyle; - linkStyle: TextStyle; -} - -export function AssistantInlineCodePathLink({ - content, - inheritedStyles, - codeInlineStyle, - linkStyle, -}: AssistantInlineCodePathLinkProps) { - const source = useMemo( - () => ({ - href: content, - text: content, - sourceType: "inline-code", - }), - [content], - ); - - return ( - - {content} - - ); -} - -const FILE_LINK_TOOLTIP_TRIGGER_STYLE: ViewStyle = { - // RN doesn't type "inline-flex" but RN-web honors it at runtime, which keeps - // the tooltip wrapper from breaking inline link flow. - display: "inline-flex" as ViewStyle["display"], -}; - -function FileLinkHoverTooltip({ - filePath, - children, -}: { - filePath: string | null; - children: ReactNode; -}) { - if (!isWeb) { - return children; - } - return ( - - - {children} - - {filePath ? ( - - - {filePath} - - - ) : null} - - ); -} - -const LINK_ANCHOR_STYLE: CSSProperties = { - display: "contents", - color: "inherit", - textDecoration: "none", -}; - -function preventAnchorNavigation(event: MouseEvent): void { - event.preventDefault(); -} - -const styles = StyleSheet.create((theme) => ({ - tooltipPath: { - color: theme.colors.foreground, - fontSize: theme.fontSize.sm, - fontWeight: theme.fontWeight.normal, - }, -})); diff --git a/packages/app/src/assistant-file-links/parse.ts b/packages/app/src/assistant-file-links/parse.ts index 0a8089acca..1904c11907 100644 --- a/packages/app/src/assistant-file-links/parse.ts +++ b/packages/app/src/assistant-file-links/parse.ts @@ -1,3 +1,4 @@ +import { ASSISTANT_FILE_EXTENSIONS } from "./extensions"; import { isAbsolutePath } from "@/utils/path"; function safeDecodeURIComponent(value: string): string { @@ -20,64 +21,6 @@ const INLINE_LINE_FRAGMENT = /^L([0-9]+)(?:C[0-9]+)?(?:-L?([0-9]+)(?:C[0-9]+)?)? const INLINE_COLON_LINE_SUFFIX = /^(.+?):([0-9]+)(?::[0-9]+)?(?:-([0-9]+)(?::[0-9]+)?)?$/; const INLINE_PAREN_LINE_SUFFIX = /^(.+?)\(([0-9]+)(?:,[0-9]+)?(?:-([0-9]+)(?:,[0-9]+)?)?\)$/; const INLINE_WORD_LINE_SUFFIX = /^(.+?)\s+lines?\s+([0-9]+)(?:-([0-9]+))?$/i; -const ASSISTANT_FILE_EXTENSIONS = new Set([ - "astro", - "bash", - "c", - "cc", - "cjs", - "cpp", - "cs", - "css", - "cts", - "cxx", - "env", - "fish", - "go", - "gql", - "gradle", - "graphql", - "h", - "hpp", - "htm", - "html", - "ini", - "java", - "js", - "json", - "jsonc", - "jsx", - "kt", - "kts", - "less", - "lock", - "lua", - "md", - "mdx", - "mjs", - "mts", - "php", - "proto", - "py", - "rb", - "rs", - "sass", - "scss", - "sh", - "sql", - "svelte", - "swift", - "toml", - "ts", - "tsx", - "txt", - "vue", - "xml", - "yaml", - "yml", - "zsh", -]); - export interface AssistantHrefParseOptions { workspaceRoot?: string; /** diff --git a/packages/app/src/assistant-file-links/path-open.test.ts b/packages/app/src/assistant-file-links/path-open.test.ts new file mode 100644 index 0000000000..a046a598f6 --- /dev/null +++ b/packages/app/src/assistant-file-links/path-open.test.ts @@ -0,0 +1,104 @@ +import { mkdtemp, mkdir, readdir, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { afterEach, expect, it } from "vitest"; +import { resolveInlinePathOpen } from "./path-open"; + +const roots: string[] = []; +afterEach(async () => { + await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); +}); + +it("distinguishes dotted and extensionless folders from files using the filesystem", async () => { + const workspaceRoot = await mkdtemp(path.join(tmpdir(), "folder-link-")); + roots.push(workspaceRoot); + await mkdir(path.join(workspaceRoot, "docs/guides"), { recursive: true }); + await mkdir(path.join(workspaceRoot, "docs/reports.v1")); + await writeFile(path.join(workspaceRoot, "Makefile"), "all:\n"); + await writeFile(path.join(workspaceRoot, "README.md"), "# Readme\n"); + const client = { + listDirectory: (root: string, file: string) => readdir(path.resolve(root, file)), + }; + for (const file of ["docs/guides", "docs/reports.v1"]) { + await expect( + resolveInlinePathOpen({ file, directory: "docs", workspaceRoot, client }), + ).resolves.toEqual({ kind: "directory", directoryPath: file }); + } + for (const file of ["Makefile", "README.md", "missing.md"]) { + await expect( + resolveInlinePathOpen({ file, directory: ".", workspaceRoot, client }), + ).resolves.toEqual({ kind: "file" }); + } +}); + +it("preserves explicit directory destinations and file line references", async () => { + const workspaceRoot = "/repo"; + await expect( + resolveInlinePathOpen({ directory: "docs/guides", workspaceRoot, client: null }), + ).resolves.toEqual({ kind: "directory", directoryPath: "docs/guides" }); + await expect( + resolveInlinePathOpen({ + file: "docs/guides", + directory: "docs", + lineStart: 12, + workspaceRoot, + client: null, + }), + ).resolves.toEqual({ kind: "file" }); +}); + +it("keeps unavailable probes and external paths on the existing file-open path", async () => { + for (const file of ["Makefile", "/external/folder", "C:/external/folder"]) { + await expect( + resolveInlinePathOpen({ file, directory: ".", workspaceRoot: "/repo", client: null }), + ).resolves.toEqual({ kind: "file" }); + } +}); + +it("ignores an older probe after a newer file link opens", async () => { + const { createInlinePathOpener } = await import("./path-open"); + let rejectProbe!: (error: Error) => void; + const pendingProbe = new Promise((_, reject) => { + rejectProbe = reject; + }); + const opened: string[] = []; + const opener = createInlinePathOpener({ + workspaceRoot: "/repo", + client: { listDirectory: () => pendingProbe }, + openFile: (location) => { + opened.push(location.path); + }, + openDirectory: (directory) => { + opened.push(directory); + }, + }); + const first = opener.open({ raw: "./first.md", path: "/repo/first.md" }, "preferred"); + await opener.open({ raw: "./second.md:2", path: "/repo/second.md", lineStart: 2 }, "preferred"); + rejectProbe(new Error("Not a directory")); + await first; + expect(opened).toEqual(["second.md"]); +}); + +it("ignores a pending probe when its view is left", async () => { + const { createInlinePathOpener } = await import("./path-open"); + let resolveProbe!: (value: unknown) => void; + const pendingProbe = new Promise((resolve) => { + resolveProbe = resolve; + }); + const opened: string[] = []; + const opener = createInlinePathOpener({ + workspaceRoot: "/repo", + client: { listDirectory: () => pendingProbe }, + openFile: (location) => { + opened.push(location.path); + }, + openDirectory: (directory) => { + opened.push(directory); + }, + }); + const pending = opener.open({ raw: "./docs", path: "/repo/docs" }, "preferred"); + opener.cancel(); + resolveProbe({}); + await pending; + expect(opened).toEqual([]); +}); diff --git a/packages/app/src/assistant-file-links/path-open.ts b/packages/app/src/assistant-file-links/path-open.ts new file mode 100644 index 0000000000..b3d2178165 --- /dev/null +++ b/packages/app/src/assistant-file-links/path-open.ts @@ -0,0 +1,74 @@ +import { normalizeInlinePathTarget, type InlinePathTarget } from "./parse"; +import { + normalizeWorkspaceFileLocation, + type WorkspaceFileLocation, + type OpenFileDisposition, +} from "@/workspace/file-open"; +import { isAbsolutePath } from "@/utils/path"; + +interface PathOpenInput { + file?: string; + directory: string; + lineStart?: number; + workspaceRoot: string; + client: { listDirectory(root: string, path: string): Promise } | null; +} + +type PathOpenTarget = { kind: "file" } | { kind: "directory"; directoryPath: string }; + +/** Resolve workspace folders from filesystem facts, including names that contain dots. */ +export async function resolveInlinePathOpen(input: PathOpenInput): Promise { + if (!input.file) { + return { kind: "directory", directoryPath: input.directory }; + } + if ( + input.lineStart !== undefined || + isAbsolutePath(input.file) || + !input.workspaceRoot.trim() || + !input.client + ) { + return { kind: "file" }; + } + try { + await input.client.listDirectory(input.workspaceRoot, input.file); + } catch { + return { kind: "file" }; + } + return { kind: "directory", directoryPath: input.file }; +} + +export function createInlinePathOpener(input: { + workspaceRoot: string; + client: PathOpenInput["client"]; + openFile: (location: WorkspaceFileLocation, disposition: OpenFileDisposition) => void; + openDirectory: (path: string) => void; +}) { + let generation = 0; + return { + async open(target: InlinePathTarget, disposition: OpenFileDisposition) { + const ownGeneration = ++generation; + const normalized = normalizeInlinePathTarget(target.path, input.workspaceRoot); + if (!normalized) return; + const destination = await resolveInlinePathOpen({ + ...normalized, + lineStart: target.lineStart, + workspaceRoot: input.workspaceRoot, + client: input.client, + }); + if (ownGeneration !== generation) return; + if (destination.kind === "directory") { + input.openDirectory(destination.directoryPath); + } else if (normalized.file) { + const location = normalizeWorkspaceFileLocation({ + path: normalized.file, + lineStart: target.lineStart, + lineEnd: target.lineEnd, + }); + if (location) input.openFile(location, disposition); + } + }, + cancel() { + generation += 1; + }, + }; +} diff --git a/packages/app/src/assistant-file-links/provider.tsx b/packages/app/src/assistant-file-links/provider.tsx index 77d0b2e034..967e1c32e3 100644 --- a/packages/app/src/assistant-file-links/provider.tsx +++ b/packages/app/src/assistant-file-links/provider.tsx @@ -14,6 +14,7 @@ import type { InlinePathTarget } from "./parse"; import type { AssistantFileLinkContext, GetDirectorySuggestions } from "./resolver"; export interface AssistantFileLinkDaemonClient { + readFile?: import("@getpaseo/client/internal/daemon-client").DaemonClient["readFile"]; getDirectorySuggestions: GetDirectorySuggestions; } @@ -78,10 +79,16 @@ export function AssistantFileLinkResolverProvider({ export function useAssistantFileLinkResolverContext(): AssistantFileLinkResolverContextValue { const context = useContext(AssistantFileLinkResolverContext); - if (!context) { - throw new Error("AssistantFileLinkResolverProvider is required for assistant file links."); - } - return context; + return context ?? EMPTY_CONTEXT; } export type { AssistantFileLinkContext }; + +const EMPTY_CONTEXT: AssistantFileLinkResolverContextValue = { + configRef: { current: {} }, + getDirectorySuggestions: async () => ({ entries: [], error: null }), +}; + +export function useAssistantFileLinkEnvironment() { + return useAssistantFileLinkResolverContext().configRef.current; +} diff --git a/packages/app/src/assistant-file-links/use-file-link.ts b/packages/app/src/assistant-file-links/use-file-link.ts index 603645f0fe..4f117e5dc1 100644 --- a/packages/app/src/assistant-file-links/use-file-link.ts +++ b/packages/app/src/assistant-file-links/use-file-link.ts @@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next"; import { useStableEvent } from "@/hooks/use-stable-event"; import type { OpenFileDisposition } from "@/workspace/file-open"; import { openExternalUrl } from "@/utils/open-external-url"; +import { formatFileLinkTooltipPath } from "./tooltip-path"; import type { InlinePathTarget } from "./parse"; import { useAssistantFileLinkResolverContext, @@ -18,6 +19,7 @@ import { export interface UseFileLinkResult { target: InlinePathTarget | null; + tooltipPath: string | null; onHoverIn: () => void; onPress: () => void; open: (source: AssistantFileLinkSource, disposition: OpenFileDisposition) => void; @@ -134,7 +136,11 @@ export function useFileLink(source: AssistantFileLinkSource): UseFileLinkResult return query.data ?? null; }, [query.data, resolution]); - return useMemo(() => ({ target, onHoverIn, onPress, open }), [target, onHoverIn, onPress, open]); + const tooltipPath = target ? formatFileLinkTooltipPath({ target, workspaceRoot }) : null; + return useMemo( + () => ({ target, tooltipPath, onHoverIn, onPress, open }), + [target, tooltipPath, onHoverIn, onPress, open], + ); } export function useAssistantFileLinkActions(): AssistantFileLinkActions { diff --git a/packages/app/src/assistant-image/lifecycle.test.ts b/packages/app/src/assistant-image/lifecycle.test.ts deleted file mode 100644 index ed7729dd38..0000000000 --- a/packages/app/src/assistant-image/lifecycle.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { createAssistantImageLifecycle, transitionAssistantImageLifecycle } from "./lifecycle"; - -describe("assistant image lifecycle", () => { - it("keeps preview URL recreation in the public loading state", () => { - const loading = transitionAssistantImageLifecycle(createAssistantImageLifecycle(), { - type: "preview_created", - uri: "blob:first", - aspectRatio: null, - }); - const loaded = transitionAssistantImageLifecycle(loading, { - type: "image_loaded", - uri: "blob:first", - aspectRatio: 1.5, - }); - - const recreating = transitionAssistantImageLifecycle(loaded, { - type: "preview_released", - }); - - expect(recreating).toEqual({ status: "loading", uri: null, aspectRatio: null }); - }); - - it("publishes loaded only after the recreated URL loads", () => { - const recreating = transitionAssistantImageLifecycle(createAssistantImageLifecycle(), { - type: "preview_created", - uri: "blob:recreated", - aspectRatio: 1.5, - }); - - expect(recreating).toEqual({ - status: "loading", - uri: "blob:recreated", - aspectRatio: 1.5, - }); - - const recreated = transitionAssistantImageLifecycle(recreating, { - type: "image_loaded", - uri: "blob:recreated", - aspectRatio: 0.75, - }); - - expect(recreated).toEqual({ - status: "loaded", - uri: "blob:recreated", - aspectRatio: 0.75, - }); - }); - - it("does not reset a loaded image when the same preview is reported again", () => { - const loading = transitionAssistantImageLifecycle(createAssistantImageLifecycle(), { - type: "preview_created", - uri: "blob:current", - aspectRatio: null, - }); - const loaded = transitionAssistantImageLifecycle(loading, { - type: "image_loaded", - uri: "blob:current", - aspectRatio: 1.5, - }); - - const repeated = transitionAssistantImageLifecycle(loaded, { - type: "preview_created", - uri: "blob:current", - aspectRatio: 1.5, - }); - - expect(repeated).toBe(loaded); - }); - - it("publishes failed for a terminal image failure on the current URI", () => { - const loading = transitionAssistantImageLifecycle(createAssistantImageLifecycle(), { - type: "preview_created", - uri: "blob:current", - aspectRatio: null, - }); - const failed = transitionAssistantImageLifecycle(loading, { - type: "failed", - uri: "blob:current", - message: "Unable to load image preview.", - }); - - expect(failed).toEqual({ - status: "failed", - message: "Unable to load image preview.", - }); - }); - - it("ignores a stale load callback from a replaced URI", () => { - const current = transitionAssistantImageLifecycle(createAssistantImageLifecycle(), { - type: "preview_created", - uri: "blob:current", - aspectRatio: 1.25, - }); - - const afterStaleLoad = transitionAssistantImageLifecycle(current, { - type: "image_loaded", - uri: "blob:released", - aspectRatio: 2, - }); - - expect(afterStaleLoad).toEqual(current); - }); - - it("ignores a stale error callback from a replaced URI", () => { - const current = transitionAssistantImageLifecycle(createAssistantImageLifecycle(), { - type: "preview_created", - uri: "blob:current", - aspectRatio: null, - }); - - const afterStaleError = transitionAssistantImageLifecycle(current, { - type: "failed", - uri: "blob:released", - message: "Image unavailable", - }); - - expect(afterStaleError).toEqual(current); - }); -}); diff --git a/packages/app/src/assistant-image/lifecycle.ts b/packages/app/src/assistant-image/lifecycle.ts deleted file mode 100644 index bf0db18c97..0000000000 --- a/packages/app/src/assistant-image/lifecycle.ts +++ /dev/null @@ -1,46 +0,0 @@ -export type AssistantImageLifecycle = - | { status: "loading"; uri: string | null; aspectRatio: number | null } - | { status: "loaded"; uri: string; aspectRatio: number } - | { status: "failed"; message: string }; - -export type AssistantImageLifecycleEvent = - | { type: "preview_created"; uri: string; aspectRatio: number | null } - | { type: "preview_released" } - | { type: "image_loaded"; uri: string; aspectRatio: number } - | { type: "failed"; uri: string; message: string }; - -export function createAssistantImageLifecycle(): AssistantImageLifecycle { - return { status: "loading", uri: null, aspectRatio: null }; -} - -export function transitionAssistantImageLifecycle( - state: AssistantImageLifecycle, - event: AssistantImageLifecycleEvent, -): AssistantImageLifecycle { - if (event.type === "image_loaded") { - if (state.status !== "loading" || state.uri !== event.uri) { - return state; - } - return { - status: "loaded", - uri: event.uri, - aspectRatio: event.aspectRatio, - }; - } - if (event.type === "failed") { - if (state.status === "failed" || state.uri !== event.uri) { - return state; - } - return { status: "failed", message: event.message }; - } - if (event.type === "preview_created") { - if (state.status === "loaded" && state.uri === event.uri) { - return state; - } - return { status: "loading", uri: event.uri, aspectRatio: event.aspectRatio }; - } - if (event.type === "preview_released") { - return { status: "loading", uri: null, aspectRatio: null }; - } - return state; -} diff --git a/packages/app/src/assistant-image/load-dimensions.test.ts b/packages/app/src/assistant-image/load-dimensions.test.ts deleted file mode 100644 index a60f9b1b6d..0000000000 --- a/packages/app/src/assistant-image/load-dimensions.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - type AssistantImageRenderedDimensionsReader, - resolveAssistantImageLoadDimensions, -} from "./load-dimensions"; - -class MemoryRenderedDimensionsReader implements AssistantImageRenderedDimensionsReader { - constructor(private readonly dimensions: { width: number; height: number } | null) {} - - read(): { width: number; height: number } | null { - return this.dimensions; - } -} - -describe("assistant image load dimensions", () => { - it("prefers native source dimensions", () => { - const dimensions = resolveAssistantImageLoadDimensions({ - source: { width: 640, height: 320 }, - target: { naturalWidth: 800, naturalHeight: 400 }, - renderedImage: null, - renderedDimensions: new MemoryRenderedDimensionsReader({ width: 900, height: 600 }), - }); - - expect(dimensions).toEqual({ width: 640, height: 320 }); - }); - - it("uses browser event dimensions when native dimensions are absent", () => { - const dimensions = resolveAssistantImageLoadDimensions({ - source: { width: 0, height: 0 }, - target: { naturalWidth: 800, naturalHeight: 400 }, - renderedImage: null, - renderedDimensions: new MemoryRenderedDimensionsReader({ width: 900, height: 600 }), - }); - - expect(dimensions).toEqual({ width: 800, height: 400 }); - }); - - it("falls back to the rendered image adapter", () => { - const renderedImage = { id: "rendered-image" }; - const dimensions = resolveAssistantImageLoadDimensions({ - target: null, - renderedImage, - renderedDimensions: new MemoryRenderedDimensionsReader({ width: 900, height: 600 }), - }); - - expect(dimensions).toEqual({ width: 900, height: 600 }); - }); -}); diff --git a/packages/app/src/assistant-image/load-dimensions.ts b/packages/app/src/assistant-image/load-dimensions.ts deleted file mode 100644 index 069814ced8..0000000000 --- a/packages/app/src/assistant-image/load-dimensions.ts +++ /dev/null @@ -1,45 +0,0 @@ -export interface AssistantImageDimensions { - width: number; - height: number; -} - -interface AssistantImageDimensionCandidate { - width?: unknown; - height?: unknown; -} - -export interface AssistantImageRenderedDimensionsReader { - read(renderedImage: unknown): AssistantImageDimensions | null; -} - -function readPositiveDimensions( - candidate: AssistantImageDimensionCandidate | null | undefined, -): AssistantImageDimensions | null { - if ( - typeof candidate?.width !== "number" || - typeof candidate.height !== "number" || - candidate.width <= 0 || - candidate.height <= 0 - ) { - return null; - } - return { width: candidate.width, height: candidate.height }; -} - -export function resolveAssistantImageLoadDimensions(input: { - source?: AssistantImageDimensionCandidate | null; - target?: { naturalWidth?: unknown; naturalHeight?: unknown } | null; - renderedImage: unknown; - renderedDimensions: AssistantImageRenderedDimensionsReader; -}): AssistantImageDimensions | null { - const source = readPositiveDimensions(input.source); - if (source) { - return source; - } - - const target = readPositiveDimensions({ - width: input.target?.naturalWidth, - height: input.target?.naturalHeight, - }); - return target ?? input.renderedDimensions.read(input.renderedImage); -} diff --git a/packages/app/src/assistant-image/retry.test.ts b/packages/app/src/assistant-image/retry.test.ts deleted file mode 100644 index 9777663bed..0000000000 --- a/packages/app/src/assistant-image/retry.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { runAssistantImageOperationWithRetry } from "./retry"; - -describe("assistant image retry", () => { - it("recovers from transient failures while the image remains mounted", async () => { - const waits: number[] = []; - let attempts = 0; - - const result = await runAssistantImageOperationWithRetry({ - operation: async () => { - attempts += 1; - if (attempts < 3) { - throw new Error("transient"); - } - return "loaded"; - }, - delaysMs: [10, 20, 30], - wait: async (delayMs) => { - waits.push(delayMs); - }, - }); - - expect({ attempts, waits, result }).toEqual({ - attempts: 3, - waits: [10, 20], - result: "loaded", - }); - }); - - it("stops retrying after the image is released", async () => { - let stopped = false; - let attempts = 0; - - await expect( - runAssistantImageOperationWithRetry({ - operation: async () => { - attempts += 1; - throw new Error("transient"); - }, - delaysMs: [10, 20], - shouldStop: () => stopped, - wait: async () => { - stopped = true; - }, - }), - ).rejects.toThrow("transient"); - expect(attempts).toBe(1); - }); -}); diff --git a/packages/app/src/assistant-image/retry.ts b/packages/app/src/assistant-image/retry.ts deleted file mode 100644 index 4a2cf5c58a..0000000000 --- a/packages/app/src/assistant-image/retry.ts +++ /dev/null @@ -1,31 +0,0 @@ -export const ASSISTANT_IMAGE_RETRY_DELAYS_MS = [100, 400, 1_200] as const; - -export async function runAssistantImageOperationWithRetry(input: { - operation: () => Promise; - delaysMs?: readonly number[]; - shouldStop?: () => boolean; - wait?: (delayMs: number) => Promise; -}): Promise { - const delays = input.delaysMs ?? ASSISTANT_IMAGE_RETRY_DELAYS_MS; - const shouldStop = input.shouldStop ?? (() => false); - const wait = - input.wait ?? - (async (delayMs: number) => { - await new Promise((resolve) => setTimeout(resolve, delayMs)); - }); - - for (let attempt = 0; ; attempt += 1) { - try { - return await input.operation(); - } catch (error) { - const delay = delays[attempt]; - if (delay === undefined || shouldStop()) { - throw error; - } - await wait(delay); - if (shouldStop()) { - throw error; - } - } - } -} diff --git a/packages/app/src/assistant-image/use-assistant-image.ts b/packages/app/src/assistant-image/use-assistant-image.ts deleted file mode 100644 index 3314b7a398..0000000000 --- a/packages/app/src/assistant-image/use-assistant-image.ts +++ /dev/null @@ -1,544 +0,0 @@ -import { - useCallback, - useEffect, - useLayoutEffect, - useMemo, - useReducer, - useRef, - useState, -} from "react"; -import type { ImageLoadEvent } from "react-native"; -import { useTranslation } from "react-i18next"; -import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; -import type { AttachmentMetadata } from "@/attachments/types"; -import { isWeb } from "@/constants/platform"; -import { useStableEvent } from "@/hooks/use-stable-event"; -import { retainAttachmentForGarbageCollection } from "@/attachments/gc-retention"; -import { - persistAttachmentFromBytes, - persistAttachmentFromDataUrl, - releaseAttachmentPreviewUrl, - resolveAttachmentPreviewUrl, -} from "@/attachments/service"; -import { createPreviewAttachmentId, parseImageDataUrl } from "@/attachments/utils"; -import { - getAssistantImageMetadata, - setAssistantImageMetadata, -} from "@/utils/assistant-image-metadata"; -import { resolveAssistantImageSource } from "@/utils/assistant-image-source"; -import { createAssistantImageAcquisitionCache } from "./acquisition-cache"; -import { - createAssistantImageFileAcquisition, - type AssistantImageAcquisition, - type AssistantImageFileAcquisitionPort, -} from "./file-acquisition"; -import { - createAssistantImageLifecycle, - transitionAssistantImageLifecycle, - type AssistantImageLifecycle, - type AssistantImageLifecycleEvent, -} from "./lifecycle"; -import { - type AssistantImageRenderedDimensionsReader, - resolveAssistantImageLoadDimensions, -} from "./load-dimensions"; -import { runAssistantImageOperationWithRetry } from "./retry"; - -interface AssistantImageRenderBinding { - uri: string; - onRef: (instance: unknown) => void; - onLoad: (event: ImageLoadEvent) => void; - onError: () => void; -} - -const renderedDimensions: AssistantImageRenderedDimensionsReader = { - read(renderedImage) { - if (!isWeb || !(renderedImage instanceof HTMLElement)) { - return null; - } - const image = renderedImage.querySelector("img"); - return image && image.naturalWidth > 0 && image.naturalHeight > 0 - ? { width: image.naturalWidth, height: image.naturalHeight } - : null; - }, -}; - -export type AssistantImageResult = - | { - status: "loading"; - binding: AssistantImageRenderBinding | null; - aspectRatio: number | null; - } - | { - status: "loaded"; - binding: AssistantImageRenderBinding; - aspectRatio: number; - } - | { status: "failed"; message: string }; - -interface UseAssistantImageInput { - source: string; - occurrenceKey: string; - client?: DaemonClient | null; - workspaceRoot?: string; - serverId?: string; -} - -type PreviewUrlState = - | { status: "waiting" } - | { status: "loading" } - | { status: "loaded"; uri: string } - | { status: "failed"; error: unknown }; - -type AttachmentAcquisitionState = - | { status: "waiting" } - | { status: "loading" } - | { status: "loaded"; attachment: AttachmentMetadata } - | { status: "failed"; error: unknown }; - -interface DataImage { - mimeType: string; - base64: string; - cacheKey: string; -} - -const attachmentAcquisitionCache = createAssistantImageAcquisitionCache({ - capacity: 500, - onRetain: (attachment) => retainAttachmentForGarbageCollection(attachment.id), -}); - -interface CachedPreviewUrl { - attachment: AttachmentMetadata; - uri: string; -} - -const previewUrlCache = createAssistantImageAcquisitionCache({ - capacity: 500, - onRetain: - ({ attachment, uri }) => - () => { - void releaseAttachmentPreviewUrl({ attachment, url: uri }); - }, -}); - -const LOADED_IMAGE_CACHE_CAPACITY = 500; -const loadedImageCache = new Map(); - -function getLoadedImageAspectRatio(uri: string): number | null { - const aspectRatio = loadedImageCache.get(uri); - if (aspectRatio === undefined) { - return null; - } - loadedImageCache.delete(uri); - loadedImageCache.set(uri, aspectRatio); - return aspectRatio; -} - -function rememberLoadedImage(uri: string, aspectRatio: number): void { - loadedImageCache.delete(uri); - loadedImageCache.set(uri, aspectRatio); - if (loadedImageCache.size <= LOADED_IMAGE_CACHE_CAPACITY) { - return; - } - const leastRecentlyUsedUri = loadedImageCache.keys().next().value; - if (leastRecentlyUsedUri !== undefined) { - loadedImageCache.delete(leastRecentlyUsedUri); - } -} - -function useAttachmentAcquisition( - acquisition: AssistantImageAcquisition | null, -): AttachmentAcquisitionState { - const acquisitionKey = acquisition?.key ?? null; - const [entry, setEntry] = useState<{ - key: string | null; - state: AttachmentAcquisitionState; - }>(() => { - const cached = acquisitionKey ? attachmentAcquisitionCache.peek(acquisitionKey) : undefined; - return { - key: acquisitionKey, - state: cached ? { status: "loaded", attachment: cached } : { status: "waiting" }, - }; - }); - - useEffect(() => { - let disposed = false; - let releaseCurrent: (() => void) | null = null; - if (!acquisition || !acquisitionKey) { - setEntry({ key: null, state: { status: "waiting" } }); - return; - } - - const acquireCurrent = () => { - releaseCurrent?.(); - const retained = attachmentAcquisitionCache.acquireRetained( - acquisitionKey, - acquisition.locate, - ); - releaseCurrent = retained.release; - return retained; - }; - const initial = acquireCurrent(); - if (initial.value) { - setEntry({ key: acquisitionKey, state: { status: "loaded", attachment: initial.value } }); - return () => { - disposed = true; - releaseCurrent?.(); - }; - } - - setEntry({ key: acquisitionKey, state: { status: "loading" } }); - void (async () => { - let firstAttempt: ReturnType | null = initial; - try { - const attachment = await runAssistantImageOperationWithRetry({ - operation: async () => { - const retained = firstAttempt ?? acquireCurrent(); - firstAttempt = null; - return await retained.promise; - }, - shouldStop: () => disposed, - }); - if (!disposed) { - setEntry({ key: acquisitionKey, state: { status: "loaded", attachment } }); - } - } catch (error) { - if (!disposed) { - setEntry({ key: acquisitionKey, state: { status: "failed", error } }); - } - } - })(); - return () => { - disposed = true; - releaseCurrent?.(); - }; - }, [acquisition, acquisitionKey]); - - if (!acquisitionKey) { - return { status: "waiting" }; - } - const cached = attachmentAcquisitionCache.peek(acquisitionKey); - if (cached) { - return { status: "loaded", attachment: cached }; - } - return entry.key === acquisitionKey ? entry.state : { status: "waiting" }; -} - -function createDataImageAcquisition(input: { - source: string; - dataImage: DataImage | null; -}): AssistantImageAcquisition | null { - if (!input.dataImage) { - return null; - } - const { dataImage, source } = input; - return { - key: dataImage.cacheKey, - locate: async () => - await persistAttachmentFromDataUrl({ - id: createPreviewAttachmentId({ - mimeType: dataImage.mimeType, - contentLength: dataImage.base64.length, - contentKey: dataImage.cacheKey, - }), - dataUrl: source, - mimeType: dataImage.mimeType, - }), - }; -} - -function usePreviewUrl(attachment: AttachmentMetadata | null | undefined): PreviewUrlState { - const id = attachment?.id; - const storageType = attachment?.storageType; - const storageKey = attachment?.storageKey; - const mimeType = attachment?.mimeType; - const previewKey = - id && storageType && storageKey && mimeType - ? `${id}:${storageType}:${storageKey}:${mimeType}` - : null; - const [entry, setEntry] = useState<{ key: string | null; state: PreviewUrlState }>(() => { - return { - key: previewKey, - state: { status: "waiting" }, - }; - }); - const getCurrentAttachment = useStableEvent(() => attachment ?? null); - - useLayoutEffect(() => { - let disposed = false; - let releaseCurrent: (() => void) | null = null; - const current = getCurrentAttachment(); - - if (!current || !previewKey) { - setEntry({ key: null, state: { status: "waiting" } }); - return; - } - - const acquireCurrent = () => { - releaseCurrent?.(); - const retained = previewUrlCache.acquireRetained(previewKey, async () => ({ - attachment: current, - uri: await resolveAttachmentPreviewUrl(current), - })); - releaseCurrent = retained.release; - return retained; - }; - const initial = acquireCurrent(); - if (initial.value) { - setEntry({ key: previewKey, state: { status: "loaded", uri: initial.value.uri } }); - return () => { - disposed = true; - releaseCurrent?.(); - }; - } - - setEntry({ key: previewKey, state: { status: "loading" } }); - void (async () => { - let firstAttempt: ReturnType | null = initial; - try { - const preview = await runAssistantImageOperationWithRetry({ - operation: async () => { - const retained = firstAttempt ?? acquireCurrent(); - firstAttempt = null; - return await retained.promise; - }, - shouldStop: () => disposed, - }); - if (!disposed) { - setEntry({ key: previewKey, state: { status: "loaded", uri: preview.uri } }); - } - } catch (error) { - if (!disposed) { - setEntry({ key: previewKey, state: { status: "failed", error } }); - } - } - })(); - - return () => { - disposed = true; - releaseCurrent?.(); - }; - }, [getCurrentAttachment, previewKey]); - - if (!previewKey) { - return { status: "waiting" }; - } - return entry.key === previewKey ? entry.state : { status: "waiting" }; -} - -function lifecycleReducer( - state: AssistantImageLifecycle, - event: AssistantImageLifecycleEvent, -): AssistantImageLifecycle { - return transitionAssistantImageLifecycle(state, event); -} - -function errorMessage(error: unknown, fallback: string): string { - return error instanceof Error ? error.message : fallback; -} - -function getAcquisitionFailure(input: { - hasResolution: boolean; - isFileSource: boolean; - isDataImage: boolean; - fileAttachment: AttachmentAcquisitionState; - dataImageAttachment: AttachmentAcquisitionState; - preview: PreviewUrlState; - hasDirectUri: boolean; - fallbackMessage: string; -}): AssistantImageResult | null { - if (!input.hasResolution) { - return { status: "failed", message: input.fallbackMessage }; - } - if (input.isFileSource && input.fileAttachment.status === "failed") { - return { - status: "failed", - message: errorMessage(input.fileAttachment.error, input.fallbackMessage), - }; - } - if (input.isDataImage && input.dataImageAttachment.status === "failed") { - return { - status: "failed", - message: errorMessage(input.dataImageAttachment.error, input.fallbackMessage), - }; - } - if (!input.hasDirectUri && input.preview.status === "failed") { - return { - status: "failed", - message: errorMessage(input.preview.error, input.fallbackMessage), - }; - } - return null; -} - -export function useAssistantImage({ - source, - occurrenceKey, - client, - workspaceRoot, - serverId, -}: UseAssistantImageInput): AssistantImageResult { - const { t } = useTranslation(); - const resolution = useMemo( - () => resolveAssistantImageSource({ source, workspaceRoot }), - [source, workspaceRoot], - ); - const dataImage = useMemo(() => parseImageDataUrl(source), [source]); - const fileAcquisition = useMemo(() => { - const port: AssistantImageFileAcquisitionPort | null = client - ? { - readFile: async (cwd, path) => await client.readFile(cwd, path), - persist: persistAttachmentFromBytes, - } - : null; - return createAssistantImageFileAcquisition({ - port, - resolution, - serverId, - occurrenceKey, - unavailableMessage: t("message.attachments.imagePreviewUnavailable"), - }); - }, [client, occurrenceKey, resolution, serverId, t]); - const dataImageAcquisition = useMemo( - () => createDataImageAcquisition({ source, dataImage }), - [dataImage, source], - ); - const fileAttachment = useAttachmentAcquisition(fileAcquisition); - const dataImageAttachment = useAttachmentAcquisition(dataImageAcquisition); - const filePreview = usePreviewUrl( - fileAttachment.status === "loaded" ? fileAttachment.attachment : null, - ); - const dataImagePreview = usePreviewUrl( - dataImageAttachment.status === "loaded" ? dataImageAttachment.attachment : null, - ); - const directUri = resolution?.kind === "direct" && !dataImage ? resolution.uri : null; - const preview = dataImage ? dataImagePreview : filePreview; - const previewUri = preview.status === "loaded" ? preview.uri : null; - const uri = directUri ?? previewUri; - const cachedAspectRatio = useMemo( - () => getAssistantImageMetadata({ source, workspaceRoot, serverId })?.aspectRatio ?? null, - [serverId, source, workspaceRoot], - ); - const [lifecycle, dispatchLifecycle] = useReducer( - lifecycleReducer, - uri, - (initialUri): AssistantImageLifecycle => { - if (initialUri) { - const aspectRatio = getLoadedImageAspectRatio(initialUri); - if (aspectRatio !== null) { - return { status: "loaded", uri: initialUri, aspectRatio }; - } - } - return createAssistantImageLifecycle(); - }, - ); - const dispatch = useCallback((event: AssistantImageLifecycleEvent) => { - if (event.type === "image_loaded") { - rememberLoadedImage(event.uri, event.aspectRatio); - } else if (event.type === "failed" && event.uri) { - loadedImageCache.delete(event.uri); - } - dispatchLifecycle(event); - }, []); - const renderedImageRef = useRef(null); - const handleImageRef = useCallback((instance: unknown) => { - renderedImageRef.current = instance; - }, []); - - useEffect(() => { - if (!uri) { - dispatch({ type: "preview_released" }); - return; - } - - dispatch({ - type: "preview_created", - uri, - aspectRatio: cachedAspectRatio, - }); - }, [cachedAspectRatio, dispatch, uri]); - - const handleImageError = useCallback(() => { - if (uri) { - dispatch({ - type: "failed", - uri, - message: t("message.attachments.imageUnavailable"), - }); - } - }, [dispatch, t, uri]); - const handleImageLoad = useCallback( - (event: ImageLoadEvent) => { - if (!uri) { - return; - } - const nativeEvent = event.nativeEvent as ImageLoadEvent["nativeEvent"] & { - target?: { naturalWidth?: unknown; naturalHeight?: unknown }; - }; - const dimensions = resolveAssistantImageLoadDimensions({ - source: nativeEvent.source, - target: nativeEvent.target, - renderedImage: renderedImageRef.current, - renderedDimensions, - }); - const metadata = dimensions - ? setAssistantImageMetadata({ source, workspaceRoot, serverId }, dimensions) - : null; - const aspectRatio = metadata?.aspectRatio ?? cachedAspectRatio; - if (!aspectRatio) { - dispatch({ - type: "failed", - uri, - message: t("message.attachments.imageUnavailable"), - }); - return; - } - dispatch({ type: "image_loaded", uri, aspectRatio }); - }, - [cachedAspectRatio, dispatch, serverId, source, t, uri, workspaceRoot], - ); - - const acquisitionFailure = getAcquisitionFailure({ - hasResolution: resolution !== null, - isFileSource: resolution?.kind === "file_rpc", - isDataImage: dataImage !== null, - fileAttachment, - dataImageAttachment, - preview, - hasDirectUri: directUri !== null, - fallbackMessage: t("message.attachments.imagePreviewLoadFailed"), - }); - if (acquisitionFailure) { - return acquisitionFailure; - } - const hasCurrentLifecycleUri = lifecycle.status !== "failed" && lifecycle.uri === uri; - let binding: AssistantImageRenderBinding | null = null; - if (hasCurrentLifecycleUri && lifecycle.uri) { - binding = { - uri: lifecycle.uri, - onRef: handleImageRef, - onLoad: handleImageLoad, - onError: handleImageError, - }; - } - if (lifecycle.status === "loaded" && lifecycle.uri === uri) { - return { - status: "loaded", - binding: { - uri: lifecycle.uri, - onRef: handleImageRef, - onLoad: handleImageLoad, - onError: handleImageError, - }, - aspectRatio: lifecycle.aspectRatio, - }; - } - if (lifecycle.status === "failed") { - return lifecycle; - } - return { - status: "loading", - binding, - aspectRatio: (hasCurrentLifecycleUri ? lifecycle.aspectRatio : null) ?? cachedAspectRatio, - }; -} diff --git a/packages/app/src/assistant-selection-copy/content.browser.test.ts b/packages/app/src/assistant-selection-copy/content.browser.test.ts index aecb4ba5b4..5c4fcfb331 100644 --- a/packages/app/src/assistant-selection-copy/content.browser.test.ts +++ b/packages/app/src/assistant-selection-copy/content.browser.test.ts @@ -760,3 +760,58 @@ describe("assistant selection copy inside highlighted code", () => { ); }); }); + +describe("semantic library output", () => { + it("copies a partial strong span as plain text", () => { + const host = document.createElement("div"); + host.innerHTML = + '

Before bold text after

'; + document.body.append(host); + const text = host.querySelector("strong")!.firstChild!; + const range = document.createRange(); + range.setStart(text, 1); + range.setEnd(text, 4); + const selection = window.getSelection()!; + selection.removeAllRanges(); + selection.addRange(range); + expect(createAssistantSelectionClipboardContent(selection)?.plainText).toBe("old"); + expect(createAssistantSelectionClipboardContent(selection)?.html).not.toContain(""); + host.remove(); + }); +}); + +it("copies native semantic ordered lists with their original numbers", () => { + const host = document.createElement("div"); + host.innerHTML = + '
  1. Third item
  2. Fourth item
'; + document.body.append(host); + const selection = selectNodeContents(host.querySelector("ol")!); + const content = createAssistantSelectionClipboardContent(selection); + expect(content?.plainText).toContain("3. Third item"); + expect(content?.plainText).toContain("4. Fourth item"); + host.remove(); +}); + +it("copies a selection inside semantic inline code without Markdown escaping", () => { + const message = mountFixture(); + message.innerHTML = "

Run apply_patch now.

"; + const code = fixtureElement(message, "code"); + expect(createAssistantSelectionClipboardContent(selectText(code, 0, 11))?.plainText).toBe( + "apply_patch", + ); +}); + +it("copies a workspace image path instead of its temporary display URL", () => { + const message = mountFixture(); + const sources = JSON.stringify({ "blob:preview": "assets/chart.png" }); + message.innerHTML = '
chart
'; + message.firstElementChild!.setAttribute("data-paseo-markdown-image-sources", sources); + const selection = window.getSelection()!; + const range = document.createRange(); + range.selectNode(message.querySelector("img")!); + selection.removeAllRanges(); + selection.addRange(range); + expect(createAssistantSelectionClipboardContent(selection)?.plainText).toBe( + "![chart](assets/chart.png)", + ); +}); diff --git a/packages/app/src/assistant-selection-copy/content.web.ts b/packages/app/src/assistant-selection-copy/content.web.ts index e59a71aeca..250d60d761 100644 --- a/packages/app/src/assistant-selection-copy/content.web.ts +++ b/packages/app/src/assistant-selection-copy/content.web.ts @@ -6,6 +6,7 @@ import { type MarkdownClipboardContent, } from "@/utils/rich-clipboard"; import { + MARKDOWN_COPY_IMAGE_SOURCES_ATTRIBUTE, MARKDOWN_COPY_ALIGN_ATTRIBUTE, MARKDOWN_COPY_ALT_ATTRIBUTE, MARKDOWN_COPY_IGNORE_ATTRIBUTE, @@ -22,9 +23,38 @@ const ASSISTANT_MESSAGE_SELECTOR = '[data-testid="assistant-message"]'; const MESSAGE_ROW_SELECTOR = "[data-message-id]"; const CHAT_SCROLL_SELECTOR = '[data-testid="agent-chat-scroll"]'; const messageRowSelector = (messageId: string) => `[data-message-id="${CSS.escape(messageId)}"]`; -const CODE_BLOCK_SELECTOR = `[${MARKDOWN_COPY_TAG_ATTRIBUTE}="pre"]`; -const CODE_REGION_SELECTOR = `${CODE_BLOCK_SELECTOR}, [${MARKDOWN_COPY_TAG_ATTRIBUTE}="code"]`; -const IMAGE_FRAME_SELECTOR = `[${MARKDOWN_COPY_TAG_ATTRIBUTE}="img"]`; +const CODE_BLOCK_SELECTOR = `pre, [${MARKDOWN_COPY_TAG_ATTRIBUTE}="pre"]`; +const CODE_REGION_SELECTOR = `${CODE_BLOCK_SELECTOR}, code, [${MARKDOWN_COPY_TAG_ATTRIBUTE}="code"]`; +const IMAGE_FRAME_SELECTOR = `img, [${MARKDOWN_COPY_TAG_ATTRIBUTE}="img"]`; + +const SEMANTIC_TAGS = + "p,blockquote,br,code,pre,h1,h2,h3,h4,h5,h6,hr,img,li,ol,ul,strong,em,s,del,table,thead,tbody,tr,th,td"; +function semanticTag(element: Element): string | null { + return ( + element.getAttribute(MARKDOWN_COPY_TAG_ATTRIBUTE) ?? + (element.matches(SEMANTIC_TAGS) ? element.tagName.toLowerCase() : null) + ); +} +function copySemanticAttributes(original: Element, copy: Element): void { + const tag = semanticTag(original); + if (!tag || original.hasAttribute(MARKDOWN_COPY_TAG_ATTRIBUTE)) return; + copy.setAttribute(MARKDOWN_COPY_TAG_ATTRIBUTE, tag); + if (tag === "ol") + copy.setAttribute(MARKDOWN_COPY_LIST_START_ATTRIBUTE, original.getAttribute("start") ?? "1"); + if (tag === "img") { + const url = original.getAttribute("src") ?? ""; + const encodedSources = original + .closest(`[${MARKDOWN_COPY_IMAGE_SOURCES_ATTRIBUTE}]`) + ?.getAttribute(MARKDOWN_COPY_IMAGE_SOURCES_ATTRIBUTE); + const sources: Record = encodedSources ? JSON.parse(encodedSources) : {}; + copy.setAttribute(MARKDOWN_COPY_SRC_ATTRIBUTE, sources[url] ?? url); + copy.setAttribute(MARKDOWN_COPY_ALT_ATTRIBUTE, original.getAttribute("alt") ?? ""); + } + if (tag === "td" || tag === "th") { + const align = (original as HTMLElement).style.textAlign; + if (align) copy.setAttribute(MARKDOWN_COPY_ALIGN_ATTRIBUTE, align); + } +} const turndown = new TurndownService({ bulletListMarker: "-", @@ -247,6 +277,7 @@ function cloneMarkdownSelection(range: Range, message: Element): Node { while (ancestor && ancestor !== message) { if (shouldPreserveSemanticElement(range, ancestor)) { const wrapper = ancestor.cloneNode(false) as Element; + copySemanticAttributes(ancestor, wrapper); normalizeOrderedListStart(wrapper, ancestor, range); wrapper.append(selected); selected = wrapper; @@ -294,25 +325,26 @@ function nodeAtPath(root: Node, path: number[]): Node { } function removeUnselectedSemantics(range: Range, source: Element, clone: Element): void { - const selector = `[${MARKDOWN_COPY_TAG_ATTRIBUTE}], a`; + const selector = `[${MARKDOWN_COPY_TAG_ATTRIBUTE}], a, ${SEMANTIC_TAGS}`; const originals = Array.from(source.querySelectorAll(selector)); const copies = Array.from(clone.querySelectorAll(selector)); originals.forEach((original, index) => { const copy = copies[index]; + if (copy) copySemanticAttributes(original, copy); if (!copy || shouldPreserveSemanticElement(range, original)) { if (copy) { normalizeOrderedListStart(copy, original, range); } return; } - const tag = original.getAttribute(MARKDOWN_COPY_TAG_ATTRIBUTE); + const tag = semanticTag(original); if (tag && isBlockBoundary(tag)) { copy.setAttribute(MARKDOWN_COPY_TAG_ATTRIBUTE, "p"); return; } copy.removeAttribute(MARKDOWN_COPY_TAG_ATTRIBUTE); - if (copy.tagName === "A") { + if (copy.matches(SEMANTIC_TAGS) || copy.tagName === "A") { copy.setAttribute(MARKDOWN_COPY_UNWRAP_ATTRIBUTE, "true"); } }); @@ -322,7 +354,7 @@ function shouldPreserveSemanticElement(range: Range, element: Element): boolean if (!range.intersectsNode(element)) { return false; } - const tag = element.getAttribute(MARKDOWN_COPY_TAG_ATTRIBUTE); + const tag = semanticTag(element); // An image has no part to select, so touching it selects all of it. if (tag === "p" || tag === "img" || isTableStructure(tag)) { return true; @@ -340,8 +372,16 @@ function shouldPreserveSemanticElement(range: Range, element: Element): boolean (item) => hasSelectedListMarker(range, item) || hasSelectedAllContents(range, item, true), ); } - if (tag === "li" && hasSelectedListMarker(range, element)) { - return true; + if (tag === "li") { + if (hasSelectedListMarker(range, element)) return true; + // CSS list markers are outside text nodes. Starting inside a semantic item + // excludes its marker even when all of the label's characters are selected. + if ( + element.tagName === "LI" && + range.startContainer !== element && + element.contains(range.startContainer) + ) + return false; } return hasSelectedAllContents(range, element, tag === "li"); } @@ -358,7 +398,7 @@ function selectionStaysInsideElement(range: Range, element: Element): boolean { function selectedListItems(list: Element, range: Range): Element[] { return Array.from(list.children).filter( (child) => - child.getAttribute(MARKDOWN_COPY_TAG_ATTRIBUTE) === "li" && + semanticTag(child) === "li" && (child.contains(range.startContainer) || child.contains(range.endContainer) || hasSelectedAllContents(range, child, true)), @@ -368,7 +408,7 @@ function selectedListItems(list: Element, range: Range): Element[] { function hasSelectedListMarker(range: Range, item: Element): boolean { const marker = item.querySelector(`:scope > [${MARKDOWN_COPY_LIST_MARKER_ATTRIBUTE}]`); if (!marker) { - return false; + return item.tagName === "LI" && range.startContainer === item && range.startOffset === 0; } const markerContents = document.createRange(); @@ -391,18 +431,20 @@ function hasSelectedListMarker(range: Range, item: Element): boolean { } function normalizeOrderedListStart(copy: Element, original: Element, range: Range): void { - if (original.getAttribute(MARKDOWN_COPY_TAG_ATTRIBUTE) !== "ol") { + if (semanticTag(original) !== "ol") { return; } const firstSelectedItem = selectedListItems(original, range)[0]; if (!firstSelectedItem) { return; } - const items = Array.from(original.children).filter( - (child) => child.getAttribute(MARKDOWN_COPY_TAG_ATTRIBUTE) === "li", - ); + const items = Array.from(original.children).filter((child) => semanticTag(child) === "li"); const omittedItems = items.indexOf(firstSelectedItem); - const originalStart = Number(original.getAttribute(MARKDOWN_COPY_LIST_START_ATTRIBUTE) ?? 1); + const originalStart = Number( + original.getAttribute(MARKDOWN_COPY_LIST_START_ATTRIBUTE) ?? + original.getAttribute("start") ?? + 1, + ); copy.setAttribute(MARKDOWN_COPY_LIST_START_ATTRIBUTE, String(originalStart + omittedItems)); } diff --git a/packages/app/src/assistant-selection-copy/markup.ts b/packages/app/src/assistant-selection-copy/markup.ts index 91c6c1618a..7b4a2a1572 100644 --- a/packages/app/src/assistant-selection-copy/markup.ts +++ b/packages/app/src/assistant-selection-copy/markup.ts @@ -19,44 +19,11 @@ export const MARKDOWN_COPY_ALT_ATTRIBUTE = "data-paseo-markdown-alt"; export const TRAILING_CODE_LINE_BREAKS = /(\r?\n[ \t]*)+$/; export const markdownCopyDataSet = { - blockquote: { paseoMarkdownTag: "blockquote" }, - br: { paseoMarkdownTag: "br" }, code: { paseoMarkdownTag: "code" }, - h1: { paseoMarkdownTag: "h1" }, - h2: { paseoMarkdownTag: "h2" }, - h3: { paseoMarkdownTag: "h3" }, - h4: { paseoMarkdownTag: "h4" }, - h5: { paseoMarkdownTag: "h5" }, - h6: { paseoMarkdownTag: "h6" }, - hr: { paseoMarkdownTag: "hr" }, - ignore: { paseoMarkdownIgnore: "true" }, - li: { paseoMarkdownTag: "li" }, - listMarker: { paseoMarkdownIgnore: "true", paseoMarkdownListMarker: "true" }, - ol: { paseoMarkdownTag: "ol" }, - p: { paseoMarkdownTag: "p" }, pre: { paseoMarkdownTag: "pre" }, - s: { paseoMarkdownTag: "s" }, - strong: { paseoMarkdownTag: "strong" }, - em: { paseoMarkdownTag: "em" }, - table: { paseoMarkdownTag: "table" }, - tbody: { paseoMarkdownTag: "tbody" }, - td: { paseoMarkdownTag: "td" }, - th: { paseoMarkdownTag: "th" }, - thead: { paseoMarkdownTag: "thead" }, - tr: { paseoMarkdownTag: "tr" }, - ul: { paseoMarkdownTag: "ul" }, - unwrap: { paseoMarkdownUnwrap: "true" }, + ignore: { paseoMarkdownIgnore: "true" }, } as const; -export type MarkdownCopyInlineTag = "br" | "code" | "em" | "s" | "strong"; - -export function markdownCopyOrderedListDataSet(start: unknown) { - return { - ...markdownCopyDataSet.ol, - paseoMarkdownListStart: String(start ?? 1), - } as const; -} - export function markdownCopyCodeBlockDataSet(language: string | null | undefined) { const fenceLanguage = language?.trim().split(/\s+/)[0]; return { @@ -65,25 +32,7 @@ export function markdownCopyCodeBlockDataSet(language: string | null | undefined } as const; } -/** - * An image copies as the Markdown it came from. The rendered `img` points at a preview - * URL for data and workspace images, and carries no alt text of its own. - */ -export function markdownCopyImageDataSet(source: string, alt: string | undefined) { - return { - paseoMarkdownTag: "img", - paseoMarkdownSrc: source, - ...(alt ? { paseoMarkdownAlt: alt } : {}), - } as const; -} - -export function markdownCopyTableCellDataSet(tag: "td" | "th", style: unknown) { - const alignment = - typeof style === "string" - ? style.match(/(?:^|;)\s*text-align\s*:\s*(left|right|center)/i)?.[1] - : null; - return { - ...markdownCopyDataSet[tag], - ...(alignment ? { paseoMarkdownAlign: alignment.toLowerCase() } : {}), - } as const; +export const MARKDOWN_COPY_IMAGE_SOURCES_ATTRIBUTE = "data-paseo-markdown-image-sources"; +export function markdownCopyImageSourcesDataSet(sources: Record) { + return { paseoMarkdownImageSources: JSON.stringify(sources) }; } diff --git a/packages/app/src/audio/audio-engine-types.ts b/packages/app/src/audio/audio-engine-types.ts index 90bec732fb..d96f9c5ef9 100644 --- a/packages/app/src/audio/audio-engine-types.ts +++ b/packages/app/src/audio/audio-engine-types.ts @@ -2,6 +2,7 @@ export interface AudioEngineCallbacks { onCaptureData(pcm: Uint8Array): void; onVolumeLevel(level: number): void; onInterruption?(): void; + onCaptureAvailabilityChange?(available: boolean): void; onError?(error: Error): void; } @@ -25,3 +26,8 @@ export interface AudioEngine { clearQueue(): void; isPlaying(): boolean; } + +export interface AudioEngineOptions { + traceLabel?: string; + captureMode?: "conversation" | "dictation"; +} diff --git a/packages/app/src/audio/audio-engine.native.ts b/packages/app/src/audio/audio-engine.native.ts index 86a6fc3d26..ed9f15a765 100644 --- a/packages/app/src/audio/audio-engine.native.ts +++ b/packages/app/src/audio/audio-engine.native.ts @@ -1,4 +1,9 @@ -import type { AudioEngine, AudioEngineCallbacks, AudioPlaybackSource } from "./audio-engine-types"; +import type { + AudioEngine, + AudioEngineCallbacks, + AudioEngineOptions, + AudioPlaybackSource, +} from "./audio-engine-types"; import { createAudioPlayer, setAudioModeAsync } from "expo-audio"; import { File, Paths } from "expo-file-system"; @@ -8,7 +13,7 @@ import { playPcm16 } from "./pcm"; export function createAudioEngine( callbacks: AudioEngineCallbacks, - _options?: { traceLabel?: string }, + options?: AudioEngineOptions, ): AudioEngine { const native = require("@getpaseo/expo-two-way-audio"); @@ -47,6 +52,12 @@ export function createAudioEngine( const interruptionSubscription = native.addExpoTwoWayAudioEventListener( "onAudioInterruption", (event: { data: string }) => { + if (event.data === "capture_paused" || event.data === "capture_resumed") { + if (refs.captureActive) { + callbacks.onCaptureAvailabilityChange?.(event.data === "capture_resumed"); + } + return; + } if (event.data !== "blocked") { return; } @@ -180,7 +191,10 @@ export function createAudioEngine( try { await ensureMicrophonePermission(); await ensureInitialized(); - const isRecording = native.toggleRecording(true); + const isRecording = + options?.captureMode === "dictation" + ? native.startDictation() + : native.toggleRecording(true); if (!isRecording) { throw new Error( "Microphone capture could not start because Android audio focus is unavailable.", @@ -201,6 +215,7 @@ export function createAudioEngine( refs.captureActive = false; refs.muted = false; callbacks.onVolumeLevel(0); + callbacks.onCaptureAvailabilityChange?.(true); releaseSessionIfIdle(); }, diff --git a/packages/app/src/audio/audio-engine.web.ts b/packages/app/src/audio/audio-engine.web.ts index dc0f049ca7..5516575e69 100644 --- a/packages/app/src/audio/audio-engine.web.ts +++ b/packages/app/src/audio/audio-engine.web.ts @@ -1,6 +1,11 @@ import { parsePcmSampleRate, resampleToPcm16 } from "./pcm"; import { isElectronRuntime } from "@/desktop/host"; -import type { AudioEngine, AudioEngineCallbacks, AudioPlaybackSource } from "./audio-engine-types"; +import type { + AudioEngine, + AudioEngineCallbacks, + AudioEngineOptions, + AudioPlaybackSource, +} from "./audio-engine-types"; import { createPlaybackQueue } from "./playback"; @@ -46,7 +51,7 @@ async function decodeAudioData(context: AudioContext, buffer: ArrayBuffer): Prom export function createAudioEngine( callbacks: AudioEngineCallbacks, - _options?: { traceLabel?: string }, + _options?: AudioEngineOptions, ): AudioEngine { const refs: { playbackContext: AudioContext | null; diff --git a/packages/app/src/components/assistant-message-render-limit.native.ts b/packages/app/src/components/assistant-message-render-limit.native.ts new file mode 100644 index 0000000000..9c80563cd9 --- /dev/null +++ b/packages/app/src/components/assistant-message-render-limit.native.ts @@ -0,0 +1,11 @@ +import type { CappedAssistantMessage } from "./assistant-message-render-limit.shared"; +export { + getUtf8ByteLength, + type CappedAssistantMessage, +} from "./assistant-message-render-limit.shared"; + +// Native passes a single string to the C++ parser, without the web renderer's +// React-node fan-out. Keep complete messages selectable across all paragraphs. +export function capAssistantMessageForRender(message: string): CappedAssistantMessage { + return { text: message, capped: false }; +} diff --git a/packages/app/src/components/assistant-message-render-limit.shared.ts b/packages/app/src/components/assistant-message-render-limit.shared.ts new file mode 100644 index 0000000000..429770ecf5 --- /dev/null +++ b/packages/app/src/components/assistant-message-render-limit.shared.ts @@ -0,0 +1,30 @@ +export interface CappedAssistantMessage { + text: string; + capped: boolean; +} + +export function getUtf8ByteLength(message: string): number { + let bytes = 0; + + for (let index = 0; index < message.length; index += 1) { + const codeUnit = message.charCodeAt(index); + if (codeUnit < 0x80) { + bytes += 1; + } else if (codeUnit < 0x800) { + bytes += 2; + } else if ( + codeUnit >= 0xd800 && + codeUnit <= 0xdbff && + index + 1 < message.length && + message.charCodeAt(index + 1) >= 0xdc00 && + message.charCodeAt(index + 1) <= 0xdfff + ) { + bytes += 4; + index += 1; + } else { + bytes += 3; + } + } + + return bytes; +} diff --git a/packages/app/src/components/assistant-message-render-limit.test.ts b/packages/app/src/components/assistant-message-render-limit.test.ts index 52a7799633..be7058e51b 100644 --- a/packages/app/src/components/assistant-message-render-limit.test.ts +++ b/packages/app/src/components/assistant-message-render-limit.test.ts @@ -56,3 +56,11 @@ describe("assistant message render limit", () => { }); }); }); + +it("preserves complete native multi-paragraph messages and the newline-heavy repro", async () => { + const native = await import("./assistant-message-render-limit.native"); + const message = "Paragraph.\n\n".repeat(3000) + "Section 60"; + expect(native.capAssistantMessageForRender(message)).toEqual({ text: message, capped: false }); + const lines = "x\n".repeat(16_000) + "HIDDEN_OVERSIZED_MESSAGE_TAIL"; + expect(native.capAssistantMessageForRender(lines)).toEqual({ text: lines, capped: false }); +}); diff --git a/packages/app/src/components/assistant-message-render-limit.ts b/packages/app/src/components/assistant-message-render-limit.ts index 0c37270cb5..6c968ad6f9 100644 --- a/packages/app/src/components/assistant-message-render-limit.ts +++ b/packages/app/src/components/assistant-message-render-limit.ts @@ -1,55 +1 @@ -const graphemeSegmenter = - typeof Intl.Segmenter === "function" - ? new Intl.Segmenter(undefined, { granularity: "grapheme" }) - : null; - -export const ASSISTANT_MESSAGE_RENDER_CHARACTER_LIMIT = 32_000; - -export interface CappedAssistantMessage { - text: string; - capped: boolean; -} - -export function getUtf8ByteLength(message: string): number { - let bytes = 0; - - for (let index = 0; index < message.length; index += 1) { - const codeUnit = message.charCodeAt(index); - if (codeUnit < 0x80) { - bytes += 1; - } else if (codeUnit < 0x800) { - bytes += 2; - } else if ( - codeUnit >= 0xd800 && - codeUnit <= 0xdbff && - index + 1 < message.length && - message.charCodeAt(index + 1) >= 0xdc00 && - message.charCodeAt(index + 1) <= 0xdfff - ) { - bytes += 4; - index += 1; - } else { - bytes += 3; - } - } - - return bytes; -} - -export function capAssistantMessageForRender(message: string): CappedAssistantMessage { - if (message.length <= ASSISTANT_MESSAGE_RENDER_CHARACTER_LIMIT) { - return { text: message, capped: false }; - } - - let end = ASSISTANT_MESSAGE_RENDER_CHARACTER_LIMIT; - if (graphemeSegmenter) { - end = graphemeSegmenter.segment(message).containing(end)!.index; - } else { - const finalCodeUnit = message.charCodeAt(end - 1); - if (finalCodeUnit >= 0xd800 && finalCodeUnit <= 0xdbff) { - end -= 1; - } - } - - return { text: message.slice(0, end), capped: true }; -} +export * from "./assistant-message-render-limit.web"; diff --git a/packages/app/src/components/assistant-message-render-limit.web.ts b/packages/app/src/components/assistant-message-render-limit.web.ts new file mode 100644 index 0000000000..eee6436595 --- /dev/null +++ b/packages/app/src/components/assistant-message-render-limit.web.ts @@ -0,0 +1,30 @@ +import type { CappedAssistantMessage } from "./assistant-message-render-limit.shared"; +export { + getUtf8ByteLength, + type CappedAssistantMessage, +} from "./assistant-message-render-limit.shared"; + +const graphemeSegmenter = + typeof Intl.Segmenter === "function" + ? new Intl.Segmenter(undefined, { granularity: "grapheme" }) + : null; + +export const ASSISTANT_MESSAGE_RENDER_CHARACTER_LIMIT = 32_000; + +export function capAssistantMessageForRender(message: string): CappedAssistantMessage { + if (message.length <= ASSISTANT_MESSAGE_RENDER_CHARACTER_LIMIT) { + return { text: message, capped: false }; + } + + let end = ASSISTANT_MESSAGE_RENDER_CHARACTER_LIMIT; + if (graphemeSegmenter) { + end = graphemeSegmenter.segment(message).containing(end)!.index; + } else { + const finalCodeUnit = message.charCodeAt(end - 1); + if (finalCodeUnit >= 0xd800 && finalCodeUnit <= 0xdbff) { + end -= 1; + } + } + + return { text: message.slice(0, end), capped: true }; +} diff --git a/packages/app/src/components/dictation-controls.tsx b/packages/app/src/components/dictation-controls.tsx index 5c8a990987..2191f0d480 100644 --- a/packages/app/src/components/dictation-controls.tsx +++ b/packages/app/src/components/dictation-controls.tsx @@ -87,7 +87,9 @@ export function DictationControls({ - {formatDuration(duration)} + + {status === "paused" ? t("message.dictation.paused") : formatDuration(duration)} + {formatDuration(duration)} + {status === "paused" ? ( + + {t("message.dictation.paused")} + + ) : null} {isFailed ? ( {errorText diff --git a/packages/app/src/components/highlighted-code-block.tsx b/packages/app/src/components/highlighted-code-block.tsx index 86fc3801a3..292ec53618 100644 --- a/packages/app/src/components/highlighted-code-block.tsx +++ b/packages/app/src/components/highlighted-code-block.tsx @@ -1,7 +1,13 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { Pressable, View, type StyleProp, type TextStyle, type ViewStyle } from "react-native"; +import { + Pressable, + View, + Text, + type StyleProp, + type TextStyle, + type ViewStyle, +} from "react-native"; import { StyleSheet } from "react-native-unistyles"; -import { MarkdownTextSpan } from "@/components/markdown-text"; import * as Clipboard from "expo-clipboard"; import { Check, Copy } from "lucide-react-native"; import { useTranslation } from "react-i18next"; @@ -96,13 +102,13 @@ export const HighlightedCodeBlock = React.memo(function HighlightedCodeBlock({ onPointerLeave={handlePointerLeave} > {keyedLines ? ( - + {renderCodeSegments(keyedLines)} - + ) : ( - + {renderedCode} - +
)} @@ -129,9 +135,7 @@ interface TokenSpanProps { const TokenSpan = React.memo(function TokenSpan({ token }: TokenSpanProps) { return ( - - {token.text} - + {token.text} ); }); @@ -140,7 +144,7 @@ interface CodeTextSpanProps { } const CodeTextSpan = React.memo(function CodeTextSpan({ text }: CodeTextSpanProps) { - return {text}; + return {text}; }); interface SplitStyles { diff --git a/packages/app/src/components/left-sidebar.tsx b/packages/app/src/components/left-sidebar.tsx index f054668347..943c1edd27 100644 --- a/packages/app/src/components/left-sidebar.tsx +++ b/packages/app/src/components/left-sidebar.tsx @@ -32,6 +32,7 @@ import { HostPicker } from "@/components/hosts/host-picker"; import { SidebarDisplayPreferencesMenu } from "@/components/sidebar/display-preferences/menu"; import { SidebarSeparator } from "@/components/sidebar/sidebar-separator"; import { SidebarNavRows } from "@/components/sidebar/sidebar-nav-rows"; +import { SidebarFooterIconHighlight } from "@/components/sidebar/sidebar-footer-icon-highlight"; import { SidebarHelpMenu } from "@/components/sidebar/sidebar-help-menu"; import { SidebarResizeHandle } from "@/components/sidebar-resize-handle"; import { DensityIcon } from "@/components/ui/density-icon"; @@ -317,13 +318,16 @@ function FooterIconButton({ accessibilityRole="button" onPress={onPress} > - {({ hovered }) => ( - + {({ hovered, pressed }) => ( + <> + + + )} diff --git a/packages/app/src/components/markdown-text-selection.test.ts b/packages/app/src/components/markdown-text-selection.test.ts deleted file mode 100644 index 6aa42afa00..0000000000 --- a/packages/app/src/components/markdown-text-selection.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { iosMarkdownTextIsSelectable } from "./markdown-text-selection"; - -describe("markdown text selection", () => { - it("uses plain text only for iOS table cells", () => { - expect({ - tableCell: iosMarkdownTextIsSelectable("table-cell"), - prose: iosMarkdownTextIsSelectable("prose"), - }).toEqual({ - tableCell: false, - prose: true, - }); - }); -}); diff --git a/packages/app/src/components/markdown-text-selection.tsx b/packages/app/src/components/markdown-text-selection.tsx deleted file mode 100644 index f83ff6a258..0000000000 --- a/packages/app/src/components/markdown-text-selection.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createContext, type ReactNode, useContext } from "react"; - -export type MarkdownTextSurface = "prose" | "table-cell"; - -const MarkdownTextSurfaceContext = createContext("prose"); - -export function MarkdownTableCellText({ children }: { children: ReactNode }) { - return {children}; -} - -export function useMarkdownTextSurface(): MarkdownTextSurface { - return useContext(MarkdownTextSurfaceContext); -} - -export function iosMarkdownTextIsSelectable(surface: MarkdownTextSurface): boolean { - return surface !== "table-cell"; -} diff --git a/packages/app/src/components/markdown-text-style.test.ts b/packages/app/src/components/markdown-text-style.test.ts deleted file mode 100644 index 8094cca386..0000000000 --- a/packages/app/src/components/markdown-text-style.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { StyleSheet, type TextStyle } from "react-native"; -import { resolvePlainMarkdownTextStyle } from "@/components/markdown-text-style"; - -function unistylesStyle(id: string, style: Record) { - return { - ...style, - [`unistyles_${id}`]: { id }, - }; -} - -function uiTextViewFlatten(rootStyle: TextStyle, style: TextStyle): Record { - // react-native-uitextview/src/util.ts:8 flattens [rootStyle, style] - // before passing the result to its native View-backed components. - return { ...(StyleSheet.flatten([rootStyle, style]) as Record) }; -} - -function unistylesMetadataKeys(style: Record) { - return Object.keys(style).filter((key) => key.startsWith("unistyles_")); -} - -describe("resolvePlainMarkdownTextStyle", () => { - it("keeps UITextView from collapsing parent and child Unistyles styles into one native View style object", () => { - const merged = uiTextViewFlatten( - resolvePlainMarkdownTextStyle(unistylesStyle("paragraph", { color: "#111" })), - resolvePlainMarkdownTextStyle(unistylesStyle("text", { fontWeight: "600" })), - ); - - expect(unistylesMetadataKeys(merged)).toHaveLength(0); - expect(merged).toMatchObject({ color: "#111", fontWeight: "600" }); - }); -}); diff --git a/packages/app/src/components/markdown-text-style.ts b/packages/app/src/components/markdown-text-style.ts deleted file mode 100644 index ab1ac4abf6..0000000000 --- a/packages/app/src/components/markdown-text-style.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { StyleSheet, type StyleProp, type TextStyle } from "react-native"; - -export function resolvePlainMarkdownTextStyle(style: StyleProp): TextStyle { - return stripUnistylesMetadata(StyleSheet.flatten(style) ?? {}); -} - -function stripUnistylesMetadata(style: TextStyle): TextStyle { - // iOS markdown text goes through react-native-uitextview. That library - // inherits text styles by flattening [parentStyle, childStyle] before handing - // the result to native View-backed components. If both entries are Unistyles - // styles, flattening preserves both `unistyles_*` metadata keys in one object, - // and Unistyles correctly warns that the style should have stayed array-shaped. - // - // `UITextView` is a third-party boundary, not a Unistyles-tracked component in - // our ownership model. Resolve the concrete style values before crossing that - // boundary and drop only Unistyles' private tracking metadata. This preserves - // iOS paragraph/spanning text selection while avoiding the metadata merge. - const plainStyle: Record = { ...style }; - for (const key of Object.keys(plainStyle)) { - if (key.startsWith("unistyles_")) { - delete plainStyle[key]; - } - } - return plainStyle as TextStyle; -} diff --git a/packages/app/src/components/markdown-text.android.tsx b/packages/app/src/components/markdown-text.android.tsx deleted file mode 100644 index f444d916bd..0000000000 --- a/packages/app/src/components/markdown-text.android.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { useMemo, type ReactNode } from "react"; -import { - Text, - View, - type StyleProp, - type TextProps, - type TextStyle, - type ViewStyle, -} from "react-native"; -import type { MarkdownCopyInlineTag } from "@/assistant-selection-copy/markup"; - -interface MarkdownTextSpanProps { - style?: StyleProp; - monoSurface?: boolean; - copyTag?: MarkdownCopyInlineTag; - children: ReactNode; - onPress?: TextProps["onPress"]; - accessibilityRole?: TextProps["accessibilityRole"]; -} - -// Android's enables per-text-node selection natively. Each -// sibling Text is its own selection scope — drag can't span across siblings -// (that requires a single UITextView ancestor and is iOS-only). onPress works -// natively here, so links routed through this span stay tappable on Android. -export function MarkdownTextSpan({ - style, - children, - onPress, - accessibilityRole, -}: MarkdownTextSpanProps) { - return ( - - {children} - - ); -} - -interface MarkdownParagraphViewProps { - paragraphStyle: ViewStyle; - containsImage?: boolean; - children: ReactNode; -} - -const MARKDOWN_PARAGRAPH_RESET: ViewStyle = {}; - -// Paragraph stays a , not a , for layout fidelity. RN Android's -// text engine *does* accept inline View children (TextInlineViewPlaceholderSpan -// in ReactBaseTextShadowNode), so this isn't a crash-avoidance choice — but -// inline-placeholder spans collapse block-level children (e.g. paragraph -// images) into one-character placeholders, which destroys image row layout. -// preserves the original block layout; the trade-off is no cross-span -// selection on Android (a UITextView-style trick has no Android equivalent). -export function MarkdownParagraphView({ paragraphStyle, children }: MarkdownParagraphViewProps) { - const style = useMemo(() => [paragraphStyle, MARKDOWN_PARAGRAPH_RESET], [paragraphStyle]); - return {children}; -} diff --git a/packages/app/src/components/markdown-text.d.ts b/packages/app/src/components/markdown-text.d.ts deleted file mode 100644 index 00194ac319..0000000000 --- a/packages/app/src/components/markdown-text.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./markdown-text.ios"; diff --git a/packages/app/src/components/markdown-text.ios.tsx b/packages/app/src/components/markdown-text.ios.tsx deleted file mode 100644 index 1b2eb405c4..0000000000 --- a/packages/app/src/components/markdown-text.ios.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { useMemo, type ReactNode } from "react"; -import { - Text, - View, - type StyleProp, - type TextProps, - type TextStyle, - type ViewStyle, -} from "react-native"; -import { UITextView } from "react-native-uitextview"; -import { resolvePlainMarkdownTextStyle } from "@/components/markdown-text-style"; -import { - iosMarkdownTextIsSelectable, - useMarkdownTextSurface, -} from "@/components/markdown-text-selection"; -import type { MarkdownCopyInlineTag } from "@/assistant-selection-copy/markup"; - -interface MarkdownTextSpanProps { - style?: StyleProp; - monoSurface?: boolean; - copyTag?: MarkdownCopyInlineTag; - children: ReactNode; - // Links route through this span too (see assistant-file-links/link.tsx). A - // plain nested in the paragraph UITextView is dropped, so the link - // must be a UITextView span to be visible. onPress is wired onto the leaf - // string children here: react-native-uitextview attaches it to the - // RNUITextViewChild nodes it builds from string content, which the native tap - // recognizer dispatches to. The link's handler reaches these leaf spans via - // AssistantLinkPressProvider (see assistant-file-links/link-press-context). - onPress?: TextProps["onPress"]; - accessibilityRole?: TextProps["accessibilityRole"]; -} - -// Inline span backed by UITextView so iOS gets native word-selection handles. -// Used inside MarkdownParagraphView (which is also a UITextView on iOS); the -// library's TextAncestorContext hoists these into UITextViewChild nodes so -// selection drags can cross sibling spans (e.g. plain text → **bold** → code). -export function MarkdownTextSpan({ - style, - children, - onPress, - accessibilityRole, -}: MarkdownTextSpanProps) { - const plainStyle = useMemo(() => resolvePlainMarkdownTextStyle(style), [style]); - const surface = useMarkdownTextSurface(); - - // Each selectable span creates a UIKit UITextView with a window-level tap recognizer. - // A large table would create one per cell and make every app touch fan out across them. - if (!iosMarkdownTextIsSelectable(surface)) { - return ( - - {children} - - ); - } - - return ( - - {children} - - ); -} - -interface MarkdownParagraphViewProps { - paragraphStyle: ViewStyle; - containsImage?: boolean; - children: ReactNode; -} - -const MARKDOWN_PARAGRAPH_RESET: ViewStyle = {}; - -// iOS-only: paragraph wraps in UITextView so the entire paragraph is one -// native text view. That's what unlocks cross-inline drag selection — handles -// can span every MarkdownTextSpan child inside this paragraph. -// ViewStyle is structurally compatible with the layout props paragraphs use -// (margin, padding, alignment); the cast lets the existing paragraphStyle -// flow through unchanged. -export function MarkdownParagraphView({ - paragraphStyle, - containsImage = false, - children, -}: MarkdownParagraphViewProps) { - const textStyle = useMemo( - () => - resolvePlainMarkdownTextStyle([ - paragraphStyle, - MARKDOWN_PARAGRAPH_RESET, - ] as StyleProp), - [paragraphStyle], - ); - const viewStyle = useMemo(() => [paragraphStyle, MARKDOWN_PARAGRAPH_RESET], [paragraphStyle]); - - if (containsImage) { - return {children}; - } - - return ( - - {children} - - ); -} diff --git a/packages/app/src/components/markdown-text.web.tsx b/packages/app/src/components/markdown-text.web.tsx deleted file mode 100644 index 0cb89f0cff..0000000000 --- a/packages/app/src/components/markdown-text.web.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { useMemo, type ReactNode } from "react"; -import { - Text, - View, - type StyleProp, - type TextProps, - type TextStyle, - type ViewStyle, -} from "react-native"; -import { CODE_SURFACE_DATASET } from "@/styles/code-surface"; -import { markdownCopyDataSet, type MarkdownCopyInlineTag } from "@/assistant-selection-copy/markup"; - -interface MarkdownTextSpanProps { - style?: StyleProp; - monoSurface?: boolean; - copyTag?: MarkdownCopyInlineTag; - children: ReactNode; - // Web links use the /Pressable path in link.tsx, not this span, so these - // are accepted for prop-shape parity with the native variants and forwarded - // harmlessly. - onPress?: TextProps["onPress"]; - accessibilityRole?: TextProps["accessibilityRole"]; -} - -// react-native-web renders Text as /
with `user-select: text` -// already applied via markdownStyleMapping. The web bundle must not import -// react-native-uitextview: its transitive import of codegenNativeComponent -// pulls in setUpReactDevTools, which doesn't resolve under Metro's web -// target in dev mode. -export function MarkdownTextSpan({ - style, - monoSurface, - copyTag, - children, - onPress, - accessibilityRole, -}: MarkdownTextSpanProps) { - const dataSet = useMemo(() => { - if (copyTag && (monoSurface || copyTag === "code")) { - return { ...CODE_SURFACE_DATASET, ...markdownCopyDataSet[copyTag] }; - } - if (copyTag) { - return markdownCopyDataSet[copyTag]; - } - return monoSurface ? CODE_SURFACE_DATASET : undefined; - }, [copyTag, monoSurface]); - - return ( - - {children} - - ); -} - -interface MarkdownParagraphViewProps { - paragraphStyle: ViewStyle; - containsImage?: boolean; - children: ReactNode; -} - -const MARKDOWN_PARAGRAPH_RESET: ViewStyle = {}; - -// Same shape as Android — paragraph is a View so block-level children (images) -// keep their natural layout. Web text selection already spans nested inline -// elements via CSS user-select, so no UITextView equivalent is needed. -export function MarkdownParagraphView({ paragraphStyle, children }: MarkdownParagraphViewProps) { - const style = useMemo(() => [paragraphStyle, MARKDOWN_PARAGRAPH_RESET], [paragraphStyle]); - return ( - - {children} - - ); -} diff --git a/packages/app/src/components/markdown/fence/index.tsx b/packages/app/src/components/markdown/fence/index.tsx deleted file mode 100644 index 846aa91355..0000000000 --- a/packages/app/src/components/markdown/fence/index.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import type { ComponentType } from "react"; -import { HighlightedCodeBlock } from "@/components/highlighted-code-block"; -import { getMarkdownFenceLanguage } from "./language"; -import { MermaidFence } from "./mermaid"; -import type { MarkdownFenceRendererProps } from "./types"; - -export interface MarkdownFenceBlockProps extends MarkdownFenceRendererProps { - info: string | null | undefined; -} - -const diagramFences: Partial>> = { - mermaid: MermaidFence, -}; - -export function MarkdownFenceBlock({ - code, - info, - phase, - inheritedStyles, - textStyle, -}: MarkdownFenceBlockProps) { - const language = getMarkdownFenceLanguage(info); - const DiagramFence = language ? diagramFences[language] : undefined; - if (DiagramFence) { - return ( - - ); - } - return ( - - ); -} diff --git a/packages/app/src/components/markdown/fence/language.test.ts b/packages/app/src/components/markdown/fence/language.test.ts deleted file mode 100644 index 172f72c310..0000000000 --- a/packages/app/src/components/markdown/fence/language.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { getMarkdownFenceLanguage } from "./language"; - -describe("getMarkdownFenceLanguage", () => { - it("normalizes the first info-string token", () => { - expect(getMarkdownFenceLanguage(" Sirena theme=dark ")).toBe("sirena"); - expect(getMarkdownFenceLanguage("TypeScript title=example")).toBe("typescript"); - }); - - it("returns null when no language is declared", () => { - expect(getMarkdownFenceLanguage("")).toBeNull(); - expect(getMarkdownFenceLanguage(null)).toBeNull(); - expect(getMarkdownFenceLanguage(undefined)).toBeNull(); - }); -}); diff --git a/packages/app/src/components/markdown/fence/language.ts b/packages/app/src/components/markdown/fence/language.ts deleted file mode 100644 index fd414a5ac2..0000000000 --- a/packages/app/src/components/markdown/fence/language.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function getMarkdownFenceLanguage(info: string | null | undefined): string | null { - return info?.trim().split(/\s+/)[0]?.toLowerCase() || null; -} diff --git a/packages/app/src/components/markdown/fence/mermaid/host.native.tsx b/packages/app/src/components/markdown/fence/mermaid/host.native.tsx index 044341f3aa..001ec5cf1c 100644 --- a/packages/app/src/components/markdown/fence/mermaid/host.native.tsx +++ b/packages/app/src/components/markdown/fence/mermaid/host.native.tsx @@ -14,7 +14,7 @@ import { useTranslation } from "react-i18next"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; import { HighlightedCodeBlock } from "@/components/highlighted-code-block"; import type { Theme } from "@/styles/theme"; -import type { MarkdownFenceRendererProps } from "../types"; +import type { MarkdownFenceRendererProps } from "./types"; import type { MermaidRenderRequest } from "./render-model"; import { mermaidRuntimeHtml } from "./runtime/html.gen"; import { diff --git a/packages/app/src/components/markdown/fence/mermaid/host.tsx b/packages/app/src/components/markdown/fence/mermaid/host.tsx index 50e9596740..ff2ca3268c 100644 --- a/packages/app/src/components/markdown/fence/mermaid/host.tsx +++ b/packages/app/src/components/markdown/fence/mermaid/host.tsx @@ -1,5 +1,5 @@ import { HighlightedCodeBlock } from "@/components/highlighted-code-block"; -import type { MarkdownFenceRendererProps } from "../types"; +import type { MarkdownFenceRendererProps } from "./types"; export function MermaidFenceHost({ code, inheritedStyles, textStyle }: MarkdownFenceRendererProps) { return ( diff --git a/packages/app/src/components/markdown/fence/mermaid/host.web.tsx b/packages/app/src/components/markdown/fence/mermaid/host.web.tsx index e79922ec43..fed9afe3a3 100644 --- a/packages/app/src/components/markdown/fence/mermaid/host.web.tsx +++ b/packages/app/src/components/markdown/fence/mermaid/host.web.tsx @@ -6,7 +6,7 @@ import { StyleSheet, withUnistyles } from "react-native-unistyles"; import { HighlightedCodeBlock } from "@/components/highlighted-code-block"; import { ZoomableViewport } from "@/components/zoomable-viewport"; import type { Theme } from "@/styles/theme"; -import type { MarkdownFenceRendererProps } from "../types"; +import type { MarkdownFenceRendererProps } from "./types"; import { MermaidFullscreenViewer } from "./fullscreen-viewer.web"; import { MermaidIframeRuntime, type MermaidRenderedMessage } from "./iframe-runtime.web"; import { useMermaidRenderModel } from "./use-render-model"; diff --git a/packages/app/src/components/markdown/fence/mermaid/index.tsx b/packages/app/src/components/markdown/fence/mermaid/index.tsx index 0f56d5f74f..fb135c8b6a 100644 --- a/packages/app/src/components/markdown/fence/mermaid/index.tsx +++ b/packages/app/src/components/markdown/fence/mermaid/index.tsx @@ -1,6 +1,19 @@ -import type { MarkdownFenceRendererProps } from "../types"; +import { createContext, useContext } from "react"; +import { withUnistyles } from "react-native-unistyles"; +import type { Theme } from "@/styles/theme"; import { MermaidFenceHost } from "./host"; - -export function MermaidFence(props: MarkdownFenceRendererProps) { - return ; +export { splitMermaidFences } from "./segments"; +export const MermaidStreamingContext = createContext(false); +const ThemedHost = withUnistyles(MermaidFenceHost); +const styles = (theme: Theme) => ({ + inheritedStyles: { + color: theme.colors.foreground, + fontFamily: theme.fontFamily.mono, + fontSize: theme.fontSize.code, + }, + textStyle: { backgroundColor: theme.colors.surface2, padding: 12, borderRadius: 8 }, +}); +export function MermaidFence({ code }: { code: string }) { + const streaming = useContext(MermaidStreamingContext); + return ; } diff --git a/packages/app/src/components/markdown/fence/mermaid/render-model.ts b/packages/app/src/components/markdown/fence/mermaid/render-model.ts index da5176dbea..16e46f2210 100644 --- a/packages/app/src/components/markdown/fence/mermaid/render-model.ts +++ b/packages/app/src/components/markdown/fence/mermaid/render-model.ts @@ -1,4 +1,4 @@ -import type { MarkdownPhase } from "../types"; +import type { MarkdownPhase } from "./types"; export type DiagramColorScheme = "light" | "dark"; diff --git a/packages/app/src/components/markdown/fence/mermaid/segments.ts b/packages/app/src/components/markdown/fence/mermaid/segments.ts new file mode 100644 index 0000000000..be2f229051 --- /dev/null +++ b/packages/app/src/components/markdown/fence/mermaid/segments.ts @@ -0,0 +1,57 @@ +import { fromMarkdown } from "mdast-util-from-markdown"; +import type { Definition, RootContent } from "mdast"; + +interface Segment { + kind: "markdown" | "mermaid"; + id: string; + source: string; +} + +/** Native enriched-markdown has no fenced-block override. Only diagrams leave its engine. */ +export function splitMermaidFences(markdown: string): Segment[] { + if (!/mermaid/i.test(markdown)) return [{ kind: "markdown", id: "0", source: markdown }]; + const segments: Segment[] = []; + const definitions = new Map(); + let cursor = 0; + function visit(node: RootContent): void { + if (node.type === "definition" && !definitions.has(node.identifier)) + definitions.set(node.identifier, node); + if (node.type === "code" && node.lang?.toLowerCase() === "mermaid" && node.position) { + const start = node.position.start.offset!; + const end = node.position.end.offset!; + if (start > cursor) + segments.push({ + kind: "markdown", + id: String(segments.length), + source: markdown.slice(cursor, start), + }); + segments.push({ kind: "mermaid", id: String(segments.length), source: node.value }); + cursor = end; + } else if ("children" in node) node.children.forEach(visit); + } + const tree = fromMarkdown(markdown); + tree.children.forEach(visit); + if (cursor < markdown.length || segments.length === 0) + segments.push({ + kind: "markdown", + id: String(segments.length), + source: markdown.slice(cursor), + }); + // Prepend the document's first definitions after splitting, so fragment-local + // duplicates cannot override them. Original source slices and fence offsets stay intact. + if (segments.some((segment) => segment.kind === "mermaid")) { + const referenceSource = [...definitions.values()] + .map((node) => { + const label = node.identifier.replace(/[\\[\]]/g, "\\$&"); + const url = node.url.replace(/[\\<>]/g, "\\$&"); + const title = node.title ? ` "${node.title.replace(/[\\"]/g, "\\$&")}"` : ""; + return `[${label}]: <${url}>${title}`; + }) + .join("\n"); + if (referenceSource) + for (const segment of segments) { + if (segment.kind === "markdown") segment.source = `${referenceSource}\n\n${segment.source}`; + } + } + return segments; +} diff --git a/packages/app/src/components/markdown/fence/types.ts b/packages/app/src/components/markdown/fence/mermaid/types.ts similarity index 99% rename from packages/app/src/components/markdown/fence/types.ts rename to packages/app/src/components/markdown/fence/mermaid/types.ts index b499bc893b..dbfec694cd 100644 --- a/packages/app/src/components/markdown/fence/types.ts +++ b/packages/app/src/components/markdown/fence/mermaid/types.ts @@ -1,7 +1,5 @@ import type { TextStyle } from "react-native"; - export type MarkdownPhase = "streaming" | "complete"; - export interface MarkdownFenceRendererProps { code: string; phase: MarkdownPhase; diff --git a/packages/app/src/components/markdown/fence/mermaid/use-render-model.ts b/packages/app/src/components/markdown/fence/mermaid/use-render-model.ts index cb35549b10..67b51f8ad6 100644 --- a/packages/app/src/components/markdown/fence/mermaid/use-render-model.ts +++ b/packages/app/src/components/markdown/fence/mermaid/use-render-model.ts @@ -8,7 +8,7 @@ import { type RenderedDiagram, } from "./render-model"; import { containsUnsafeMermaidSource } from "./source-policy"; -import type { MarkdownPhase } from "../types"; +import type { MarkdownPhase } from "./types"; const renderCache = new Map(); const RENDER_CACHE_LIMIT = 50; diff --git a/packages/app/src/components/markdown/frame.tsx b/packages/app/src/components/markdown/frame.tsx new file mode 100644 index 0000000000..86e8a4df13 --- /dev/null +++ b/packages/app/src/components/markdown/frame.tsx @@ -0,0 +1,18 @@ +import React, { type ReactElement } from "react"; +import { View } from "react-native"; +import { markdownCopyImageSourcesDataSet } from "@/assistant-selection-copy/markup"; +import { isWeb } from "@/constants/platform"; + +export function MarkdownRendererFrame({ + imageSources, + children, +}: { + imageSources: Record; + children: ReactElement; +}) { + return ( + + {children} + + ); +} diff --git a/packages/app/src/components/markdown/html-ish.test.ts b/packages/app/src/components/markdown/html-ish.test.ts deleted file mode 100644 index f8606d2714..0000000000 --- a/packages/app/src/components/markdown/html-ish.test.ts +++ /dev/null @@ -1,529 +0,0 @@ -import { readFileSync } from "node:fs"; -import { describe, expect, it } from "vitest"; -import { normalizeHtmlishMarkdown, splitHtmlishMarkdown } from "./html-ish"; - -describe("splitHtmlishMarkdown", () => { - it("renders heading text beside a linked picture without literal heading tags", () => { - const source = - '

RetriggerConfidence Score: 5/5

'; - - expect(splitHtmlishMarkdown(source)).toEqual([ - { - kind: "inlineImage", - alt: "Retrigger", - src: "https://example.com/l.svg", - href: "https://example.com/r", - flowsWithText: true, - }, - { kind: "markdown", text: "Confidence Score: 5/5" }, - ]); - }); - - it.each(["h1", "h2", "h3", "h4", "h5", "h6"])( - "keeps a mixed %s heading together after preceding markdown", - (heading) => { - const source = `Intro\n\n<${heading}>IconCaption\n\nOutro`; - - expect(splitHtmlishMarkdown(source)).toEqual([ - { kind: "markdown", text: "Intro\n\n" }, - { - kind: "inlineImage", - alt: "Icon", - src: "https://example.com/icon.svg", - flowsWithText: true, - }, - { kind: "markdown", text: "Caption\n\nOutro" }, - ]); - }, - ); - - it("preserves formatted text before and after images in a details heading", () => { - const source = - '
More

BeforeIconAfter

'; - - expect(splitHtmlishMarkdown(source)).toEqual([ - { - kind: "details", - summary: "More", - body: "**Before***After*", - bodyParts: [ - { kind: "markdown", text: "**Before**" }, - { kind: "inlineImage", alt: "Icon", src: "https://example.com/icon.svg" }, - { kind: "markdown", text: "*After*" }, - ], - }, - ]); - }); - - it("keeps mixed HTML headings literal in code examples", () => { - const heading = '

Caption

'; - const source = `Use \`${heading}\`\n\n\`\`\`html\n${heading}\n\`\`\``; - - expect(splitHtmlishMarkdown(source)).toEqual([{ kind: "markdown", text: source }]); - }); - - it("preserves unknown wrappers around text-only headings", () => { - expect(splitHtmlishMarkdown("

Heading

")).toEqual([ - { kind: "markdown", text: "
Heading
" }, - ]); - }); - - const inlineImageBody = [ - 'Priority Spoofed browser User-Agent allows access control bypass', - "", - "The middleware now trusts any browser-like User-Agent for unauthenticated requests.", - "", - "```ts", - 'const isBrowser = userAgent.includes("Mozilla");', - "```", - ].join("\n"); - - const multiDetailsBody = [ - "### Bot Review", - "", - "
", - "

Important Files Changed

", - "", - "- `packages/server/src/server/session.ts`", - "", - "
", - "", - "
", - "

Security Findings

", - "", - "No blocking findings.", - "", - "
", - "", - "", - 'Reviews (8): Last reviewed commit: “revert: undo parser” | Re-trigger Greptile', - ].join("\n"); - - it("classifies linked HTML images as generic inline images, not block markdown images", () => { - const [image, text] = splitHtmlishMarkdown(inlineImageBody); - - expect(image).toEqual({ - kind: "inlineImage", - alt: "Priority", - src: "https://example.com/assets/priority.svg?v=9", - flowsWithText: true, - }); - expect(text).toEqual({ - kind: "markdown", - text: [ - " Spoofed browser User-Agent allows access control bypass", - "", - "The middleware now trusts any browser-like User-Agent for unauthenticated requests.", - "", - "```ts", - 'const isBrowser = userAgent.includes("Mozilla");', - "```", - ].join("\n"), - }); - }); - - it("flags both images as flowsWithText when two inline images precede title text on the same line", () => { - const twoImageBody = [ - 'Priority Security **Title text here**', - "", - "Body paragraph.", - ].join("\n"); - - const parts = splitHtmlishMarkdown(twoImageBody); - const [first, second, third] = parts; - - expect(first).toEqual({ - kind: "inlineImage", - alt: "Priority", - src: "https://example.com/priority.svg", - flowsWithText: true, - }); - expect(second).toEqual({ kind: "markdown", text: " " }); - expect(third).toEqual({ - kind: "inlineImage", - alt: "Security", - src: "https://example.com/security.svg", - flowsWithText: true, - }); - }); - - it("keeps safe width and height attributes on inline images", () => { - expect( - splitHtmlishMarkdown( - 'Small', - ), - ).toEqual([ - { - kind: "inlineImage", - alt: "Small", - src: "https://example.com/small.svg", - width: 18, - height: 12, - }, - ]); - }); - - it("keeps safe non-empty image links", () => { - expect( - splitHtmlishMarkdown( - 'Small', - ), - ).toEqual([ - { - kind: "inlineImage", - alt: "Small", - src: "https://example.com/small.svg", - href: "https://example.com/details", - }, - ]); - }); - - it("renders a linked picture inside a heading without exposing its HTML", () => { - const source = [ - '

', - '', - '', - 'Retrigger', - "

", - "", - "Confidence Score: 5/5", - "", - "
SummaryNo outstanding findings.
", - ].join("\n"); - - expect(splitHtmlishMarkdown(source)).toEqual([ - { - kind: "inlineImage", - alt: "Retrigger", - src: "https://example.com/default.svg", - href: "https://example.com/retrigger?id=73851569", - width: 85, - height: 24, - }, - { kind: "markdown", text: "\n\nConfidence Score: 5/5\n\n" }, - { kind: "details", summary: "Summary", body: "No outstanding findings." }, - ]); - }); - - it.each([ - ["", ""], - ["

", "

"], - ['', ""], - ])("uses the picture fallback without activating unsafe links (%s)", (wrapper, closing) => { - const picture = - 'Image'; - - expect(splitHtmlishMarkdown(`${wrapper}${picture}${closing}`)).toEqual([ - { kind: "inlineImage", src: "https://example.com/default.svg", alt: "Image" }, - ]); - }); - - it("keeps picture examples literal inside inline and fenced code", () => { - const picture = - ''; - const source = `Use \`${picture}\`\n\n\`\`\`html\n${picture}\n\`\`\``; - expect(splitHtmlishMarkdown(source)).toEqual([{ kind: "markdown", text: source }]); - }); - - it.each(["p", "div"])("renders a linked picture inside %s", (wrapper) => { - const source = `<${wrapper}>Image`; - expect(splitHtmlishMarkdown(source)).toEqual([ - { - kind: "inlineImage", - src: "https://example.com/default.svg", - alt: "Image", - href: "https://example.com/page", - }, - ]); - }); - - it.each(["h2", "p", "div"])("keeps text after an image-only %s on a separate row", (wrapper) => { - const source = `<${wrapper}>ImageNext section`; - expect(splitHtmlishMarkdown(source)).toEqual([ - { kind: "inlineImage", src: "https://example.com/image.svg", alt: "Image" }, - { kind: "markdown", text: "Next section" }, - ]); - }); - - it("handles deeply nested image wrappers without exhausting the stack", () => { - const source = `${"
".repeat(10000)}Image${"
".repeat(10000)}`; - expect(splitHtmlishMarkdown(source)).toEqual([ - { - kind: "inlineImage", - src: "https://example.com/image.svg", - alt: "Image", - href: "https://example.com/page", - }, - ]); - }); - - it("preserves inline image parts inside details bodies", () => { - expect( - splitHtmlishMarkdown( - '
ImagesIcon Inline text
', - ), - ).toEqual([ - { - kind: "details", - summary: "Images", - body: "Inline text", - bodyParts: [ - { - kind: "inlineImage", - alt: "Icon", - src: "https://example.com/icon.svg", - href: "https://example.com/page", - flowsWithText: true, - }, - { kind: "markdown", text: " Inline text" }, - ], - }, - ]); - }); - - it("does not flag standalone inline images as flowing with text", () => { - expect( - splitHtmlishMarkdown('Shot\n\nCaption below'), - ).toEqual([ - { kind: "inlineImage", alt: "Shot", src: "https://example.com/shot.png" }, - { kind: "markdown", text: "\n\nCaption below" }, - ]); - }); - - it("does not flag mid-line inline images as flowing with text", () => { - const [, image] = splitHtmlishMarkdown( - 'Before Icon after', - ); - - expect(image).toEqual({ - kind: "inlineImage", - alt: "Icon", - src: "https://example.com/icon.png", - }); - }); - - it("leaves ordinary markdown images on the markdown path", () => { - const source = "![Ordinary](https://example.com/full-size.png)"; - - expect(splitHtmlishMarkdown(source)).toEqual([{ kind: "markdown", text: source }]); - }); - - it("leaves unsafe HTML image sources inert", () => { - const source = 'Bad'; - - expect(splitHtmlishMarkdown(source)).toEqual([{ kind: "markdown", text: source }]); - }); - - it("removes raw image anchor and image tags from rendered markdown text", () => { - const text = splitHtmlishMarkdown(inlineImageBody) - .map((part) => (part.kind === "markdown" ? part.text : "")) - .join(""); - - expect(text).not.toContain(""); - }); - - it("unwraps sub text and strips HTML comments", () => { - const parts = splitHtmlishMarkdown(multiDetailsBody); - const tail = parts.at(-1); - - expect(tail).toEqual({ - kind: "markdown", - text: "\n\nReviews (8): Last reviewed commit: “revert: undo parser” | [Re-trigger Greptile](https://app.greptile.com)", - }); - }); - - it("does not leak stray closing details tags across multiple details blocks", () => { - const renderedText = splitHtmlishMarkdown(multiDetailsBody) - .map((part) => { - if (part.kind === "markdown") return part.text; - if (part.kind === "details") return `${part.summary}\n${part.body}`; - return part.alt; - }) - .join("\n"); - - expect(renderedText).not.toContain(""); - expect(renderedText).not.toContain(" 3<4")).toBe( - "1 { - const source = "
OpenStill open"; - - expect(splitHtmlishMarkdown(source)).toEqual([{ kind: "markdown", text: source }]); - }); - - it("falls back to inert markdown when summary is missing", () => { - const source = "
Hidden
"; - - expect(splitHtmlishMarkdown(source)).toEqual([{ kind: "markdown", text: source }]); - }); - - it("does not execute or render unknown HTML as structured content", () => { - const source = - '
Safe
'; - - expect(splitHtmlishMarkdown(source)).toEqual([ - { kind: "markdown", text: "" }, - { kind: "details", summary: "Safe", body: '' }, - ]); - }); -}); - -function withoutArrayToSorted(callback: () => T): T { - const descriptor = Object.getOwnPropertyDescriptor(Array.prototype, "toSorted"); - Reflect.deleteProperty(Array.prototype, "toSorted"); - try { - return callback(); - } finally { - if (descriptor) { - Reflect.defineProperty(Array.prototype, "toSorted", descriptor); - } - } -} diff --git a/packages/app/src/components/markdown/html-ish.ts b/packages/app/src/components/markdown/html-ish.ts deleted file mode 100644 index 94eb7cab5b..0000000000 --- a/packages/app/src/components/markdown/html-ish.ts +++ /dev/null @@ -1,926 +0,0 @@ -import { Parser } from "htmlparser2"; - -export interface MarkdownTextPart { - kind: "markdown"; - text: string; -} - -export interface MarkdownDetailsPart { - kind: "details"; - summary: string; - body: string; - bodyParts?: MarkdownDisplayPart[]; -} - -export interface MarkdownInlineImagePart { - kind: "inlineImage"; - alt: string; - src: string; - href?: string; - width?: number; - height?: number; - /** The image starts a line and text continues on that same line. */ - flowsWithText?: boolean; -} - -export type MarkdownDisplayPart = MarkdownTextPart | MarkdownDetailsPart | MarkdownInlineImagePart; - -const FENCE_LINE_RE = /^ {0,3}([`~]{3,})[^\n\r]*(?:\r?\n|$)/gm; -const BACKTICK_RUN_RE = /`+/g; -const SAFE_IMAGE_SRC_RE = /^(https?:\/\/|data:image\/(?:png|gif|jpe?g);base64,)/i; -const SAFE_LINK_HREF_RE = /^(https?:\/\/|#(?:$|[\w-]))/i; -// CommonMark open and closing tags (spec "Raw HTML"); comments are matched separately. -const HTML_ATTRIBUTE = String.raw`\s+[A-Za-z_:][\w.:-]*(?:\s*=\s*(?:[^\s"'=<>\x60]+|'[^']*'|"[^"]*"))?`; -const HTML_TAG_RE = new RegExp( - String.raw`<(?:[A-Za-z][A-Za-z0-9-]*(?:${HTML_ATTRIBUTE})*\s*\/?|\/[A-Za-z][A-Za-z0-9-]*\s*)>`, - "y", -); -const VOID_HTML_TAGS = new Set(["br", "img", "source"]); -const MARKDOWN_TAG_WRAPPERS: Readonly> = { - b: ["**", "**"], - del: ["~~", "~~"], - em: ["*", "*"], - i: ["*", "*"], - s: ["~~", "~~"], - strike: ["~~", "~~"], - strong: ["**", "**"], -}; - -interface ProtectedMarkdownRange { - start: number; - end: number; -} - -interface MarkdownImageDimensions { - width?: number; - height?: number; -} - -interface HtmlTextToken { - kind: "text"; - value: string; -} - -interface HtmlCommentToken { - kind: "comment"; -} - -interface HtmlTagToken { - kind: "tag"; - name: string; - closing: boolean; - selfClosing: boolean; - attributes: Record; - raw: string; -} - -type HtmlToken = HtmlTextToken | HtmlCommentToken | HtmlTagToken; - -interface InlineImageParseResult { - part: MarkdownInlineImagePart; - end: number; -} - -interface MarkdownDelimiterMatch { - index: number; - end: number; -} - -interface HtmlishTokenParser { - write(chunk: string): void; - skip(length: number): void; - end(): void; -} - -export function splitHtmlishMarkdown(source: string): MarkdownDisplayPart[] { - return splitHtmlishTokens(tokenizeHtmlishMarkdown(source)); -} - -function splitHtmlishTokens(tokens: HtmlToken[]): MarkdownDisplayPart[] { - const parts: MarkdownDisplayPart[] = []; - let cursor = 0; - - while (cursor < tokens.length) { - const token = tokens[cursor]; - if (isOpenTag(token, "details")) { - const closeIndex = findMatchingClose(tokens, cursor, "details"); - if (closeIndex !== null) { - const details = parseDetailsTokens(tokens.slice(cursor + 1, closeIndex)); - if (details) { - parts.push(details); - cursor = closeIndex + 1; - continue; - } - } - } - - const inlineImage = parseInlineImageAt(tokens, cursor); - if (inlineImage) { - parts.push( - flowsWithFollowingText(tokens, cursor, inlineImage.end) - ? { ...inlineImage.part, flowsWithText: true } - : inlineImage.part, - ); - cursor = inlineImage.end; - continue; - } - - if (isHeadingTag(token)) { - const closeIndex = findMixedHeadingClose(tokens, cursor); - if (closeIndex !== null) { - // Headings render as their contents. Split those contents together so an - // embedded image cannot strand the opening heading tag in a text slice. - for (const part of splitHtmlishTokens(tokens.slice(cursor + 1, closeIndex))) { - appendDisplayPart(parts, part); - } - cursor = closeIndex + 1; - continue; - } - } - - const end = findNextDisplayPartIndex(tokens, cursor + 1) ?? tokens.length; - appendMarkdownPart(parts, renderInlineTokens(tokens.slice(cursor, end))); - cursor = end; - } - - return parts; -} - -function parseInlineImageAt(tokens: HtmlToken[], start: number): InlineImageParseResult | null { - const token = tokens[start]; - if (token?.kind !== "tag" || token.closing) { - return null; - } - - if (token.name === "img") { - const image = imageTokenToInlineImage(token, undefined); - return image ? { part: image, end: start + 1 } : null; - } - - if (token.name !== "a" && token.name !== "picture" && !isImageBlockWrapper(token)) { - return null; - } - - return parseWrappedImageAt(tokens, start); -} - -function parseWrappedImageAt(tokens: HtmlToken[], start: number): InlineImageParseResult | null { - // Walk an image-only wrapper chain once. Recursing over copied child slices - // makes deeply nested review HTML quadratic and can exhaust the JS stack. - const wrappers: string[] = []; - let image: MarkdownInlineImagePart | null = null; - let href: string | undefined; - let hasLink = false; - - for (let index = start; index < tokens.length; index += 1) { - const child = tokens[index]; - if (child.kind === "comment" || isWhitespaceText(child)) { - continue; - } - if (child.kind !== "tag") { - return null; - } - if (child.closing) { - if (!image || wrappers.pop() !== child.name) { - return null; - } - if (wrappers.length === 0) { - return { part: image, end: index + 1 }; - } - continue; - } - // Pictures use their img fallback, not responsive source selection. - if (child.name === "source" && wrappers.at(-1) === "picture") { - continue; - } - if (image) { - return null; - } - if (child.name === "img") { - image = imageTokenToInlineImage(child, href); - if (!image) { - return null; - } - continue; - } - if (child.name !== "a" && child.name !== "picture" && !isImageBlockWrapper(child)) { - return null; - } - if (child.name === "a" && !hasLink) { - href = safeHref(child.attributes.href); - hasLink = true; - } - wrappers.push(child.name); - } - return null; -} - -function isImageBlockWrapper(token: HtmlToken | undefined): boolean { - return isHeadingTag(token) || isOpenTag(token, "p") || isOpenTag(token, "div"); -} - -function flowsWithFollowingText(tokens: HtmlToken[], start: number, end: number): boolean { - if (isImageBlockWrapper(tokens[start])) { - return false; - } - - const previous = tokens[start - 1]; - // At line start if nothing precedes this image, or the preceding token ends with only - // whitespace since the last newline (covers a bare space between two images on the same line). - const atLineStart = - previous === undefined || - (previous.kind === "text" && /(?:^|[\n\r])[ \t]*$/.test(previous.value)); - if (!atLineStart) { - return false; - } - - // Scan forward past same-line whitespace-only text tokens and inline images to find - // the first substantive text token, without crossing a newline. - let cursor = end; - while (cursor < tokens.length) { - const token = tokens[cursor]; - if (token === undefined) { - break; - } - if (token.kind === "text") { - const sameLine = token.value.split(/\r?\n/, 1)[0] ?? ""; - if (sameLine.trim().length > 0) { - return true; - } - // Whitespace-only on this line — keep scanning only if no newline was crossed. - if (token.value.includes("\n") || token.value.includes("\r")) { - return false; - } - cursor += 1; - continue; - } - // An inline image tag — skip over it (the image itself and its possible wrapping close tag). - if (token.kind === "tag") { - if (isImageBlockWrapper(token)) { - return false; - } - const imageResult = parseInlineImageAt(tokens, cursor); - if (imageResult) { - cursor = imageResult.end; - continue; - } - } - break; - } - return false; -} - -function findNextDisplayPartIndex(tokens: HtmlToken[], start: number): number | null { - for (let index = start; index < tokens.length; index += 1) { - if ( - isOpenTag(tokens[index], "details") || - findMixedHeadingClose(tokens, index) !== null || - parseInlineImageAt(tokens, index) - ) { - return index; - } - } - return null; -} - -function findMixedHeadingClose(tokens: HtmlToken[], start: number): number | null { - const token = tokens[start]; - if (!isHeadingTag(token)) { - return null; - } - const closeIndex = findMatchingClose(tokens, start, token.name); - if (closeIndex === null) { - return null; - } - for (let index = start + 1; index < closeIndex; index += 1) { - if (parseInlineImageAt(tokens, index)) { - return closeIndex; - } - } - return null; -} - -function appendDisplayPart(parts: MarkdownDisplayPart[], part: MarkdownDisplayPart): void { - if (part.kind === "markdown") { - appendMarkdownPart(parts, part.text); - } else { - parts.push(part); - } -} - -function appendMarkdownPart(parts: MarkdownDisplayPart[], text: string): void { - if (!text) { - return; - } - const previous = parts.at(-1); - if (previous?.kind === "markdown") { - previous.text += text; - return; - } - parts.push({ kind: "markdown", text }); -} - -function parseDetailsTokens(tokens: HtmlToken[]): MarkdownDetailsPart | null { - const summaryOpenIndex = findNextOpenTag(tokens, 0, "summary"); - if (summaryOpenIndex === null) { - return null; - } - - const summaryCloseIndex = findMatchingClose(tokens, summaryOpenIndex, "summary"); - if (summaryCloseIndex === null) { - return null; - } - - const summaryTokens = tokens.slice(summaryOpenIndex + 1, summaryCloseIndex); - const bodyTokens = [...tokens.slice(0, summaryOpenIndex), ...tokens.slice(summaryCloseIndex + 1)]; - const summary = renderSummaryTokens(summaryTokens).trim(); - if (!summary) { - return null; - } - - const bodyParts = splitHtmlishTokens(bodyTokens); - const body = renderBodyText(bodyParts); - - return { - kind: "details", - summary, - body: body.trim(), - ...(bodyParts.some((part) => part.kind !== "markdown") - ? { bodyParts: trimBodyParts(bodyParts) } - : {}), - }; -} - -function renderBodyText(parts: MarkdownDisplayPart[]): string { - return parts.map((part) => (part.kind === "markdown" ? part.text : "")).join(""); -} - -function trimBodyParts(parts: MarkdownDisplayPart[]): MarkdownDisplayPart[] { - const trimmed = [...parts]; - const first = trimmed[0]; - if (first?.kind === "markdown") { - first.text = first.text.trimStart(); - } - const last = trimmed.at(-1); - if (last?.kind === "markdown") { - last.text = last.text.trimEnd(); - } - return trimmed.filter((part) => part.kind !== "markdown" || part.text.length > 0); -} - -function renderSummaryTokens(tokens: HtmlToken[]): string { - return stripSingleHeadingWrapper(renderInlineTokens(tokens)); -} - -function stripSingleHeadingWrapper(text: string): string { - const tokens = tokenizeHtmlishMarkdown(text.trim()); - if (tokens.length < 3) { - return text; - } - - const first = tokens[0]; - const last = tokens.at(-1); - if (!isHeadingTag(first) || !isClosingTag(last, first.name)) { - return text; - } - - return renderInlineTokens(tokens.slice(1, -1)); -} - -function renderInlineTokens(tokens: HtmlToken[]): string { - let output = ""; - for (let index = 0; index < tokens.length; index += 1) { - const token = tokens[index]; - if (!token) { - continue; - } - if (token.kind === "text") { - output += token.value; - continue; - } - if (token.kind === "comment" || token.closing) { - continue; - } - - if (token.name === "br") { - output += "\n"; - continue; - } - - if (token.name === "img") { - output += renderImageToken(token); - continue; - } - - const closeIndex = token.selfClosing ? null : findMatchingClose(tokens, index, token.name); - if (closeIndex === null) { - output += renderUnknownTag(token); - continue; - } - - const children = tokens.slice(index + 1, closeIndex); - const close = tokens[closeIndex]; - const closeRaw = isClosingTag(close, token.name) ? close.raw : ``; - output += renderHtmlTag(token, children, closeRaw); - index = closeIndex; - } - - return output; -} - -function renderHtmlTag(token: HtmlTagToken, children: HtmlToken[], closeRaw: string): string { - if (token.name === "a") { - return renderLinkToken(token, children); - } - if (token.name === "sub" || isHeadingTagName(token.name)) { - return renderInlineTokens(children); - } - if (token.name === "code" && children.every((child) => child.kind === "text")) { - return `\`${renderInlineTokens(children)}\``; - } - const wrapper = MARKDOWN_TAG_WRAPPERS[token.name]; - if (wrapper) { - return `${wrapper[0]}${renderInlineTokens(children)}${wrapper[1]}`; - } - if (token.name === "table") { - return renderTableTokens(children); - } - if (token.name === "p" || token.name === "div") { - return `\n\n${renderInlineTokens(children).trim()}\n\n`; - } - return `${token.raw}${renderInlineTokens(children)}${closeRaw}`; -} - -function renderTableTokens(tokens: HtmlToken[]): string { - const rows: string[] = []; - - for (let index = 0; index < tokens.length; index += 1) { - if (!isOpenTag(tokens[index], "tr")) { - continue; - } - const closeIndex = findMatchingClose(tokens, index, "tr"); - if (closeIndex === null) { - continue; - } - const cells = renderTableCells(tokens.slice(index + 1, closeIndex)); - if (cells.length === 1) { - rows.push(`- ${cells[0]}`); - } else if (cells.length > 1) { - const label = - cells[0].startsWith("**") && cells[0].endsWith("**") ? cells[0] : `**${cells[0]}**`; - rows.push(`- ${label}: ${cells.slice(1).join(" ")}`); - } - index = closeIndex; - } - - return rows.length > 0 ? `\n${rows.join("\n")}\n` : ""; -} - -function renderTableCells(tokens: HtmlToken[]): string[] { - const cells: string[] = []; - - for (let index = 0; index < tokens.length; index += 1) { - const token = tokens[index]; - if (!isOpenTag(token, "td") && !isOpenTag(token, "th")) { - continue; - } - const closeIndex = findMatchingClose(tokens, index, token.name); - if (closeIndex === null) { - continue; - } - const cell = renderInlineTokens(tokens.slice(index + 1, closeIndex)).trim(); - if (cell) { - cells.push(cell); - } - index = closeIndex; - } - - return cells; -} - -function renderImageToken(token: HtmlTagToken): string { - const image = imageTokenToInlineImage(token, undefined); - if (!image) { - return token.raw; - } - - return `![${escapeMarkdownImageAlt(image.alt)}](${image.src})`; -} - -function renderLinkToken(token: HtmlTagToken, children: HtmlToken[]): string { - const imageOnly = getSingleImageChild(children); - if (imageOnly) { - return renderImageToken(imageOnly); - } - - const label = renderInlineTokens(children); - const href = token.attributes.href ?? ""; - if (!label || !SAFE_LINK_HREF_RE.test(href) || href === "#") { - return label; - } - - return `[${label}](${href})`; -} - -function imageTokenToInlineImage( - token: HtmlTagToken, - href: string | undefined, -): MarkdownInlineImagePart | null { - const src = token.attributes.src ?? ""; - if (!SAFE_IMAGE_SRC_RE.test(src)) { - return null; - } - - return { - kind: "inlineImage", - alt: token.attributes.alt ?? "", - src, - ...(href ? { href } : {}), - ...parseImageDimensions(token.attributes), - }; -} - -function parseImageDimensions(attributes: Record): MarkdownImageDimensions { - return { - ...parseImageDimension("width", attributes.width), - ...parseImageDimension("height", attributes.height), - }; -} - -function parseImageDimension(key: "width" | "height", value: string | undefined) { - if (!value || !/^\d+(?:\.\d+)?$/.test(value)) { - return {}; - } - - const parsed = Number(value); - return Number.isFinite(parsed) && parsed > 0 ? { [key]: parsed } : {}; -} - -function safeHref(href: string | undefined): string | undefined { - if (!href || href === "#" || !SAFE_LINK_HREF_RE.test(href)) { - return undefined; - } - return href; -} - -function getSingleImageChild(tokens: HtmlToken[]): HtmlTagToken | null { - const visible = tokens.filter((token) => token.kind !== "comment" && !isWhitespaceText(token)); - return visible.length === 1 && isOpenTag(visible[0], "img") ? visible[0] : null; -} - -function renderUnknownTag(token: HtmlTagToken): string { - if (token.name === "script" || token.name === "style") { - return ""; - } - return token.raw; -} - -function escapeMarkdownImageAlt(value: string): string { - return value.replace(/]/g, "\\]"); -} - -function findNextOpenTag(tokens: HtmlToken[], start: number, name: string): number | null { - for (let index = start; index < tokens.length; index += 1) { - if (isOpenTag(tokens[index], name)) { - return index; - } - } - return null; -} - -function findMatchingClose(tokens: HtmlToken[], openIndex: number, name: string): number | null { - let depth = 1; - for (let index = openIndex + 1; index < tokens.length; index += 1) { - const token = tokens[index]; - if (isOpenTag(token, name) && !token.selfClosing) { - depth += 1; - continue; - } - if (isClosingTag(token, name)) { - depth -= 1; - if (depth === 0) { - return index; - } - } - } - return null; -} - -function isOpenTag(token: HtmlToken | undefined, name: string): token is HtmlTagToken { - return token?.kind === "tag" && token.name === name && !token.closing; -} - -function isClosingTag(token: HtmlToken | undefined, name: string): token is HtmlTagToken { - return token?.kind === "tag" && token.name === name && token.closing; -} - -function isHeadingTag(token: HtmlToken | undefined): token is HtmlTagToken { - return token?.kind === "tag" && isHeadingTagName(token.name) && !token.closing; -} - -function isHeadingTagName(name: string): boolean { - return ( - name === "h1" || - name === "h2" || - name === "h3" || - name === "h4" || - name === "h5" || - name === "h6" - ); -} - -function isWhitespaceText(token: HtmlToken): boolean { - return token.kind === "text" && token.value.trim() === ""; -} - -function tokenizeHtmlishMarkdown(source: string): HtmlToken[] { - const protectedRanges = getProtectedMarkdownRanges(source); - const tokens: HtmlToken[] = []; - const parser = createHtmlishTokenParser(source, tokens); - let cursor = 0; - - for (const range of protectedRanges) { - parser.write(source.slice(cursor, range.start)); - tokens.push({ kind: "text", value: source.slice(range.start, range.end) }); - parser.skip(range.end - range.start); - cursor = range.end; - } - - parser.write(source.slice(cursor)); - parser.end(); - return tokens; -} - -function createHtmlishTokenParser(source: string, tokens: HtmlToken[]): HtmlishTokenParser { - let stripNextLeadingLineBreak = false; - let skippedLength = 0; - let writtenLength = 0; - let chunkStart = 0; - let parser!: Parser; - // Unknown tags render as text, so keep them as written: the parser lowercases names - // and rewrites attributes. A tag split by a protected range has no single slice. - const readRawTag = (): string | null => - parser.startIndex < chunkStart - ? null - : source.slice(parser.startIndex + skippedLength, parser.endIndex + 1 + skippedLength); - parser = new Parser( - { - onopentag(name, attributes, isImplied) { - if (isImplied) { - return; - } - const selfClosing = VOID_HTML_TAGS.has(name); - tokens.push({ - kind: "tag", - name, - closing: false, - selfClosing, - attributes, - raw: readRawTag() ?? renderStartTag(name, attributes), - }); - }, - onclosetag(name, isImplied) { - if (isImplied || VOID_HTML_TAGS.has(name)) { - return; - } - tokens.push({ - kind: "tag", - name, - closing: true, - selfClosing: false, - attributes: {}, - raw: readRawTag() ?? ``, - }); - }, - ontext(value) { - if (stripNextLeadingLineBreak) { - stripNextLeadingLineBreak = false; - value = stripLeadingLineBreak(value); - } - if (!value) { - return; - } - tokens.push({ kind: "text", value }); - }, - oncomment() { - stripNextLeadingLineBreak = isLineStart(source, parser.startIndex + skippedLength); - tokens.push({ kind: "comment" }); - }, - }, - { - decodeEntities: false, - lowerCaseAttributeNames: true, - lowerCaseTags: true, - recognizeSelfClosing: true, - }, - ); - return { - write(chunk) { - chunkStart = writtenLength; - writtenLength += chunk.length; - parser.write(chunk); - }, - skip(length) { - skippedLength += length; - }, - end() { - parser.end(); - }, - }; -} - -function isLineStart(source: string, index: number): boolean { - return index === 0 || source[index - 1] === "\n" || source[index - 1] === "\r"; -} - -function stripLeadingLineBreak(value: string): string { - if (value.startsWith("\r\n")) { - return value.slice(2); - } - if (value.startsWith("\n") || value.startsWith("\r")) { - return value.slice(1); - } - return value; -} - -function renderStartTag(name: string, attributes: Record): string { - const renderedAttributes = Object.entries(attributes) - .map(([key, value]) => (value === "" ? ` ${key}` : ` ${key}="${escapeAttribute(value)}"`)) - .join(""); - return `<${name}${renderedAttributes}>`; -} - -function escapeAttribute(value: string): string { - return value.replaceAll("&", "&").replaceAll('"', """); -} - -function getProtectedMarkdownRanges(source: string): ProtectedMarkdownRange[] { - const fencedRanges = getFencedCodeRanges(source); - const codeRanges = mergeProtectedRanges([ - ...fencedRanges, - ...getInlineCodeRanges(source, fencedRanges), - ]); - return mergeProtectedRanges([...codeRanges, ...getLiteralAngleBracketRanges(source, codeRanges)]); -} - -/** - * htmlparser2 reads any `<` followed by a letter as a tag and keeps collecting attributes up - * to the next `>`, so `1", index + 2); - commentClose = close === -1 ? source.length : close; - } - htmlEnd = commentClose === source.length ? null : commentClose + 3; - } else { - HTML_TAG_RE.lastIndex = index; - htmlEnd = HTML_TAG_RE.test(source) ? HTML_TAG_RE.lastIndex : null; - } - - if (htmlEnd === null) { - ranges.push({ start: index, end: index + 1 }); - htmlEnd = index + 1; - } - index = source.indexOf("<", htmlEnd); - } - - return ranges; -} - -function getFencedCodeRanges(source: string): ProtectedMarkdownRange[] { - const ranges: ProtectedMarkdownRange[] = []; - FENCE_LINE_RE.lastIndex = 0; - - while (true) { - const open = FENCE_LINE_RE.exec(source); - if (!open) { - return ranges; - } - - const marker = open[1]; - if (!marker) { - continue; - } - - const close = findClosingFence(source, FENCE_LINE_RE.lastIndex, marker); - if (!close) { - ranges.push({ start: open.index, end: source.length }); - return ranges; - } - - ranges.push({ start: open.index, end: close.end }); - FENCE_LINE_RE.lastIndex = close.end; - } -} - -function findClosingFence( - source: string, - start: number, - marker: string, -): MarkdownDelimiterMatch | null { - const closeRe = new RegExp( - `^ {0,3}[${marker[0]}]{${marker.length},}[^\\n\\r]*(?:\\r?\\n|$)`, - "gm", - ); - closeRe.lastIndex = start; - const close = closeRe.exec(source); - return close ? { index: close.index, end: closeRe.lastIndex } : null; -} - -function getInlineCodeRanges( - source: string, - fencedRanges: ProtectedMarkdownRange[], -): ProtectedMarkdownRange[] { - const ranges: ProtectedMarkdownRange[] = []; - BACKTICK_RUN_RE.lastIndex = 0; - - while (true) { - const open = BACKTICK_RUN_RE.exec(source); - if (!open) { - return ranges; - } - if (isProtectedIndex(open.index, fencedRanges)) { - continue; - } - - const marker = open[0]; - const afterOpen = BACKTICK_RUN_RE.lastIndex; - const close = findClosingBacktickRun(source, afterOpen, marker, fencedRanges); - if (!close) { - // Unmatched backtick run — skip past it so the loop doesn't restart from 0. - // findClosingBacktickRun exhausts the global regex, which resets lastIndex - // to 0 when exec() returns null (ECMAScript §22.2.7.2). - BACKTICK_RUN_RE.lastIndex = afterOpen; - continue; - } - - ranges.push({ start: open.index, end: close.end }); - BACKTICK_RUN_RE.lastIndex = close.end; - } -} - -function findClosingBacktickRun( - source: string, - start: number, - marker: string, - fencedRanges: ProtectedMarkdownRange[], -): MarkdownDelimiterMatch | null { - BACKTICK_RUN_RE.lastIndex = start; - - while (true) { - const close = BACKTICK_RUN_RE.exec(source); - if (!close) { - return null; - } - if (close[0] === marker && !isProtectedIndex(close.index, fencedRanges)) { - return { index: close.index, end: BACKTICK_RUN_RE.lastIndex }; - } - } -} - -function mergeProtectedRanges(ranges: ProtectedMarkdownRange[]): ProtectedMarkdownRange[] { - const sorted = [...ranges].sort((a, b) => a.start - b.start); - const merged: ProtectedMarkdownRange[] = []; - - for (const range of sorted) { - const previous = merged.at(-1); - if (!previous || range.start > previous.end) { - merged.push({ ...range }); - continue; - } - previous.end = Math.max(previous.end, range.end); - } - - return merged; -} - -function isProtectedIndex(index: number, ranges: ProtectedMarkdownRange[]): boolean { - return ranges.some((range) => index >= range.start && index < range.end); -} - -export function normalizeHtmlishMarkdown(source: string): string { - return renderInlineTokens(tokenizeHtmlishMarkdown(source)); -} diff --git a/packages/app/src/components/markdown/html.ts b/packages/app/src/components/markdown/html.ts new file mode 100644 index 0000000000..a370372c26 --- /dev/null +++ b/packages/app/src/components/markdown/html.ts @@ -0,0 +1,179 @@ +import { Parser } from "htmlparser2"; +import { fromMarkdown } from "mdast-util-from-markdown"; +import { decodeString } from "micromark-util-decode-string"; +import type { Nodes } from "mdast"; + +const ATTRIBUTE = String.raw`\s+[A-Za-z_:][\w.:-]*(?:\s*=\s*(?:[^\s"'=<>\x60]+|'[^']*'|"[^"]*"))?`; +const TAG = String.raw`<(?:[A-Za-z][A-Za-z0-9-]*(?:${ATTRIBUTE})*\s*\/?|\/[A-Za-z][A-Za-z0-9-]*\s*)>`; +const HTML = new RegExp( + String.raw`(?|]*>[\s\S]*?<\/code\s*>|]*>[\s\S]*?<\/summary\s*>|${TAG})`, + "gi", +); +const BLOCK_TAGS = new Set([ + "div", + "p", + "table", + "tr", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "section", + "article", + "ul", + "ol", + "li", + "blockquote", + "pre", +]); +const EMPHASIS: Readonly> = { b: "**", strong: "**", i: "*", em: "*" }; + +/** Convert comment HTML to Markdown while preserving the original spelling of code. */ +export function normalizeMarkdownHtml(source: string): string { + if (!source.includes("<")) return source; + const htmlRanges: { start: number; end: number }[] = []; + function findHtml(node: Nodes) { + if (node.type === "html" && node.position) { + htmlRanges.push({ start: node.position.start.offset!, end: node.position.end.offset! }); + } else if ("children" in node) node.children.forEach(findHtml); + } + findHtml(fromMarkdown(source)); + const codeRanges: { start: number; end: number }[] = []; + // Disable HTML parsing so code inside
remains visible to the Markdown parser. + const tree = fromMarkdown(source, { + extensions: [{ disable: { null: ["htmlFlow", "htmlText"] } }], + }); + function protectCode(node: Nodes) { + if (node.type === "code" || node.type === "inlineCode") { + const start = node.position?.start.offset; + const end = node.position?.end.offset; + if (start !== undefined && end !== undefined) codeRanges.push({ start, end }); + } else if ("children" in node) node.children.forEach(protectCode); + } + protectCode(tree); + const links: (string | undefined)[] = []; + let previousTag: { name: string; end: number } | null = null; + const convert = (raw: string, offset: number): string => { + if (codeRanges.some(({ start, end }) => offset >= start && offset < end)) return raw; + if (raw.startsWith("B\n```\n\n> Last."; + expect(splitMermaidFences(source)).toEqual([ + { kind: "markdown", id: "0", source: "First.\n\n```ts\nconst n = 1;\n```\n\n" }, + { kind: "mermaid", id: "1", source: "graph TD\nA-->B" }, + { kind: "markdown", id: "2", source: "\n\n> Last." }, + ]); + expect(splitMermaidFences("```md\n```mermaid\n```")).toEqual([ + { kind: "markdown", id: "0", source: "```md\n```mermaid\n```" }, + ]); +}); + +it("keeps reference links available across native Mermaid boundaries", async () => { + const { splitMermaidFences } = await import("./fence/mermaid/segments"); + const parts = splitMermaidFences( + "[Before][file]\n\n```mermaid\ngraph TD\nA-->B\n```\n\n[After][file]\n\n[file]: src/file.ts", + ); + for (const part of parts.filter((candidate) => candidate.kind === "markdown")) + expect(part.source).toContain("[file]: "); +}); + +it("recognizes mixed-case Mermaid fence info and incomplete streaming fences", async () => { + const { splitMermaidFences } = await import("./fence/mermaid/segments"); + expect(splitMermaidFences("```Mermaid title\ngraph TD\nA-->B")).toEqual([ + { kind: "mermaid", id: "0", source: "graph TD\nA-->B" }, + ]); +}); + +it("carries reference definitions nested in quotes across a diagram", async () => { + const { splitMermaidFences } = await import("./fence/mermaid/segments"); + const parts = splitMermaidFences( + "[open][file]\n\n```mermaid\ngraph TD\nA-->B\n```\n\n> [file]: src/file.ts", + ); + expect(parts[0].source).toContain("[file]: "); +}); + +it("keeps diagram identity when an earlier workspace image resolves", async () => { + const { splitMermaidFences } = await import("./fence/mermaid/segments"); + const source = "![image](a.png)\n\n```mermaid\ngraph TD\nA-->B\n```"; + const before = splitMermaidFences(source).find((part) => part.kind === "mermaid"); + const after = splitMermaidFences( + source.replace("a.png", "file:///long/cache/path/image.png"), + ).find((part) => part.kind === "mermaid"); + expect(before).toHaveProperty("id"); + expect(after?.id).toBe(before?.id); +}); + +it("keeps workspace-relative hover titles on explicit absolute file links", () => { + expect(prepareMarkdownSource("[Open](/repo/src/file.ts:42-45)", "/repo").render()).toBe( + '[Open](/repo/src/file.ts:42-45 "src/file.ts:42-45")', + ); +}); + +it.each(['""', "''", "()"])("preserves an authored empty link title: %s", (title) => { + const source = `[Open](/repo/file.ts ${title})`; + expect(prepareMarkdownSource(source, "/repo").render()).toBe(source); +}); + +describe("shared Markdown theme colours", () => { + it.each([ + ["dark", darkTheme], + ["light", lightTheme], + ] as const)("uses %s theme colours for tables and task labels", (_name, theme) => { + const { markdownStyle: style } = createMarkdownStyles(theme); + expect(style.table).toMatchObject({ + color: theme.colors.foreground, + headerTextColor: theme.colors.foreground, + headerBackgroundColor: theme.colors.surface2, + rowEvenBackgroundColor: theme.colors.surface0, + rowOddBackgroundColor: theme.colors.surface1, + borderColor: theme.colors.border, + }); + expect(style.list).toMatchObject({ + color: theme.colors.foreground, + bulletColor: theme.colors.foregroundMuted, + markerColor: theme.colors.foregroundMuted, + }); + expect(style.h6?.color).toBe(theme.colors.foregroundMuted); + expect(style.taskList).toMatchObject({ + checkedTextColor: theme.colors.foregroundMuted, + checkedColor: theme.colors.accent, + checkmarkColor: theme.colors.accentForeground, + borderColor: theme.colors.foregroundMuted, + }); + }); +}); + +it.each([ + ["dark", darkTheme], + ["light", lightTheme], +] as const)( + "uses %s theme colours for quotes, admonitions, code, links and dividers", + (_name, theme) => { + const { markdownStyle: style } = createMarkdownStyles(theme); + expect(style.blockquote).toMatchObject({ + color: `${theme.colors.foreground}cc`, + backgroundColor: theme.colors.surface1, + borderColor: theme.colors.surface2, + admonitions: { + note: { color: theme.colors.accentBright, backgroundColor: theme.colors.surface2 }, + tip: { color: theme.colors.statusSuccess, backgroundColor: theme.colors.surface2 }, + important: { color: theme.colors.statusMerged, backgroundColor: theme.colors.surface2 }, + warning: { color: theme.colors.statusWarning, backgroundColor: theme.colors.surface2 }, + caution: { color: theme.colors.statusDanger, backgroundColor: theme.colors.surface2 }, }, - children, + }); + expect(style.code).toMatchObject({ + color: theme.colors.foreground, + backgroundColor: theme.colors.surface2, + borderColor: "transparent", + }); + expect(style.link?.color).toBe(theme.colors.accentBright); + expect(style.linkVariants?.["^(?!https?://).+"]).toEqual({ + color: theme.colors.accentBright, + }); + expect(style.thematicBreak?.color).toBe(theme.colors.border); + }, +); + +it.each([darkTheme, lightTheme])("supplies the theme selection colour to the renderer", (theme) => { + expect(createMarkdownStyles(theme)).toHaveProperty( + "selectionColor", + theme.colors.terminal.selectionBackground, + ); +}); + +describe("GitHub comment HTML normalization", () => { + it.each([ + [ + 'P1', + "![P1]()", + ], + ['▶', "[▶]()"], + ["Before
After
End", "Before \nAfter \nEnd"], + ["bold and strong", "**bold** and **strong**"], + ["italic and emphasis", "*italic* and *emphasis*"], + ["Use const n = 1.", "Use `const n = 1`."], + ["H2O and x2", "H2O and x2"], + [ + "
Review detailsBody **stays**.
", + "**Review details**\n\nBody **stays**.", + ], + ["BeforeAfter", "BeforeAfter"], + [ + "

Important Files Changed

Body
", + "**Important Files Changed**\n\nBody", + ], + [ + "

First

Second

Body
", + "**First Second**\n\nBody", + ], + ["file.ts", "file.ts"], + ['GitHub', "[Git]()Hub"], + ["GitHub", "GitHub"], + ["Git Hub", "Git Hub"], + ["
Keep the content
", "Keep the content"], + [ + 'Review', + "[**Review**]()", + ], + ['`` and bold', '`` and **bold**'], + [ + '```html\n\n```\n\nbold', + '```html\n\n```\n\n**bold**', + ], + ['~~~html\nlink\n~~~', '~~~html\nlink\n~~~'], + [ + '
Code\n\n```html\n\n```\n
', + '**Code**\n\n\n\n```html\n\n```\n', + ], + [ + " and ", + " and ", + ], + ])("normalizes %s without changing code", (source, expected) => { + expect(prepareMarkdownSource(source).render()).toBe(expected); + }); +}); + +it("preserves backslashes and decodes entities inside HTML code", () => { + const source = String.raw`\*literal\* <img>`; + expect(prepareMarkdownSource(source).render()).toBe("`" + String.raw`\*literal\* ` + "`"); +}); + +it.each([false, true])( + "caps inline images to the paragraph line height (compact: %s)", + (compact) => { + const { markdownStyle } = createMarkdownStyles(darkTheme, compact); + expect(markdownStyle.inlineImage?.size).toBe(markdownStyle.paragraph?.lineHeight); + }, +); + +it("shows native SVG badges as alt text without reserving an image box", () => { + const source = "![P1](https://example.com/p1.svg?v=9) **Finding**"; + const prepared = prepareMarkdownSource(source, undefined, { supportsSvg: false }); + expect(prepared.render()).toBe("P1 **Finding**"); + expect(prepared.images).toEqual([]); +}); + +it("collapses images that fail to load or decode to their alt text", () => { + const prepared = prepareMarkdownSource( + "![Remote](https://example.com/broken.png) and ![Local](broken.png)", + ); + expect( + prepared.render( + new Map([["broken.png", "file:///broken.png"]]), + new Set(["https://example.com/broken.png", "broken.png"]), ), - Text: ({ children, style }: { children?: ReactNode; style?: StyleProp }) => - createElement("span", { style: flattenStyle(style) }, children), -})); + ).toBe("Remote and Local"); +}); -function flattenStyle(style: StyleProp): TextStyle { - return Object.assign({}, ...(Array.isArray(style) ? style.filter(Boolean) : [style])); -} +it("preserves angle-bracket image destinations and authored link titles during HTML normalization", () => { + const prepared = prepareMarkdownSource( + '![shot]() and [open]( "literal title")', + ); + expect(prepared.images.map((image) => image.source)).toEqual(["my file.png"]); + expect(prepared.render(new Map([["my file.png", "blob:shot"]]))).toBe( + '![shot]() and [open]( "literal title")', + ); +}); -describe("resolveInlineImageSize", () => { - it("respects a one-sided explicit width using natural aspect ratio", () => { - expect( - resolveInlineImageSize({ explicit: { width: 18 }, natural: { width: 90, height: 45 } }), - ).toEqual({ - width: 18, - height: 9, - }); +it("uses the first repeated image reference definition", () => { + const source = "![chart][image]\n\n[image]: first.png\n[image]: second.png"; + const prepared = prepareMarkdownSource(source); + expect(prepared.images.map((image) => image.source)).toEqual(["first.png"]); + expect(prepared.render(new Map([["first.png", "blob:first"]]))).toBe( + "![chart]()\n\n[image]: first.png\n[image]: second.png", + ); +}); + +it("preserves authored adjacency in the Greptile header", () => { + const source = + '

RetriggerView in GreptileConfidence Score: 2/5

'; + const rendered = prepareMarkdownSource(source, undefined, { supportsSvg: false }).render(); + expect(rendered).toContain( + "[Retrigger]()[View in Greptile]", + ); + expect(rendered).toContain(")Confidence Score: 2/5"); +}); + +it("keeps paragraph, heading and table-row boundaries when stripping HTML", () => { + const source = + "
First

Second
line

Heading

AB
CD
"; + const rendered = prepareMarkdownSource(source).render(); + expect(rendered).toBe("First\n\nSecond \nline\n\nHeading\n\nA B\n\nC D"); +}); + +it("normalizes images following HTML code in the same HTML block", () => { + const prepared = prepareMarkdownSource('\nx\n\n'); + expect(prepared.images.map((image) => image.source)).toEqual(["shot.png"]); + expect(prepared.render(new Map([["shot.png", "blob:shot"]]))).toContain("![]()"); + expect(prepared.render()).toContain("`\nx\n`"); +}); + +it("preserves literal Markdown punctuation in unsupported and failed image labels", () => { + const label = String.raw`\*literal\* \[label\]`; + expect( + prepareMarkdownSource(`![${label}](badge.svg)`, undefined, { supportsSvg: false }).render(), + ).toBe(label); + expect( + prepareMarkdownSource(`![${label}](https://example.com/badge.png)`).render( + new Map(), + new Set(["https://example.com/badge.png"]), + ), + ).toBe(label); +}); + +it("retries transient image decoding failures instead of caching an unavailable image", async () => { + const client = new QueryClient(); + let attempts = 0; + const decoded = await client.fetchQuery({ + queryKey: ["markdownImageDecode", "transient.png"], + retry: 1, + retryDelay: 0, + queryFn: () => + decodeMarkdownImage("transient.png", (_uri, success, failure) => { + attempts += 1; + if (attempts === 1) failure(new Error("temporarily unavailable")); + else success(20, 20); + }), }); + expect(decoded).toBe(true); + expect(attempts).toBe(2); + client.clear(); +}); - it("respects a one-sided explicit height using natural aspect ratio", () => { - expect( - resolveInlineImageSize({ explicit: { height: 18 }, natural: { width: 90, height: 45 } }), - ).toEqual({ - width: 36, - height: 18, +it("keeps the document's first reference definition across every native Mermaid segment", async () => { + const { splitMermaidFences } = await import("./fence/mermaid/segments"); + const before = '[first]: /repo/first.ts "Original"\n\n'; + const after = '\n\n[open][first]\n\n[first]: /repo/second.ts "Later"'; + const source = `${before}\`\`\`mermaid\ngraph TD\nA-->B\n\`\`\`${after}`; + const firstDefinition = (markdown: string) => + fromMarkdown(markdown).children.find((node) => node.type === "definition"); + const canonical = firstDefinition(source); + const segments = splitMermaidFences(source); + for (const segment of segments.filter((candidate) => candidate.kind === "markdown")) { + expect(firstDefinition(segment.source)).toMatchObject({ + identifier: canonical?.identifier, + url: canonical?.url, + title: canonical?.title, }); + } + expect(segments[0].source.endsWith(before)).toBe(true); + expect(segments[2].source.endsWith(after)).toBe(true); + expect(segments[1]).toEqual({ kind: "mermaid", id: "1", source: "graph TD\nA-->B" }); +}); + +it("reserves pending image geometry until decode succeeds and collapses only confirmed failures", () => { + const source = "![Local](shot.png)\n\n![Remote](https://example.com/image.png)"; + const prepared = prepareMarkdownSource(source); + const pending = prepared.render( + new Map(), + new Set(), + new Set(prepared.images.map((image) => image.source)), + ); + expect(pending.match(/!\[/g)).toHaveLength(2); + expect(pending).toContain("data:image/png;base64,"); + expect(prepared.render(new Map(), new Set(prepared.images.map((image) => image.source)))).toBe( + "Local\n\nRemote", + ); +}); + +it("normalizes a large HTML table into one block per row", () => { + const cell = "cell".repeat(8); + const rows = 8000; + const source = `${``.repeat(rows)}
${cell}${cell}
`; + const rendered = normalizeMarkdownHtml(source); + expect(rendered.split("\n\n")).toHaveLength(rows); + expect(rendered.startsWith(`${cell} ${cell}`)).toBe(true); + expect(rendered.endsWith(`${cell} ${cell}`)).toBe(true); +}); + +it.each([ + "3/4", + "1.5", + "v1.2", + "10/10", + "2024/10/16", + "10.0.0.1", + "word.word", + "example.com", + "file.tsxextra", + "v1.2.3-beta", + "123/456.789", + "×/÷", + "2024/10/16", +])("leaves non-path tokens unlinked in prose and inline code: %s", (token) => { + const { linkRecognition } = prepareMarkdownSource(`Value ${token}, then continue.`); + expect([...`Value ${token}, then continue.`.matchAll(linkRecognition.text)]).toEqual([]); + expect(linkRecognition.inlineCode.test(token)).toBe(false); +}); + +it.each([ + "src/index.ts", + "README.md", + "./scripts/run", + "./x/y", + "../x/y", + "/x/y", + "a/b/c", + String.raw`C:\Users\x`, + "src/index.ts:12:3", + "src/run:12", + "src/run:12:3", + "src/run:12-20", + "~/dev/paseo", + String.raw`C:\Users\x\file.txt`, + "file.ts:12", + "file.ts:12-20", + "123.ts", + "FILE.TS", + "https://example.com/path", +])("recognizes complete paths and URLs in prose and inline code: %s", (token) => { + const { linkRecognition } = prepareMarkdownSource(`Open ${token} then continue.`); + expect( + [...`Open ${token} then continue.`.matchAll(linkRecognition.text)].map(([text]) => text), + ).toEqual([token]); + expect(linkRecognition.inlineCode.test(token)).toBe(true); +}); + +it.each([ + "before/after", + "and/or", + "input/output", + "read/write", + "packages/app", + "123/file", + "src/123", +])("requires stronger path intent in prose while preserving inline code: %s", (token) => { + const { linkRecognition } = prepareMarkdownSource(`Compare ${token}, then continue.`); + expect([...`Compare ${token}, then continue.`.matchAll(linkRecognition.text)]).toEqual([]); + expect(linkRecognition.inlineCode.test(token)).toBe(true); +}); + +it("keeps prose and inline-code recognition enabled through the native regex adapter", () => { + const { linkRecognition } = prepareMarkdownSource("3/4 src/index.ts"); + const native = toNativeLinkRecognition(linkRecognition); + expect(native.text).toMatchObject({ + pattern: linkRecognition.text.source, + isDisabled: false, + isDefault: false, }); - - it("reserves a capped 3:2 placeholder when no dimensions are known", () => { - expect(resolveInlineImageSize({ explicit: {}, natural: null })).toEqual({ - width: 240, - height: 160, - }); + expect(native.inlineCode).toMatchObject({ + pattern: linkRecognition.inlineCode.source, + isDisabled: false, + isDefault: false, }); }); -describe("shared Markdown links", () => { - it("renders accent text and underlines it while hovered", () => { - const onPress = vi.fn(); - const children = colorMarkdownLinkChildren( - createElement(Text, { style: { color: "white" } }, "Paseo"), - "rgb(0, 122, 255)", - ); - const view = render( - createElement(MarkdownLinkText, { style: { color: "rgb(0, 122, 255)" }, onPress }, children), - ); - const link = view.getByRole("link"); - const linkText = link.firstElementChild as HTMLElement; +it.each([undefined, "file:///cache/shot.png"])( + "reserves a native workspace image while acquiring or decoding %s", + (url) => { + const status = getMarkdownImageStatus({ + canAcquire: true, + url, + acquisitionFailed: false, + decodeFailed: false, + decoded: false, + }); + expect(status).toBe("pending"); + const prepared = prepareMarkdownSource("Before ![image](shot.png) after", undefined, { + supportsSvg: false, + }); + const rendered = prepared.render(new Map(), new Set(), new Set(["shot.png"])); + expect(rendered).toBe(`Before ![image](<${PENDING_MARKDOWN_IMAGE}>) after`); + expect(fromMarkdown(rendered).children).toMatchObject([ + { + type: "paragraph", + children: [ + { type: "text", value: "Before " }, + { type: "image", url: PENDING_MARKDOWN_IMAGE }, + { type: "text", value: " after" }, + ], + }, + ]); + }, +); - expect((view.getByText("Paseo") as HTMLElement).style.color).toBe("rgb(0, 122, 255)"); - expect(linkText.style.textDecorationLine).toBe(""); - - fireEvent.mouseEnter(link); - expect(linkText.style.textDecorationLine).toBe("underline"); - - fireEvent.mouseLeave(link); - expect(linkText.style.textDecorationLine).toBe(""); - - fireEvent.click(link); - expect(onPress).toHaveBeenCalledOnce(); +it.each([ + { acquisitionFailed: true, decodeFailed: false }, + { acquisitionFailed: false, decodeFailed: true }, +])("collapses confirmed native image failures to alt text: %s", (failure) => { + expect( + getMarkdownImageStatus({ + canAcquire: true, + url: "file:///cache/shot.png", + decoded: false, + ...failure, + }), + ).toBe("unavailable"); + const prepared = prepareMarkdownSource("Before ![image](shot.png) after", undefined, { + supportsSvg: false, }); + expect(prepared.render(new Map(), new Set(["shot.png"]), new Set(["shot.png"]))).toBe( + "Before image after", + ); +}); + +it("replaces the native placeholder only after the resolved URL decodes", () => { + const url = "file:///cache/shot.png"; + expect( + getMarkdownImageStatus({ + canAcquire: true, + url, + acquisitionFailed: false, + decodeFailed: false, + decoded: true, + }), + ).toBe("ready"); + const prepared = prepareMarkdownSource("![image](shot.png)", undefined, { supportsSvg: false }); + expect(prepared.render(new Map([["shot.png", url]]))).toBe(`![image](<${url}>)`); }); diff --git a/packages/app/src/components/markdown/renderer.tsx b/packages/app/src/components/markdown/renderer.tsx index 83380ea919..c4a832342f 100644 --- a/packages/app/src/components/markdown/renderer.tsx +++ b/packages/app/src/components/markdown/renderer.tsx @@ -1,790 +1,120 @@ -import React, { - useCallback, - useEffect, - useMemo, - useState, - type ComponentType, - type ReactNode, -} from "react"; +import { memo, useMemo, useState } from "react"; +import { EnrichedMarkdownText, type LinkPressEvent } from "react-native-enriched-markdown"; +import { withUnistyles } from "react-native-unistyles"; import { - Image, - Pressable, - Text, - View, - type TextProps, - type TextStyle, - type ViewStyle, -} from "react-native"; -import { ChevronDown, ChevronRight } from "lucide-react-native"; -import Markdown, { - MarkdownIt, - type ASTNode, - type RenderRules, -} from "react-native-markdown-display"; -import { StyleSheet, withUnistyles } from "react-native-unistyles"; -import { HighlightedCodeBlock } from "@/components/highlighted-code-block"; -import { MarkdownFenceBlock } from "@/components/markdown/fence"; -import { MarkdownParagraphView, MarkdownTextSpan } from "@/components/markdown-text"; -import { MarkdownTableCellText } from "@/components/markdown-text-selection"; -import { getMarkdownListMarker, getMarkdownListSpacing } from "@/utils/markdown-list"; -import { markdownNodeContainsType } from "@/utils/markdown-ast"; -import { createMarkdownParser } from "@/utils/markdown-parser"; -import { createCompactMarkdownStyles, createMarkdownStyles } from "@/styles/markdown-styles"; + useAssistantFileLinkActions, + useAssistantFileLinkEnvironment, +} from "@/assistant-file-links"; +import { useStableEvent } from "@/hooks/use-stable-event"; +import { AttachmentLightbox, type ImageLightboxSource } from "@/components/attachment-lightbox"; import type { Theme } from "@/styles/theme"; -import { openExternalUrl } from "@/utils/open-external-url"; -import { isNative } from "@/constants/platform"; -import { - splitHtmlishMarkdown, - type MarkdownDisplayPart, - type MarkdownInlineImagePart, -} from "./html-ish"; -import { resolveInlineImageSize, type InlineImageDimensions } from "./inline-image-size"; -import { - getAssistantImageMetadata, - setAssistantImageMetadata, -} from "@/utils/assistant-image-metadata"; -import { groupMarkdownParts, type MarkdownPartGroup } from "./part-groups"; -import { colorMarkdownLinkChildren } from "./link-children"; -import { MarkdownLinkText } from "./link-text"; +import { isWeb } from "@/constants/platform"; +import { MarkdownRendererFrame } from "./frame"; +import { WEB_RENDERERS } from "./renderers"; +import { createMarkdownStyles } from "./styles"; +import { PENDING_MARKDOWN_IMAGE } from "./source"; +import { splitStreamingMarkdown } from "./streaming"; +import { useMarkdownSource, type MarkdownImageContext } from "./images"; -export type MarkdownStyles = Record; +import { MermaidFence, MermaidStreamingContext, splitMermaidFences } from "./fence/mermaid"; -interface MarkdownWithStableRendererProps { - children: ReactNode; - style: ReturnType | ReturnType; - rules?: RenderRules; - markdownit?: ReturnType; - onLinkPress?: (url: string) => boolean; - allowedImageHandlers?: readonly string[]; - topLevelMaxExceededItem?: ReactNode; -} +const ThemedMarkdown = withUnistyles(EnrichedMarkdownText); +const MD4C_FLAGS = { latexMath: false, hardSoftBreaks: true, admonitions: true }; -const MarkdownWithStableRenderer = Markdown as ComponentType; -const ThemedMarkdown = withUnistyles(MarkdownWithStableRenderer); +const regularStyle = (theme: Theme) => createMarkdownStyles(theme); +const compactStyle = (theme: Theme) => createMarkdownStyles(theme, true); +const WEB_CONTAINER = { userSelect: "text" as const }; -function markdownStyleMapping(theme: Theme): Partial { - return { style: createMarkdownStyles(theme) }; -} - -function compactMarkdownStyleMapping(theme: Theme): Partial { - return { style: createCompactMarkdownStyles(theme) }; -} - -// Serves PR comment bodies and the markdown file preview; agent chat passes its -// own parser. The preview has to show the bytes on disk, so no typographer. -const defaultMarkdownParser = createMarkdownParser({ linkify: true }); -const EMPTY_TEXT_STYLE: TextStyle = {}; -const MARKDOWN_LIST_ITEM_CONTENT_FLEX: ViewStyle = { flex: 1, flexShrink: 1, minWidth: 0 }; -export interface MarkdownRendererProps { +export interface MarkdownRendererProps extends MarkdownImageContext { text: string; compact?: boolean; - rules?: RenderRules; - markdownit?: ReturnType; - onLinkPress?: (url: string) => boolean; - allowedImageHandlers?: readonly string[]; - topLevelMaxExceededItem?: ReactNode; - enableHtmlish?: boolean; + streaming?: boolean; + onLinkPress?: (url: string) => unknown; } -export function MarkdownRenderer({ - text, - compact = false, - rules, - markdownit = defaultMarkdownParser, - onLinkPress, - allowedImageHandlers, - topLevelMaxExceededItem, - enableHtmlish = true, -}: MarkdownRendererProps) { - const markdownRules = useMemo(() => rules ?? createSharedMarkdownRules(), [rules]); - const parts = useMemo( - () => (enableHtmlish ? splitHtmlishMarkdown(text) : [{ kind: "markdown" as const, text }]), - [enableHtmlish, text], +/** All Markdown surfaces share the same engine and source adaptation. */ +export const MarkdownRenderer = memo(function MarkdownRenderer(props: MarkdownRendererProps) { + const streaming = !isWeb && !!props.streaming; + // Only completed lines affect block closure. Do not run mdast on every reveal frame. + const completeLines = streaming ? props.text.slice(0, props.text.lastIndexOf("\n") + 1) : ""; + const boundary = useMemo( + () => splitStreamingMarkdown(completeLines, true).head.length, + [completeLines], ); - const rendererProps = useMemo( - () => ({ - compact, - rules: markdownRules, - markdownit, - onLinkPress, - allowedImageHandlers, - topLevelMaxExceededItem, - }), - [ - allowedImageHandlers, - compact, - markdownRules, - markdownit, - onLinkPress, - topLevelMaxExceededItem, - ], - ); - - return ; -} - -type MarkdownPartRendererProps = Omit & { - rules: RenderRules; -}; - -function MarkdownPartList({ - parts, - rendererProps, -}: { - parts: MarkdownDisplayPart[]; - rendererProps: MarkdownPartRendererProps; -}) { - const keyedGroups = useMemo(() => keyMarkdownGroups(groupMarkdownParts(parts)), [parts]); + if (!streaming || boundary === 0) return ; return ( <> - {keyedGroups.map(({ key, group }) => - group.kind === "part" ? ( - + + + + ); +}); + +const MarkdownPart = memo(function MarkdownPart({ + text, + continues = false, + compact = false, + streaming = false, + onLinkPress, + ...imageContext +}: MarkdownRendererProps & { continues?: boolean }) { + const environment = useAssistantFileLinkEnvironment(); + const actions = useAssistantFileLinkActions(); + const client = + imageContext.client ?? + (environment.client?.readFile + ? { readFile: environment.client.readFile.bind(environment.client) } + : undefined); + const { markdown, imageSources, linkRecognition, linkSource } = useMarkdownSource(text, { + workspaceRoot: environment.workspaceRoot, + serverId: environment.serverId, + ...imageContext, + client, + }); + const [image, setImage] = useState(null); + const pressLink = useStableEvent(({ url, sourceType }: LinkPressEvent) => { + const source = linkSource(url, sourceType); + if (onLinkPress && !actions.canResolveFile(source)) onLinkPress(url); + else actions.open(source, "preferred"); + }); + const pressImage = useStableEvent(({ url }: { url: string }) => { + if (url !== PENDING_MARKDOWN_IMAGE) setImage({ type: "uri", uri: url }); + }); + const closeImage = useStableEvent(() => setImage(null)); + const segments = useMemo( + () => + isWeb ? [{ kind: "markdown", id: "0", source: markdown }] : splitMermaidFences(markdown), + [markdown], + ); + const content = ( + + {segments.map((segment) => + segment.kind === "mermaid" ? ( + ) : ( - + ), )} - + {image ? : null} + ); -} - -function keyMarkdownGroups( - groups: MarkdownPartGroup[], -): { key: string; group: MarkdownPartGroup }[] { - return groups.map((group, index) => ({ key: `${group.kind}:${index}`, group })); -} - -function MarkdownPart({ - part, - rendererProps, -}: { - part: MarkdownDisplayPart; - rendererProps: MarkdownPartRendererProps; -}) { - if (part.kind === "details") { - return ; - } - - if (part.kind === "inlineImage") { - return ; - } - - if (part.text.length === 0) { - return null; - } - - return ; -} - -function MarkdownFragment({ - text, - compact, - rules, - markdownit, - onLinkPress, - allowedImageHandlers, - topLevelMaxExceededItem, -}: MarkdownRendererProps & { rules: RenderRules }) { - const uniProps = compact ? compactMarkdownStyleMapping : markdownStyleMapping; - return ( - - {text} - - ); -} - -function useNaturalImageDimensions(part: MarkdownInlineImagePart): { - natural: InlineImageDimensions | null; - failed: boolean; - setFailed: (failed: boolean) => void; -} { - const cached = useMemo(() => getAssistantImageMetadata({ source: part.src }), [part.src]); - const [resolved, setResolved] = useState<{ - source: string; - dimensions: InlineImageDimensions; - } | null>(null); - const natural = resolved?.source === part.src ? resolved.dimensions : cached; - const [failed, setFailed] = useState(false); - - useEffect(() => { - if ((part.width && part.height) || cached) { - return; - } - - let cancelled = false; - Image.getSize( - part.src, - (width, height) => { - setAssistantImageMetadata({ source: part.src }, { width, height }); - if (!cancelled) { - setResolved({ source: part.src, dimensions: { width, height } }); - } - }, - () => { - if (!cancelled) { - setFailed(true); - } - }, - ); - - return () => { - cancelled = true; - }; - }, [cached, part.height, part.src, part.width]); - - return { natural, failed, setFailed }; -} - -function MarkdownInlineImage({ - part, - onLinkPress, -}: { - part: Extract; - onLinkPress?: (url: string) => boolean; -}) { - const { natural: naturalDimensions } = useNaturalImageDimensions(part); - const explicitDimensions = useMemo( - () => ({ width: part.width, height: part.height }), - [part.height, part.width], - ); - const handlePress = useCallback(() => { - if (!part.href) return; - if (onLinkPress?.(part.href) === false) return; - void openExternalUrl(part.href); - }, [onLinkPress, part.href]); - const source = useMemo(() => ({ uri: part.src }), [part.src]); - const imageSize = useMemo( - () => resolveInlineImageSize({ explicit: explicitDimensions, natural: naturalDimensions }), - [explicitDimensions, naturalDimensions], - ); - const imageStyle = useMemo(() => [detailsStyles.inlineImage, imageSize], [imageSize]); - - const image = ( - - ); - - if (!part.href) { - return {image}; - } - - return ( - - {image} - - ); -} - -const FLOW_IMAGE_MAX_HEIGHT = 18; - -function MarkdownFlowImage({ - part, - onLinkPress, -}: { - part: MarkdownInlineImagePart; - onLinkPress?: (url: string) => boolean; -}) { - const { natural, failed, setFailed } = useNaturalImageDimensions(part); - const handlePress = useCallback(() => { - if (!part.href) return; - if (onLinkPress?.(part.href) === false) return; - void openExternalUrl(part.href); - }, [onLinkPress, part.href]); - const handleError = useCallback(() => setFailed(true), [setFailed]); - const source = useMemo(() => ({ uri: part.src }), [part.src]); - const imageSize = useMemo(() => { - const size = resolveInlineImageSize({ - explicit: { width: part.width, height: part.height }, - natural, - }); - const scale = Math.min(1, FLOW_IMAGE_MAX_HEIGHT / size.height); - return { width: Math.round(size.width * scale), height: Math.round(size.height * scale) }; - }, [natural, part.height, part.width]); - const imageStyle = useMemo(() => [detailsStyles.flowImage, imageSize], [imageSize]); - - if (failed) { - if (!part.alt) { - return null; - } - return ( - - {part.alt} - - ); - } - - const image = ( - - ); - - if (!part.href) { - return image; - } - - return ( - - {image} - - ); -} - -function MarkdownImageTextGroup({ - group, - rendererProps, -}: { - group: Extract; - rendererProps: MarkdownPartRendererProps; -}) { - return ( - <> - - {group.images.map((image) => ( - - ))} - - - - - {group.rest ? : null} - - ); -} - -function MarkdownDetails({ - part, - rendererProps, -}: { - part: Extract; - rendererProps: MarkdownPartRendererProps; -}) { - const [open, setOpen] = useState(false); - const toggle = useCallback(() => setOpen((current) => !current), []); - const bodyParts = useMemo( - () => part.bodyParts ?? [{ kind: "markdown" as const, text: part.body }], - [part.body, part.bodyParts], - ); - return ( - - - {open ? ( - - ) : ( - - )} - {part.summary} - - {open ? ( - - - - ) : null} - - ); -} - -interface MarkdownInheritedTextProps { - inheritedStyles: TextStyle; - textStyle: TextStyle; - style?: TextStyle; - monoSurface?: boolean; - onPress?: TextProps["onPress"]; - accessibilityRole?: TextProps["accessibilityRole"]; - children: ReactNode; -} - -export function MarkdownInheritedText({ - inheritedStyles, - textStyle, - style: overrideStyle, - monoSurface, - onPress, - accessibilityRole, - children, -}: MarkdownInheritedTextProps) { - const style = useMemo( - () => [inheritedStyles, textStyle, overrideStyle], - [inheritedStyles, textStyle, overrideStyle], - ); - return ( - - {children} - - ); -} - -interface MarkdownListItemContentProps { - contentStyle: ViewStyle; - children: ReactNode; -} - -function MarkdownListItemContent({ contentStyle, children }: MarkdownListItemContentProps) { - const style = useMemo(() => [contentStyle, MARKDOWN_LIST_ITEM_CONTENT_FLEX], [contentStyle]); - return {children}; -} - -interface MarkdownListViewProps { - baseStyle: ViewStyle; - spacing: { marginTop: number; marginBottom: number }; - children: ReactNode; -} - -function MarkdownListView({ baseStyle, spacing, children }: MarkdownListViewProps) { - const style = useMemo(() => [baseStyle, spacing], [baseStyle, spacing]); - return {children}; -} - -interface SharedMarkdownLinkProps { - href: string; - inheritedStyles: TextStyle; - linkStyle: TextStyle; - onLinkPress?: (url: string) => boolean; - children: ReactNode; -} - -function SharedMarkdownLink({ - href, - inheritedStyles, - linkStyle, - onLinkPress, - children, -}: SharedMarkdownLinkProps) { - const handlePress = useCallback(() => { - if (!href) return; - if (onLinkPress?.(href) === false) return; - void openExternalUrl(href); - }, [href, onLinkPress]); - const style = useMemo(() => [inheritedStyles, linkStyle], [inheritedStyles, linkStyle]); - - if (!isNative) { - return ( - - {children} - - ); - } - - return ( - - {children} - - ); -} - -function getMarkdownLinkHref(node: ASTNode): string { - const href = node.attributes?.href; - return typeof href === "string" ? href : ""; -} - -export function createSharedMarkdownRules(): RenderRules { - return { - text: ( - node: ASTNode, - _children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {node.content} - - ), - textgroup: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {children} - - ), - strong: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {children} - - ), - em: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {children} - - ), - s: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {children} - - ), - hardbreak: ( - node: ASTNode, - _children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {"\n"} - - ), - softbreak: ( - node: ASTNode, - _children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {"\n"} - - ), - code_block: ( - node: ASTNode, - _children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - ), - fence: ( - node: ASTNode, - _children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - ), - code_inline: ( - node: ASTNode, - _children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {node.content ?? ""} - - ), - bullet_list: ( - node: ASTNode, - children: ReactNode[], - parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {children} - - ), - ordered_list: ( - node: ASTNode, - children: ReactNode[], - parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {children} - - ), - list_item: ( - node: ASTNode, - children: ReactNode[], - parent: ASTNode[], - styles: MarkdownStyles, - ) => { - const { isOrdered, marker } = getMarkdownListMarker(node, parent); - const iconStyle = isOrdered ? styles.ordered_list_icon : styles.bullet_list_icon; - const contentStyle = isOrdered ? styles.ordered_list_content : styles.bullet_list_content; - - return ( - - {marker} - {children} - - ); - }, - th: (node: ASTNode, children: ReactNode[], _parent: ASTNode[], styles: MarkdownStyles) => ( - - {children} - - ), - td: (node: ASTNode, children: ReactNode[], _parent: ASTNode[], styles: MarkdownStyles) => ( - - {children} - - ), - paragraph: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {children} - - ), - link: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - onLinkPress?: (url: string) => boolean, - ) => ( - - {colorMarkdownLinkChildren(children, styles.link.color)} - - ), - }; -} - -const detailsStyles = StyleSheet.create((theme) => ({ - container: { - borderWidth: 1, - borderColor: theme.colors.border, - borderRadius: theme.borderRadius.md, - marginBottom: theme.spacing[2], - overflow: "hidden", - }, - summaryRow: { - flexDirection: "row", - alignItems: "center", - gap: theme.spacing[1], - paddingHorizontal: theme.spacing[2], - paddingVertical: theme.spacing[2], - backgroundColor: theme.colors.surface2, - }, - summaryIcon: { - color: theme.colors.foregroundMuted, - }, - summaryText: { - flex: 1, - minWidth: 0, - color: theme.colors.foreground, - fontSize: theme.fontSize.base, - fontWeight: theme.fontWeight.normal, - lineHeight: 18, - }, - body: { - paddingHorizontal: theme.spacing[2], - paddingTop: theme.spacing[2], - }, - inlineImageWrap: { - alignSelf: "flex-start", - marginBottom: theme.spacing[1], - }, - inlineImage: {}, - imageTextRow: { - flexDirection: "row", - alignItems: "flex-start", - gap: theme.spacing[2], - marginBottom: theme.spacing[3], - }, - imageTextRowContent: { - flex: 1, - minWidth: 0, - }, - flowImage: { - marginTop: 2, - }, - flowImageFallback: { - marginTop: 2, - paddingHorizontal: theme.spacing[1], - borderRadius: theme.borderRadius.sm, - backgroundColor: theme.colors.surface2, - }, - flowImageFallbackText: { - fontSize: 10, - lineHeight: 14, - color: theme.colors.foregroundMuted, - }, -})); + return {content}; +}); diff --git a/packages/app/src/components/markdown/renderers.tsx b/packages/app/src/components/markdown/renderers.tsx new file mode 100644 index 0000000000..37132d27e9 --- /dev/null +++ b/packages/app/src/components/markdown/renderers.tsx @@ -0,0 +1,3 @@ +import type { MarkdownRenderers } from "react-native-enriched-markdown"; + +export const WEB_RENDERERS: MarkdownRenderers | undefined = undefined; diff --git a/packages/app/src/components/markdown/renderers.web.tsx b/packages/app/src/components/markdown/renderers.web.tsx new file mode 100644 index 0000000000..6f0afc666b --- /dev/null +++ b/packages/app/src/components/markdown/renderers.web.tsx @@ -0,0 +1,81 @@ +import { Text } from "react-native"; +import { forwardRef, useMemo, type ComponentProps } from "react"; +import type { + CodeBlockRendererProps, + LinkRendererProps, + MarkdownRenderers, +} from "react-native-enriched-markdown"; +import { StyleSheet, withUnistyles } from "react-native-unistyles"; +import { useFileLink } from "@/assistant-file-links"; +import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"; +import { HighlightedCodeBlock } from "@/components/highlighted-code-block"; +import type { Theme } from "@/styles/theme"; +import { MermaidFence } from "./fence/mermaid"; +import { markdownLinkSource } from "./source"; + +const ThemedCodeBlock = withUnistyles(HighlightedCodeBlock); +const codeStyle = (theme: Theme) => ({ + inheritedStyles: { + color: theme.colors.foreground, + fontFamily: theme.fontFamily.mono, + fontSize: theme.fontSize.code, + }, + textStyle: { backgroundColor: theme.colors.surface2, padding: 12, borderRadius: 8 }, +}); +function WebCodeBlock({ code, language }: CodeBlockRendererProps) { + if (language?.toLowerCase() === "mermaid") return ; + return ; +} + +function WebLink({ link, anchorProps, children }: LinkRendererProps) { + const { tooltipPath, onHoverIn } = useFileLink(markdownLinkSource(link.url, link.sourceType)); + const anchorStyle = useMemo( + () => ({ ...anchorProps.style, overflowWrap: "anywhere" as const }), + [anchorProps.style], + ); + const anchor = ( + + {children} + + ); + return ( + + + {anchor} + + {tooltipPath ? ( + + + {tooltipPath} + + + ) : null} + + ); +} +// TooltipTrigger also supplies native Pressable handlers; forward only DOM events. +// A View renders a div, which is invalid inside the library's prose paragraphs. +const InlineLinkTrigger = forwardRef>( + function InlineLinkTrigger({ children, onMouseEnter, onMouseLeave, onFocus, onBlur }, ref) { + return ( + + {children} + + ); + }, +); +const styles = StyleSheet.create((theme) => ({ + tooltipPath: { + color: theme.colors.foreground, + fontSize: theme.fontSize.sm, + fontWeight: theme.fontWeight.normal, + }, +})); + +export const WEB_RENDERERS: MarkdownRenderers = { codeBlock: WebCodeBlock, link: WebLink }; diff --git a/packages/app/src/components/markdown/source.ts b/packages/app/src/components/markdown/source.ts new file mode 100644 index 0000000000..fc6f7463a5 --- /dev/null +++ b/packages/app/src/components/markdown/source.ts @@ -0,0 +1,201 @@ +import { ASSISTANT_FILE_EXTENSIONS } from "@/assistant-file-links/extensions"; +import { normalizeMarkdownHtml } from "./html"; +import { gfmTable } from "micromark-extension-gfm-table"; +import { gfmTableFromMarkdown } from "mdast-util-gfm-table"; +import type { AssistantFileLinkSource } from "@/assistant-file-links"; +import { decodeString } from "micromark-util-decode-string"; +import { fromMarkdown } from "mdast-util-from-markdown"; +import type { Link, Nodes, Root } from "mdast"; + +// Keep this regex in the syntax shared by JavaScript, ICU and java.util.regex. +// The Unicode flag makes Metro expand it to escapes rejected by native engines. +const PATH_CHARACTER = String.raw`[a-zA-Z0-9\u0080-\uFFFF_@.%+-]`; +const PATH_LETTER = String.raw`[a-zA-Z]`; +const PATH_SEGMENT = String.raw`${PATH_CHARACTER}*[a-zA-Z0-9\u0080-\uFFFF_@%+-]`; +const LINE_SUFFIX = String.raw`(?::\d+(?::\d+)?(?:-\d+(?::\d+)?)?)?`; +const FILE_EXTENSIONS = [...ASSISTANT_FILE_EXTENSIONS] + .map((extension) => extension.replace(/[a-z]/g, (letter) => `[${letter}${letter.toUpperCase()}]`)) + .join("|"); +const PATH_PREFIX = String.raw`(?:[A-Za-z]:[\\/]|\.{0,2}\/|~\/)`; +const HAS_PATH_LETTER = String.raw`(?=${PATH_CHARACTER}*(?:[\\/]${PATH_CHARACTER}*)*${PATH_LETTER})`; +const SEPARATED_PATH = String.raw`${PATH_SEGMENT}(?:[\\/]${PATH_SEGMENT})+`; +const FILE_NAME = String.raw`${PATH_SEGMENT}\.(?:${FILE_EXTENSIONS})`; +// Bare prose paths need a known extension, a line suffix, or at least two separators. +const PROSE_PATH = String.raw`(?:${PATH_PREFIX}${HAS_PATH_LETTER}${SEPARATED_PATH}|${HAS_PATH_LETTER}(?:${PATH_SEGMENT}(?:[\\/]${PATH_SEGMENT})*[\\/]${FILE_NAME}|${PATH_SEGMENT}(?:[\\/]${PATH_SEGMENT}){2,}|${SEPARATED_PATH}(?=:)))`; +const INLINE_CODE_PATH = String.raw`${PATH_PREFIX}?${HAS_PATH_LETTER}${SEPARATED_PATH}`; + +function createFilePattern(path: string): RegExp { + // The native adapter mistakes a literal + in lookbehind for a quantifier; use its hex escape. + return new RegExp( + String.raw`(?:https?://[^\s<>]+|(? + (tree ??= fromMarkdown(source, { + extensions: [gfmTable()], + mdastExtensions: [gfmTableFromMarkdown()], + })); + const edits: { start: number; end: number; text: string }[] = []; + if ( + !source.includes("![") && + !source.includes("||") && + !source.includes("&") && + !(workspaceRoot && source.includes("](")) + ) { + return { images, ...linkMetadata(), render: () => source }; + } + + const definitions = new Map(); + function definitionsIn(node: Nodes) { + if (node.type === "definition" && !definitions.has(node.identifier)) + definitions.set(node.identifier, node); + if ("children" in node) node.children.forEach(definitionsIn); + } + definitionsIn(getTree()); + function collectImage( + node: Extract, + start: number, + end: number, + ) { + const target = node.type === "image" ? node : definitions.get(node.identifier); + if ( + target && + options.supportsSvg === false && + /(?:\.svg(?:[?#]|$)|^data:image\/svg\+xml)/i.test(target.url) + ) { + edits.push({ start, end, text: escapeMarkdownText(node.alt ?? "") }); + return; + } + if (target) + images.push({ + source: target.url, + alt: node.alt ?? "", + title: target.title ?? null, + start, + end, + }); + } + function visit(node: Nodes) { + const start = node.position?.start.offset; + const end = node.position?.end.offset; + if (start !== undefined && end !== undefined) { + if (node.type === "link") { + const title = workspaceHoverTitle(node, source.slice(start, end), workspaceRoot); + if (title !== null) + edits.push({ start: end - 1, end: end - 1, text: ` ${JSON.stringify(title)}` }); + } + if (node.type === "tableRow") { + // MD4C's always-enabled spoiler extension otherwise consumes adjacent cell + // delimiters. Spaces keep empty GFM cells empty without producing spoilers. + const row = source.slice(start, end); + for (const match of row.matchAll(/(? + decodeString(entity).replace(/[!-/:-@[-`{-~]/g, "\\$&"), + ) + .replace(/(? = new Map(), + unavailableImages: ReadonlySet = new Set(), + pendingImages: ReadonlySet = new Set(), + ) { + const replacements = [...edits]; + for (const image of images) { + const unavailable = unavailableImages.has(image.source); + const pending = pendingImages.has(image.source); + if (!unavailable && !pending && /^(https?:|data:|blob:)/i.test(image.source)) continue; + let url = unavailable ? undefined : urls.get(image.source); + if (pending && !unavailable) url = PENDING_MARKDOWN_IMAGE; + replacements.push({ + start: image.start, + end: image.end, + text: url + ? `![${image.alt.replace(/[[\]\\]/g, "\\$&")}](<${url}>${image.title ? ` ${JSON.stringify(image.title)}` : ""})` + : escapeMarkdownText(image.alt), + }); + } + let result = source; + for (const edit of replacements.sort((a, b) => b.start - a.start)) { + result = result.slice(0, edit.start) + edit.text + result.slice(edit.end); + } + return result; + }, + }; +} + +function escapeMarkdownText(text: string): string { + return text.replace(/[!-/:-@[-`{-~]/g, "\\$&"); +} diff --git a/packages/app/src/components/markdown/streaming.fixture.md b/packages/app/src/components/markdown/streaming.fixture.md new file mode 100644 index 0000000000..da20f9dac7 --- /dev/null +++ b/packages/app/src/components/markdown/streaming.fixture.md @@ -0,0 +1,47 @@ +# Streaming feel experiment + +The stream should stay easy to follow while new text arrives. Small groups of words explain the next step without distracting from what is already readable. We keep the same fixture and delivery rate so differences come from rendering rather than the content. The stream should stay easy to follow while new text arrives. Small groups of words explain the next step without distracting from what is already readable. We keep the same fixture and delivery rate so differences come from rendering rather than the content. The stream should stay easy to follow while new text arrives. Small groups of words explain the next step without distracting from what is already readable. We keep the same fixture and delivery rate so differences come from rendering rather than the content. + +## Plan + +- Read the current state and identify the relevant boundary. +- Keep the implementation small and verify the visible result. +- Compare the recordings at their original playback speed. + +The stream should stay easy to follow while new text arrives. Small groups of words explain the next step without distracting from what is already readable. We keep the same fixture and delivery rate so differences come from rendering rather than the content. The stream should stay easy to follow while new text arrives. Small groups of words explain the next step without distracting from what is already readable. We keep the same fixture and delivery rate so differences come from rendering rather than the content. + +## Example + +```typescript +const value1 = calculateTotal(items.slice(0, 1)); // update the next result +const value2 = calculateTotal(items.slice(0, 2)); // update the next result +const value3 = calculateTotal(items.slice(0, 3)); // update the next result +const value4 = calculateTotal(items.slice(0, 4)); // update the next result +const value5 = calculateTotal(items.slice(0, 5)); // update the next result +const value6 = calculateTotal(items.slice(0, 6)); // update the next result +const value7 = calculateTotal(items.slice(0, 7)); // update the next result +const value8 = calculateTotal(items.slice(0, 8)); // update the next result +const value9 = calculateTotal(items.slice(0, 9)); // update the next result +const value10 = calculateTotal(items.slice(0, 10)); // update the next result +const value11 = calculateTotal(items.slice(0, 11)); // update the next result +const value12 = calculateTotal(items.slice(0, 12)); // update the next result +``` + +## Results + +| Stage | Observation | Result | +| ------ | ---------------------------------------- | ------ | +| Step 1 | The next group of words becomes readable | Ready | +| Step 2 | The next group of words becomes readable | Ready | +| Step 3 | The next group of words becomes readable | Ready | +| Step 4 | The next group of words becomes readable | Ready | +| Step 5 | The next group of words becomes readable | Ready | +| Step 6 | The next group of words becomes readable | Ready | +| Step 7 | The next group of words becomes readable | Ready | +| Step 8 | The next group of words becomes readable | Ready | + +## Explanation + +The stream should stay easy to follow while new text arrives. Small groups of words explain the next step without distracting from what is already readable. We keep the same fixture and delivery rate so differences come from rendering rather than the content. The stream should stay easy to follow while new text arrives. Small groups of words explain the next step without distracting from what is already readable. We keep the same fixture and delivery rate so differences come from rendering rather than the content. The stream should stay easy to follow while new text arrives. Small groups of words explain the next step without distracting from what is already readable. We keep the same fixture and delivery rate so differences come from rendering rather than the content. The stream should stay easy to follow while new text arrives. Small groups of words explain the next step without distracting from what is already readable. We keep the same fixture and delivery rate so differences come from rendering rather than the content. The stream should stay easy to follow while new text arrives. Small groups of words explain the next step without distracting from what is already readable. We keep the same fixture and delivery rate so differences come from rendering rather than the content. The stream should stay easy to follow while new text arrives. Small groups of words explain the next step without distracting from what is already readable. We keep the same fixture and delivery rate so differences come from rendering rather than the content. The stream should stay easy to follow while new text arrives. Small groups of words explain the next step without distracting from what is already readable. We keep the same fixture and delivery rate so differences come from rendering rather than the content. The stream should stay easy to follow while new text arrives. Small groups of words explain the next step without distracting from what is already readable. We keep the same fixture and delivery rate so differences come from rendering rather than the content. + +END STREAMING FEEL diff --git a/packages/app/src/components/markdown/streaming.test.ts b/packages/app/src/components/markdown/streaming.test.ts new file mode 100644 index 0000000000..ba18f28c89 --- /dev/null +++ b/packages/app/src/components/markdown/streaming.test.ts @@ -0,0 +1,71 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; +import { splitStreamingMarkdown } from "./streaming"; + +describe("native streaming Markdown", () => { + it("keeps completed messages together for selection", () => { + const text = "First.\n\nSecond.\n\nThird."; + expect(splitStreamingMarkdown(text, false)).toEqual({ head: text, tail: "" }); + }); + it("commits earlier blocks only after the following source line is complete", () => { + expect(splitStreamingMarkdown("First.\n\nSecond", true).head).toBe(""); + expect(splitStreamingMarkdown("First.\n\nSecond\n", true)).toEqual({ + head: "First.\n\n", + tail: "Second\n", + }); + }); + it.each([ + "- one\n\n- two\n", + "> first\n> second\n", + "Title\n---\n", + "```mermaid\ngraph LR\nA --> B\n```\n", + "- one\n - nested\n\n paragraph\n", + " indented code\n\n still code\n", + "| A | B |\n| --- | --- |\n| x | y |\n", + "```ts\nconst value = 1;\n```\n", + "
\nbody\n\nmore\n
\n", + "[label]\n\n[label]: /target\n", + "[label]: /target\n\n[label]\n", + ])("keeps an unfinished construct together: %s", (block) => { + const prefix = "Introduction.\n\n"; + const source = prefix + block; + const split = splitStreamingMarkdown(source, true); + expect(split.head.length).toBeLessThanOrEqual(prefix.length); + expect(split.head + split.tail).toBe(source); + let previous = 0; + for (let end = 0; end <= source.length; end++) { + const prefixSplit = splitStreamingMarkdown(source.slice(0, end), true); + expect(prefixSplit.head.length, `prefix ${end} of ${source}`).toBeGreaterThanOrEqual( + previous, + ); + previous = prefixSplit.head.length; + } + }); + it("preserves the indentation that controls fence content and list continuation", () => { + for (const tail of [" ```ts\n const n = 1;\n", " - first\n continued\n"]) { + expect(splitStreamingMarkdown(`Intro.\n\n${tail}`, true)).toEqual({ + head: "Intro.\n\n", + tail, + }); + } + }); + it("keeps unresolved references and HTML in the tail even after later blocks", () => { + for (const block of ["[label]", "[label]: /target", "
body
"]) { + expect(splitStreamingMarkdown(`Intro.\n\n${block}\n\nNext.\n\nEnd.\n`, true).head).toBe( + "Intro.\n\n", + ); + } + }); + it("never retreats or loses source across every prefix of mixed Markdown", () => { + const fixture = readFileSync(new URL("./streaming.fixture.md", import.meta.url), "utf8"); + let previous = 0; + for (let end = 0; end <= fixture.length; end++) { + const text = fixture.slice(0, end); + const { head, tail } = splitStreamingMarkdown(text, true); + expect(head.length, `prefix ${end}`).toBeGreaterThanOrEqual(previous); + expect(head + tail).toBe(text); + previous = head.length; + } + expect(previous).toBeGreaterThan(1000); + }, 15000); +}); diff --git a/packages/app/src/components/markdown/streaming.ts b/packages/app/src/components/markdown/streaming.ts new file mode 100644 index 0000000000..1b6153ca44 --- /dev/null +++ b/packages/app/src/components/markdown/streaming.ts @@ -0,0 +1,32 @@ +import { fromMarkdown } from "mdast-util-from-markdown"; +import { gfmTable } from "micromark-extension-gfm-table"; +import { gfmTableFromMarkdown } from "mdast-util-gfm-table"; +import type { Nodes } from "mdast"; + +// A later definition can resolve even a currently plain-text shortcut reference. +// Keep its entire containing block, and all definitions, in the shared tail. +// HTML normalization likewise needs the complete enclosing source. +function needsDocumentContext(node: Nodes): boolean { + if (node.type === "html" || node.type === "definition") return true; + if (node.type === "linkReference" || node.type === "imageReference") return true; + if (node.type === "text" && node.value.includes("[")) return true; + return "children" in node && node.children.some(needsDocumentContext); +} + +/** The last parsed block and unfinished line are mutable; earlier blocks are closed. */ +export function splitStreamingMarkdown(text: string, streaming: boolean) { + if (!streaming) return { head: text, tail: "" }; + const completeLines = text.slice(0, text.lastIndexOf("\n") + 1); + const tree = fromMarkdown(completeLines, { + extensions: [gfmTable()], + mdastExtensions: [gfmTableFromMarkdown()], + }); + let boundary = 0; + for (let index = 0; index < tree.children.length - 1; index++) { + if (needsDocumentContext(tree.children[index])) break; + // mdast starts some nodes after their indentation; the next parser needs it too. + const nextStart = tree.children[index + 1].position!.start.offset!; + boundary = text.lastIndexOf("\n", nextStart - 1) + 1; + } + return { head: text.slice(0, boundary), tail: text.slice(boundary) }; +} diff --git a/packages/app/src/components/markdown/styles.ts b/packages/app/src/components/markdown/styles.ts new file mode 100644 index 0000000000..03d6c69b23 --- /dev/null +++ b/packages/app/src/components/markdown/styles.ts @@ -0,0 +1,100 @@ +import type { MarkdownStyle } from "react-native-enriched-markdown"; +import type { Theme } from "@/styles/theme"; +import { isWeb } from "@/constants/platform"; + +export function createMarkdownStyles( + theme: Theme, + compact = false, +): { markdownStyle: MarkdownStyle; selectionColor: string } { + const fontSize = compact ? theme.fontSize.base : theme.fontSize.content; + const paragraph = { + fontSize, + fontFamily: theme.fontFamily.ui, + lineHeight: Math.round(fontSize * 1.4), + color: theme.colors.foreground, + marginTop: 0, + marginBottom: theme.spacing[3], + }; + const heading = (scale: number) => ({ + ...paragraph, + fontSize: Math.round(fontSize * scale), + lineHeight: Math.round(fontSize * scale * 1.3), + fontWeight: "bold", + marginTop: theme.spacing[3], + }); + return { + selectionColor: theme.colors.terminal.selectionBackground, + markdownStyle: { + paragraph, + h1: heading(1.6), + h2: heading(1.4), + h3: heading(1.2), + h4: heading(1.1), + h5: heading(1), + h6: { ...heading(1), color: theme.colors.foregroundMuted }, + list: { + ...paragraph, + bulletColor: theme.colors.foregroundMuted, + markerColor: theme.colors.foregroundMuted, + gapWidth: 7, + itemSpacing: 4, + marginLeft: 20, + }, + blockquote: { + ...paragraph, + color: `${theme.colors.foreground}cc`, + backgroundColor: theme.colors.surface1, + borderColor: theme.colors.surface2, + borderWidth: 3, + padding: 12, + gapWidth: 8, + admonitions: { + note: { color: theme.colors.accentBright, backgroundColor: theme.colors.surface2 }, + tip: { color: theme.colors.statusSuccess, backgroundColor: theme.colors.surface2 }, + important: { color: theme.colors.statusMerged, backgroundColor: theme.colors.surface2 }, + warning: { color: theme.colors.statusWarning, backgroundColor: theme.colors.surface2 }, + caution: { color: theme.colors.statusDanger, backgroundColor: theme.colors.surface2 }, + }, + }, + link: { color: theme.colors.accentBright, underline: false }, + linkVariants: { + "^(?!https?://).+": { + color: theme.colors.accentBright, + }, + }, + code: { + fontFamily: theme.fontFamily.mono === "ui-monospace" && !isWeb ? "" : theme.fontFamily.mono, + fontSize: theme.fontSize.code, + color: theme.colors.foreground, + backgroundColor: theme.colors.surface2, + borderColor: "transparent", + }, + codeBlock: { + ...paragraph, + fontFamily: theme.fontFamily.mono === "ui-monospace" && !isWeb ? "" : theme.fontFamily.mono, + fontSize: theme.fontSize.code, + backgroundColor: theme.colors.surface2, + borderColor: theme.colors.border, + borderRadius: 8, + padding: 12, + }, + table: { + ...paragraph, + borderColor: theme.colors.border, + headerBackgroundColor: theme.colors.surface2, + headerTextColor: theme.colors.foreground, + rowEvenBackgroundColor: theme.colors.surface0, + rowOddBackgroundColor: theme.colors.surface1, + }, + taskList: { + checkedTextColor: theme.colors.foregroundMuted, + checkedColor: theme.colors.accent, + checkmarkColor: theme.colors.accentForeground, + borderColor: theme.colors.foregroundMuted, + }, + inlineImage: { size: paragraph.lineHeight }, + image: { height: 320, resizeMode: "contain", borderRadius: 8 }, + thematicBreak: { color: theme.colors.border, height: 1, marginTop: 12, marginBottom: 12 }, + }, + }; +} diff --git a/packages/app/src/components/message.tsx b/packages/app/src/components/message.tsx index 7915832470..314f414d48 100644 --- a/packages/app/src/components/message.tsx +++ b/packages/app/src/components/message.tsx @@ -1,53 +1,73 @@ -import type { AgentMessage } from "@getpaseo/protocol/agent-message"; -import { ASSISTANT_IMAGE_DEFAULT_ASPECT_RATIO } from "@/utils/assistant-image-metadata"; -import { LoadingSpinner } from "@/components/ui/loading-spinner"; +import { useStableEvent } from "@/hooks/use-stable-event"; +import { getAgentAttachmentPillContent } from "@/attachments/attachment-pill-content"; +import { AssistantForkMenu, type AssistantForkTarget } from "@/components/assistant-fork-menu"; +import { AttachmentLightbox, type ImageLightboxSource } from "@/components/attachment-lightbox"; +import { + AttachmentFrame, + AttachmentLabel, + AttachmentThumbnail, +} from "@/components/attachment-pill"; +import { MarkdownRenderer } from "@/components/markdown/renderer"; +import { useRetainedPanelActive } from "@/components/retained-panel"; +import { RewindMenu, type RewindMode } from "@/components/rewind/rewind-menu"; +import { useRewindAgentMutation } from "@/components/rewind/use-rewind-agent-mutation"; import { TaskListRow } from "@/components/task-list-row"; -import { - View, - Text, - Image, - Pressable, - type GestureResponderEvent, - type LayoutChangeEvent, - StyleProp, - ViewStyle, - type TextStyle, -} from "react-native"; -import { useTranslation } from "react-i18next"; -import { MarkdownParagraphView, MarkdownTextSpan } from "@/components/markdown-text"; -import { MarkdownTableCellText } from "@/components/markdown-text-selection"; -import * as React from "react"; -import { - useState, - useEffect, - useRef, - memo, - useMemo, - useCallback, - createContext, - useContext, -} from "react"; -import type { ComponentType, ReactNode } from "react"; -import type MarkdownIt from "markdown-it"; -import { type ASTNode, type RenderRules } from "react-native-markdown-display"; +import { LoadingSpinner } from "@/components/ui/loading-spinner"; +import { useIsCompactFormFactor } from "@/constants/layout"; +import { isNative, isWeb } from "@/constants/platform"; +import { useRevealedText, type MarkdownPhase } from "@/hooks/use-revealed-text"; +import { ICON_SIZE, type Theme } from "@/styles/theme"; +import { inlineUnistylesStyle } from "@/styles/unistyles-inline-style"; +import { buildToolCallPresentation } from "@/tool-calls/presentation"; +import type { TaskActivity, TodoEntry, UserMessageImageAttachment } from "@/types/stream"; +import { setAssistantMessageHeight } from "@/utils/assistant-message-height-estimate"; +import { isRenderProfileEnabled } from "@/utils/render-profiler"; +import { writeMarkdownToRichClipboard } from "@/utils/rich-clipboard"; +import { getDefaultMarkdownClipboardEnvironment } from "@/utils/rich-clipboard-default-environment"; +import { formatDuration, formatMessageTimestamp } from "@/utils/time"; +import { resolveToolCallIcon } from "@/utils/tool-call-icon"; +import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; +import type { AgentMessage } from "@getpaseo/protocol/agent-message"; +import type { AgentCapabilityFlags, ToolCallDetail } from "@getpaseo/protocol/agent-types"; +import type { AgentAttachment } from "@getpaseo/protocol/messages"; import MaskedView from "@react-native-masked-view/masked-view"; import { - Info, - XCircle, - ChevronRight, Check, CheckSquare, + ChevronRight, CircleDot, Copy, - Plus, - TriangleAlertIcon, - Scissors, - MicVocal, FileSymlink, + Info, + MicVocal, + Plus, + Scissors, + TriangleAlertIcon, + XCircle, } from "lucide-react-native"; -import { StyleSheet, withUnistyles } from "react-native-unistyles"; -import { ICON_SIZE, type Theme } from "@/styles/theme"; -import { useIsCompactFormFactor } from "@/constants/layout"; +import type { ComponentType, ReactNode } from "react"; +import * as React from "react"; +import { + createContext, + memo, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { useTranslation } from "react-i18next"; +import { + Pressable, + StyleProp, + Text, + View, + ViewStyle, + type GestureResponderEvent, + type LayoutChangeEvent, + type TextStyle, +} from "react-native"; import Animated, { Easing, cancelAnimation, @@ -56,67 +76,14 @@ import Animated, { withRepeat, withTiming, } from "react-native-reanimated"; -import Svg, { Defs, LinearGradient as SvgLinearGradient, Rect, Stop } from "react-native-svg"; -import { inlineUnistylesStyle } from "@/styles/unistyles-inline-style"; -import { MarkdownRenderer, type MarkdownStyles } from "@/components/markdown/renderer"; -import type { TaskActivity, TodoEntry, UserMessageImageAttachment } from "@/types/stream"; -import type { AgentAttachment } from "@getpaseo/protocol/messages"; -import type { ToolCallDetail } from "@getpaseo/protocol/agent-types"; -import { buildToolCallPresentation } from "@/tool-calls/presentation"; -import { resolveToolCallIcon } from "@/utils/tool-call-icon"; -import { getMarkdownListMarker, getMarkdownListSpacing } from "@/utils/markdown-list"; -import { markdownNodeContainsType } from "@/utils/markdown-ast"; -import { useStableEvent } from "@/hooks/use-stable-event"; -import { HighlightedCodeBlock } from "@/components/highlighted-code-block"; -import { MarkdownFenceBlock } from "@/components/markdown/fence"; -import type { MarkdownPhase } from "@/components/markdown/fence/types"; -import { splitMarkdownBlocks } from "@/utils/split-markdown-blocks"; -import { useRevealedText } from "@/hooks/use-revealed-text"; -import { colorMarkdownLinkChildren } from "@/components/markdown/link-children"; -import { createAssistantMarkdownParser } from "@/utils/assistant-markdown-parser"; -import { formatDuration, formatMessageTimestamp } from "@/utils/time"; -import { getTurnDurationLabel } from "./assistant-turn-footer-label"; -import { writeMarkdownToRichClipboard } from "@/utils/rich-clipboard"; -import { getDefaultMarkdownClipboardEnvironment } from "@/utils/rich-clipboard-default-environment"; -import { setAssistantMarkdownBlockHeight } from "@/utils/assistant-message-height-estimate"; -import { isRenderProfileEnabled } from "@/utils/render-profiler"; -import { getAgentAttachmentPillContent } from "@/attachments/attachment-pill-content"; -import { PlanCard } from "./plan-card"; -import { useToolCallSheet } from "./tool-call-sheet"; -import { ToolCallDetailsContent } from "./tool-call-details"; -import { - AssistantInlineCodePathLink, - type AssistantFileLinkSource, - AssistantMarkdownCodeLink, - AssistantMarkdownLink, - type InlinePathTarget, - useAssistantFileLinkActions, - useAssistantLinkPress, -} from "@/assistant-file-links"; -import { getCompactionMarkerLabel } from "./message-compaction-label"; -import { useAssistantImage } from "@/assistant-image/use-assistant-image"; -import { localFileSourceToPath } from "@/attachments/utils"; -import { - AttachmentFrame, - AttachmentLabel, - AttachmentThumbnail, -} from "@/components/attachment-pill"; -import { AttachmentLightbox, type ImageLightboxSource } from "@/components/attachment-lightbox"; -import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; -import { isWeb, isNative } from "@/constants/platform"; -import type { AgentCapabilityFlags } from "@getpaseo/protocol/agent-types"; -import { RewindMenu, type RewindMode } from "@/components/rewind/rewind-menu"; -import { useRewindAgentMutation } from "@/components/rewind/use-rewind-agent-mutation"; -import { AssistantForkMenu, type AssistantForkTarget } from "@/components/assistant-fork-menu"; -import { useRetainedPanelActive } from "@/components/retained-panel"; -import { - markdownCopyDataSet, - markdownCopyImageDataSet, - markdownCopyOrderedListDataSet, - markdownCopyTableCellDataSet, - type MarkdownCopyInlineTag, -} from "@/assistant-selection-copy/markup"; +import Svg, { Defs, Rect, Stop, LinearGradient as SvgLinearGradient } from "react-native-svg"; +import { StyleSheet, withUnistyles } from "react-native-unistyles"; import { capAssistantMessageForRender, getUtf8ByteLength } from "./assistant-message-render-limit"; +import { getTurnDurationLabel } from "./assistant-turn-footer-label"; +import { getCompactionMarkerLabel } from "./message-compaction-label"; +import { PlanCard } from "./plan-card"; +import { ToolCallDetailsContent } from "./tool-call-details"; +import { useToolCallSheet } from "./tool-call-sheet"; export type { InlinePathTarget } from "@/assistant-file-links"; export type { AssistantForkTarget }; @@ -159,20 +126,10 @@ function useDisableOuterSpacing(disableOuterSpacing: boolean | undefined) { const WEB_TOOLCALL_SHIMMER_KEYFRAME_ID = "paseo-toolcall-shimmer-keyframes"; const WEB_TOOLCALL_SHIMMER_ANIMATION_NAME = "paseo-toolcall-shimmer"; -const MARKDOWN_ALLOWED_IMAGE_HANDLERS = [ - "data:image/png;base64", - "data:image/gif;base64", - "data:image/jpeg;base64", - "https://", - "http://", -] as const; -const MARKDOWN_TOP_LEVEL_MAX_EXCEEDED_ITEM = ...; - const ThemedMicVocal = withUnistyles(MicVocal); const ThemedFileSymlinkIcon = withUnistyles(FileSymlink); const ThemedTriangleAlertIcon = withUnistyles(TriangleAlertIcon); const ThemedChevronRightIcon = withUnistyles(ChevronRight); -const ThemedLoadingSpinner = withUnistyles(LoadingSpinner); const ThemedNotificationInfo = withUnistyles(Info); const ThemedNotificationWarning = withUnistyles(TriangleAlertIcon); const ThemedNotificationError = withUnistyles(XCircle); @@ -818,200 +775,6 @@ export const assistantMessageStylesheet = StyleSheet.create((theme) => ({ }, })); -function AssistantMarkdownImage({ - source, - occurrenceKey, - alt, - hasLeadingContent, - client, - workspaceRoot, - serverId, -}: { - source: string; - occurrenceKey: string; - alt?: string; - hasLeadingContent: boolean; - client?: DaemonClient | null; - workspaceRoot?: string; - serverId?: string; -}) { - const { t } = useTranslation(); - const [viewerOpen, setViewerOpen] = useState(false); - const openViewer = useCallback(() => setViewerOpen(true), []); - const closeViewer = useCallback(() => setViewerOpen(false), []); - const containerStyle = useMemo>( - () => ({ - marginTop: hasLeadingContent ? 16 : 0, - marginBottom: 0, - }), - [hasLeadingContent], - ); - const image = useAssistantImage({ - source, - occurrenceKey, - client, - workspaceRoot, - serverId, - }); - const binding = image.status === "failed" ? null : image.binding; - const aspectRatio = image.status === "failed" ? null : image.aspectRatio; - const imageUri = binding?.uri ?? ""; - const imageSource = useMemo(() => ({ uri: imageUri }), [imageUri]); - const frameStyle = useMemo>( - () => [assistantMessageStylesheet.imageFrame, containerStyle], - [containerStyle], - ); - const imageSizeStyle = useMemo(() => { - if (image.status === "failed") return { height: 160 }; - return { aspectRatio: aspectRatio ?? ASSISTANT_IMAGE_DEFAULT_ASPECT_RATIO }; - }, [aspectRatio, image.status]); - const surfaceStyle = useMemo>( - () => [assistantMessageStylesheet.imageSurface, imageSizeStyle], - [imageSizeStyle], - ); - const lightboxSource = useMemo(() => { - if (!viewerOpen || !imageUri) return null; - return { - type: "uri", - uri: imageUri, - contentSize: aspectRatio ? { width: aspectRatio, height: 1 } : undefined, - }; - }, [aspectRatio, imageUri, viewerOpen]); - - const stateFrameStyle = useMemo>( - () => [ - assistantMessageStylesheet.imageFrame, - containerStyle, - imageSizeStyle, - assistantMessageStylesheet.imageState, - ], - [containerStyle, imageSizeStyle], - ); - const copyDataSet = useMemo(() => markdownCopyImageDataSet(source, alt), [source, alt]); - - if (image.status === "failed") { - return ( - - {image.message} - - ); - } - - if (!binding) { - return ( - - - - ); - } - - return ( - - - - - {image.status === "loading" ? ( - - - - ) : null} - - - - - ); -} - -function getInlineCodeAutoLinkUrl(markdownParser: MarkdownIt, content: string): string | null { - const trimmed = content.trim(); - if (!trimmed) { - return null; - } - - const matches: - | { - index: number; - lastIndex: number; - url: string; - }[] - | null = markdownParser.linkify.match(trimmed); - if (!matches || matches.length !== 1) { - return null; - } - - const [match] = matches; - if (!match || match.index !== 0 || match.lastIndex !== trimmed.length) { - return null; - } - - return match.url; -} - -function getInlineCodeAutoLinkSource(input: { - href: string; - content: string; -}): AssistantFileLinkSource { - return { - href: input.href, - text: input.content, - markup: "linkify", - sourceInfo: "auto", - }; -} - -interface AssistantMarkdownAstNode extends ASTNode { - sourceInfo?: string; -} - -function getMarkdownLinkSource(node: AssistantMarkdownAstNode): AssistantFileLinkSource { - return { - href: typeof node.attributes?.href === "string" ? node.attributes.href : "", - text: getMarkdownNodeText(node), - title: typeof node.attributes?.title === "string" ? node.attributes.title : undefined, - markup: node.markup, - sourceInfo: node.sourceInfo, - sourceType: node.sourceType === "inline-code" ? "inline-code" : undefined, - }; -} - -function getMarkdownNodeText(node: ASTNode): string { - if (!node.children.length) { - return node.content ?? ""; - } - - return node.children.map(getMarkdownNodeText).join(""); -} - -function nodeHasParentType(parent: unknown, type: string): boolean { - if (Array.isArray(parent)) { - return parent.some((entry) => entry?.type === type); - } - - return ( - typeof parent === "object" && - parent !== null && - "type" in parent && - (parent as Record<"type", unknown>)["type"] === type - ); -} - const turnCopyButtonStylesheet = StyleSheet.create((theme) => ({ container: { alignSelf: "flex-start", @@ -1364,135 +1127,6 @@ function NativeShimmerPeakSvg({ gradientId }: { gradientId: string }) { ); } -interface AssistantMessageBlockContainerProps { - block: string; - marginBottom: number; - children: ReactNode; -} - -function AssistantMessageBlockContainer({ - block, - marginBottom, - children, -}: AssistantMessageBlockContainerProps) { - const style = useMemo(() => (marginBottom > 0 ? { marginBottom } : undefined), [marginBottom]); - const handleLayout = useCallback( - (event: LayoutChangeEvent) => { - const { width, height } = event.nativeEvent.layout; - setAssistantMarkdownBlockHeight({ block, width, height }); - }, - [block], - ); - return ( - - {children} - - ); -} - -interface MemoizedMarkdownBlockProps { - text: string; - rules: RenderRules; - parser: MarkdownIt; - onLinkPress: (url: string) => boolean; -} - -const MemoizedMarkdownBlock = React.memo(function MemoizedMarkdownBlock({ - text, - rules, - parser, - onLinkPress, -}: MemoizedMarkdownBlockProps) { - return ( - - ); -}); - -interface MarkdownInheritedTextProps { - inheritedStyles: TextStyle; - textStyle: TextStyle; - style?: StyleProp; - monoSurface?: boolean; - copyTag?: MarkdownCopyInlineTag; - children: ReactNode; -} - -function MarkdownInheritedText({ - inheritedStyles, - textStyle, - style: overrideStyle, - monoSurface, - copyTag, - children, -}: MarkdownInheritedTextProps) { - const style = useMemo( - () => [inheritedStyles, textStyle, overrideStyle], - [inheritedStyles, textStyle, overrideStyle], - ); - // When this span renders link label text on iOS, pick up the link's press - // handler from context and hand it to MarkdownTextSpan, which forwards it to - // the leaf string children react-native-uitextview makes tappable. Null - // outside a link (and on every other platform, where no provider mounts), so - // ordinary text is unaffected. See assistant-file-links/link-press-context. - const linkPress = useAssistantLinkPress(); - return ( - - {children} - - ); -} - -interface MarkdownListItemContentProps { - contentStyle: ViewStyle; - children: ReactNode; -} - -const MARKDOWN_LIST_ITEM_CONTENT_FLEX: ViewStyle = { flex: 1, flexShrink: 1, minWidth: 0 }; - -function MarkdownListItemContent({ contentStyle, children }: MarkdownListItemContentProps) { - const style = useMemo(() => [contentStyle, MARKDOWN_LIST_ITEM_CONTENT_FLEX], [contentStyle]); - return {children}; -} - -interface MarkdownListViewProps { - baseStyle: ViewStyle; - copyTag: "ol" | "ul"; - orderedStart?: unknown; - spacing: { marginTop: number; marginBottom: number }; - children: ReactNode; -} - -function MarkdownListView({ - baseStyle, - copyTag, - orderedStart, - spacing, - children, -}: MarkdownListViewProps) { - const style = useMemo(() => [baseStyle, spacing], [baseStyle, spacing]); - const copyDataSet = - copyTag === "ol" ? markdownCopyOrderedListDataSet(orderedStart) : markdownCopyDataSet.ul; - return ( - - {children} - - ); -} - export const AssistantMessage = memo(function AssistantMessage({ renderFullContent = false, occurrenceKey, @@ -1505,11 +1139,6 @@ export const AssistantMessage = memo(function AssistantMessage({ phase, }: AssistantMessageProps) { const { t } = useTranslation(); - const markdownParser = useMemo(createAssistantMarkdownParser, []); - const streamingMarkdownParser = useMemo( - () => createAssistantMarkdownParser({ streaming: true }), - [], - ); const renderedMessage = useMemo( () => renderFullContent ? { text: message, capped: false } : capAssistantMessageForRender(message), @@ -1522,449 +1151,6 @@ export const AssistantMessage = memo(function AssistantMessage({ [message, phase, renderedMessage.capped], ); - const fileLinkActions = useAssistantFileLinkActions(); - const handleMarkdownLinkPress = useStableEvent((url: string) => { - fileLinkActions.open({ href: url }, "preferred"); - // react-native-markdown-display opens the link itself when this returns true. - // We already handled it above, so return false to avoid duplicate opens. - return false; - }); - - const markdownRules = useMemo(() => { - return { - heading1: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {children} - - ), - heading2: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {children} - - ), - heading3: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {children} - - ), - heading4: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {children} - - ), - heading5: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {children} - - ), - heading6: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {children} - - ), - blockquote: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {children} - - ), - hr: (node: ASTNode, _children: ReactNode[], _parent: ASTNode[], styles: MarkdownStyles) => ( - - ), - table: (node: ASTNode, children: ReactNode[], _parent: ASTNode[], styles: MarkdownStyles) => ( - - {children} - - ), - thead: (node: ASTNode, children: ReactNode[], _parent: ASTNode[], styles: MarkdownStyles) => ( - - {children} - - ), - tbody: (node: ASTNode, children: ReactNode[], _parent: ASTNode[], styles: MarkdownStyles) => ( - - {children} - - ), - tr: (node: ASTNode, children: ReactNode[], _parent: ASTNode[], styles: MarkdownStyles) => ( - - {children} - - ), - text: ( - node: ASTNode, - _children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {node.content} - - ), - textgroup: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {children} - - ), - // strong/em/s have no custom rule in react-native-markdown-display's - // defaults beyond wrapping children in a plain RN . On iOS the - // paragraph/textgroup are native UITextViews (see markdown-text.ios.tsx), - // and a plain nested inside one is not hoisted into a - // UITextViewChild, so its content renders invisibly. Route these inline - // marks through MarkdownTextSpan (same path as text/textgroup) so the - // styled content composes and stays visible + selectable on iOS. - strong: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {children} - - ), - em: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {children} - - ), - s: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {children} - - ), - // hardbreak/softbreak fall back to react-native-markdown-display's - // default, a plain RN {"\n"}. Inside the paragraph UITextView that - // plain is not hoisted into a UITextViewChild and is dropped (same - // root cause as strong/em/s) — so on iOS a hard line break vanished, and - // a softbreak between words jammed them together ("one\ntwo" -> "onetwo"). - // Emit the break through MarkdownTextSpan so it composes on iOS. Keep - // the resolved break styles: hardbreak is a full-width flex-row child on - // Android, and dropping that width joins the surrounding text spans. - hardbreak: ( - node: ASTNode, - _children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {"\n"} - - ), - softbreak: ( - node: ASTNode, - _children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {"\n"} - - ), - code_block: ( - node: ASTNode, - _children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - ), - fence: ( - node: ASTNode, - _children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - ), - code_inline: ( - node: ASTNode, - _children: ReactNode[], - parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => { - const content = node.content ?? ""; - const isLinkedInlineCode = nodeHasParentType(parent, "link"); - const inlineCodeSource: AssistantFileLinkSource = { - href: content, - text: content, - sourceType: "inline-code", - }; - const shouldResolveInlinePath = - !isLinkedInlineCode && fileLinkActions.canResolveFile(inlineCodeSource); - - if (shouldResolveInlinePath) { - return ( - - ); - } - - const inlineCodeLinkUrl = getInlineCodeAutoLinkUrl(markdownParser, content); - if (inlineCodeLinkUrl) { - const source = getInlineCodeAutoLinkSource({ - href: inlineCodeLinkUrl, - content, - }); - return ( - - {content} - - ); - } - - return ( - - {content} - - ); - }, - bullet_list: ( - node: ASTNode, - children: ReactNode[], - parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {children} - - ), - ordered_list: ( - node: ASTNode, - children: ReactNode[], - parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {children} - - ), - list_item: ( - node: ASTNode, - children: ReactNode[], - parent: ASTNode[], - styles: MarkdownStyles, - ) => { - const { isOrdered, marker } = getMarkdownListMarker(node, parent); - const iconStyle = isOrdered ? styles.ordered_list_icon : styles.bullet_list_icon; - const contentStyle = isOrdered ? styles.ordered_list_content : styles.bullet_list_content; - - return ( - - - {marker} - - - {children} - - - ); - }, - th: (node: ASTNode, children: ReactNode[], _parent: ASTNode[], styles: MarkdownStyles) => ( - - - {children} - - - ), - td: (node: ASTNode, children: ReactNode[], _parent: ASTNode[], styles: MarkdownStyles) => ( - - - {children} - - - ), - paragraph: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {children} - - ), - link: (node: ASTNode, children: ReactNode[], _parent: ASTNode[], styles: MarkdownStyles) => ( - - {colorMarkdownLinkChildren(children, styles.link.color)} - - ), - image: ( - node: ASTNode, - _children: ReactNode[], - parent: ASTNode[], - _styles: MarkdownStyles, - ) => { - const paragraphNode = Array.isArray(parent) - ? parent.find((ancestor) => ancestor?.type === "paragraph") - : null; - const paragraphChildren = Array.isArray(paragraphNode?.children) - ? paragraphNode.children - : []; - const imageIndex = paragraphChildren.findIndex((child: ASTNode) => child?.key === node.key); - const hasLeadingContent = imageIndex > 0; - - return ( - - ); - }, - }; - }, [client, fileLinkActions, markdownParser, occurrenceKey, phase, serverId, workspaceRoot]); - - const blocks = useMemo(() => splitMarkdownBlocks(revealedMessage), [revealedMessage]); - const keyedBlocks = useMemo( - () => blocks.map((block, index) => ({ key: `block:${index}`, block })), - [blocks], - ); - const assistantContainerStyle = useMemo( () => [ assistantMessageStylesheet.container, @@ -1987,26 +1173,23 @@ export const AssistantMessage = memo(function AssistantMessage({ [occurrenceKey, revealedMessage.length], ); + const measureMessage = useStableEvent((event: import("react-native").LayoutChangeEvent) => { + setAssistantMessageHeight({ markdown: revealedMessage, ...event.nativeEvent.layout }); + }); return ( - - {keyedBlocks.map(({ key, block }, index) => ( - - - - ))} + + {fullMessageByteLength !== null ? ( ; - -function MarkdownInlineText({ - inheritedStyle, - ruleStyle, - children, -}: { - inheritedStyle: StyleProp; - ruleStyle: StyleProp; - children: ReactNode; -}) { - const style = useMemo(() => [inheritedStyle, ruleStyle], [inheritedStyle, ruleStyle]); - return {children}; -} - -function MarkdownListItemContent({ - contentStyle, - children, -}: { - contentStyle: StyleProp; - children: ReactNode; -}) { - const style = useMemo(() => [contentStyle, LIST_ITEM_CONTENT_INNER], [contentStyle]); - return {children}; -} - -function MarkdownParagraph({ - paragraphStyle, - isLastChild, - children, -}: { - paragraphStyle: StyleProp; - isLastChild: boolean; - children: ReactNode; -}) { - const style = useMemo>( - () => [paragraphStyle, isLastChild ? PARAGRAPH_LAST_CHILD : null], - [paragraphStyle, isLastChild], - ); - return {children}; -} - -function createPlanMarkdownRules() { - return { - text: ( - node: ASTNode, - _children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownRuleStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {node.content} - - ), - textgroup: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownRuleStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {children} - - ), - code_block: ( - node: ASTNode, - _children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownRuleStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {node.content} - - ), - fence: ( - node: ASTNode, - _children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownRuleStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {node.content} - - ), - code_inline: ( - node: ASTNode, - _children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownRuleStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {node.content} - - ), - bullet_list: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownRuleStyles, - ) => ( - - {children} - - ), - ordered_list: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownRuleStyles, - ) => ( - - {children} - - ), - list_item: ( - node: ASTNode, - children: ReactNode[], - parent: ASTNode[], - styles: MarkdownRuleStyles, - ) => { - const { isOrdered, marker } = getMarkdownListMarker(node, parent); - const iconStyle = isOrdered ? styles.ordered_list_icon : styles.bullet_list_icon; - const contentStyle = isOrdered ? styles.ordered_list_content : styles.bullet_list_content; - - return ( - - {marker} - {children} - - ); - }, - paragraph: ( - node: ASTNode, - children: ReactNode[], - parent: ASTNode[], - styles: MarkdownRuleStyles, - ) => { - const isLastChild = parent[0]?.children?.at(-1)?.key === node.key; - return ( - - {children} - - ); - }, - }; -} +import { ChevronRight } from "lucide-react-native"; +import { useCallback, useMemo, useState, type ReactNode } from "react"; +import { useTranslation } from "react-i18next"; +import { Pressable, Text, View } from "react-native"; +import { StyleSheet, withUnistyles } from "react-native-unistyles"; export type PlanOutcome = "pending" | "approved" | "rejected" | "canceled"; @@ -204,7 +21,6 @@ interface PlanCardProps { const ThemedChevron = withUnistyles(ChevronRight); const chevronColor = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); -const markdownRules = createPlanMarkdownRules(); export function PlanCard(props: PlanCardProps) { // A resolution starts its own presentation state; subsequent taps stay local. @@ -262,7 +78,7 @@ function PlanCardContent({ {expanded ? ( {description ? {description} : null} - + ) : null} {footer ? {footer} : null} @@ -307,6 +123,3 @@ const styles = StyleSheet.create((theme) => ({ gap: theme.spacing[2], }, })); - -const LIST_ITEM_CONTENT_INNER = { flex: 1, flexShrink: 1, minWidth: 0 }; -const PARAGRAPH_LAST_CHILD = { marginBottom: 0 }; diff --git a/packages/app/src/components/provider-icons.ts b/packages/app/src/components/provider-icons.ts index fec4003705..883b4820f7 100644 --- a/packages/app/src/components/provider-icons.ts +++ b/packages/app/src/components/provider-icons.ts @@ -3,6 +3,7 @@ import { providerSnapshotIcons } from "@/data/provider-icons"; import { Bot, PackagePlus } from "lucide-react-native"; import { createElement, useCallback, type ComponentType } from "react"; import { SvgXml } from "react-native-svg"; +import { TERMINAL_PROFILE_ICON_NAMES } from "@getpaseo/protocol/provider-icon-names"; import { ClaudeIcon } from "@/components/icons/claude-icon"; import { CodexIcon } from "@/components/icons/codex-icon"; import { CopilotIcon } from "@/components/icons/copilot-icon"; @@ -10,6 +11,7 @@ import { MiniMaxIcon } from "@/components/icons/minimax-icon"; import { OpenCodeIcon } from "@/components/icons/opencode-icon"; import { OmpIcon } from "@/components/icons/omp-icon"; import { PiIcon } from "@/components/icons/pi-icon"; +import { ACP_PROVIDER_ICON_SVGS } from "@/assets/acp-provider-icons"; import { ACP_PROVIDER_CATALOG } from "@/data/acp-provider-catalog"; import { resolveProviderIconName } from "@/components/provider-icon-name"; @@ -31,10 +33,17 @@ const BUILTIN_PROVIDER_ICONS: Record = { pi: PiIcon as unknown as ProviderIconComponent, }; -const CATALOG_ICON_SVGS = new Map( +const CATALOG_ICON_SVGS = new Map( ACP_PROVIDER_CATALOG.flatMap((entry) => (entry.iconSvg ? [[entry.id, entry.iconSvg]] : [])), ); +for (const name of TERMINAL_PROFILE_ICON_NAMES) { + const iconSvg = (ACP_PROVIDER_ICON_SVGS as Record)[name]; + if (iconSvg) { + CATALOG_ICON_SVGS.set(name, iconSvg); + } +} + const catalogIconComponents = new Map(); const snapshotIconComponents = new Map(); diff --git a/packages/app/src/components/question-form-card.browser.test.tsx b/packages/app/src/components/question-form-card.browser.test.tsx index 630b001aa6..83120931d1 100644 --- a/packages/app/src/components/question-form-card.browser.test.tsx +++ b/packages/app/src/components/question-form-card.browser.test.tsx @@ -58,6 +58,7 @@ function mountCard(question: Record) { permission={buildPermission(question)} onRespond={onRespond} isResponding={false} + responseError={null} />, ), ); diff --git a/packages/app/src/components/question-form-card.tsx b/packages/app/src/components/question-form-card.tsx index 655e01d210..7014de89d1 100644 --- a/packages/app/src/components/question-form-card.tsx +++ b/packages/app/src/components/question-form-card.tsx @@ -1,3 +1,4 @@ +import { Alert } from "@/components/ui/alert"; import { LoadingSpinner } from "@/components/ui/loading-spinner"; import { useState, useCallback, useMemo, useRef, type RefObject } from "react"; import { View, Text, Pressable, type PressableStateCallbackType } from "react-native"; @@ -27,6 +28,7 @@ interface QuestionFormCardProps { permission: PendingPermission; onRespond: (response: AgentPermissionResponse) => void; isResponding: boolean; + responseError: string | null; } const IS_WEB = isWeb; @@ -339,7 +341,12 @@ function QuestionOtherInput({ ); } -export function QuestionFormCard({ permission, onRespond, isResponding }: QuestionFormCardProps) { +export function QuestionFormCard({ + permission, + onRespond, + isResponding, + responseError, +}: QuestionFormCardProps) { const { theme } = useUnistyles(); const { t } = useTranslation(); const isMobile = useIsCompactFormFactor(); @@ -438,7 +445,7 @@ export function QuestionFormCard({ permission, onRespond, isResponding }: Questi ]); const handleDeny = useCallback(() => { - if (!questions) return; + if (!questions || isResponding) return; setRespondingAction("dismiss"); if (shouldSubmitEmptyOnDismiss(questions)) { onRespond({ @@ -454,7 +461,7 @@ export function QuestionFormCard({ permission, onRespond, isResponding }: Questi behavior: "deny", message: "Dismissed by user", }); - }, [questions, onRespond, otherTexts, permission.request.input, selections]); + }, [questions, isResponding, onRespond, otherTexts, permission.request.input, selections]); const handleSelectQuestion = useCallback((index: number) => { setActiveQuestionIndex(index); @@ -487,6 +494,7 @@ export function QuestionFormCard({ permission, onRespond, isResponding }: Questi [theme.colors.surface2, theme.colors.surface1, theme.colors.borderAccent], ); + const pendingAction = isResponding ? respondingAction : null; const primaryDisabled = isResponding || (isLastQuestion ? !allAnswered : !activeQuestionAnswered); const primaryActionLabel = isLastQuestion ? t("message.question.submit") @@ -550,7 +558,7 @@ export function QuestionFormCard({ permission, onRespond, isResponding }: Questi const dismissLabel = resolveDismissLabel(questions, t("common.actions.dismiss")); const selected = selections[resolvedActiveQuestionIndex] ?? new Set(); const otherText = otherTexts[resolvedActiveQuestionIndex] ?? ""; - const showTextInput = activeQuestion ? questionShowsTextInput(activeQuestion) : false; + const showTextInput = questionShowsTextInput(questions[resolvedActiveQuestionIndex]); return ( @@ -604,6 +612,8 @@ export function QuestionFormCard({ permission, onRespond, isResponding }: Questi ) : null} + {responseError ? : null} + - {respondingAction === "dismiss" ? ( + {pendingAction === "dismiss" ? ( ) : ( @@ -631,7 +641,7 @@ export function QuestionFormCard({ permission, onRespond, isResponding }: Questi accessibilityLabel={primaryActionLabel} testID="question-form-primary-action" > - {respondingAction === "submit" ? ( + {pendingAction === "submit" ? ( ) : ( diff --git a/packages/app/src/components/sidebar/display-preferences/menu.tsx b/packages/app/src/components/sidebar/display-preferences/menu.tsx index d62b3f9ff1..be5a66ff6f 100644 --- a/packages/app/src/components/sidebar/display-preferences/menu.tsx +++ b/packages/app/src/components/sidebar/display-preferences/menu.tsx @@ -301,13 +301,6 @@ export function SidebarDisplayPreferencesMenu(): ReactElement { sheetTitle={t("sidebar.display.heading")} testID="sidebar-display-preferences-content" > - - {t("sidebar.display.showBackground")} - ) : null} + + + {t("sidebar.display.showBackground")} + diff --git a/packages/app/src/components/sidebar/sidebar-footer-icon-highlight.tsx b/packages/app/src/components/sidebar/sidebar-footer-icon-highlight.tsx new file mode 100644 index 0000000000..7b901693fa --- /dev/null +++ b/packages/app/src/components/sidebar/sidebar-footer-icon-highlight.tsx @@ -0,0 +1,25 @@ +import { View } from "react-native"; +import { StyleSheet } from "react-native-unistyles"; + +/** + * The round highlight behind a sidebar footer icon. It is a layer inside the button rather than a + * radius on it: web hit-testing follows `border-radius`, so a round button would stop taking taps + * near the corners of its square finger-sized target. + */ +export function SidebarFooterIconHighlight({ visible }: { visible: boolean }) { + return ; +} + +const styles = StyleSheet.create((theme) => ({ + highlight: { + position: "absolute", + top: 0, + right: 0, + bottom: 0, + left: 0, + borderRadius: theme.borderRadius.full, + }, + visible: { + backgroundColor: theme.colors.interactionHighlight, + }, +})); diff --git a/packages/app/src/components/sidebar/sidebar-header-row.tsx b/packages/app/src/components/sidebar/sidebar-header-row.tsx index 2eeded947d..f38d27c65c 100644 --- a/packages/app/src/components/sidebar/sidebar-header-row.tsx +++ b/packages/app/src/components/sidebar/sidebar-header-row.tsx @@ -54,6 +54,10 @@ export function SidebarHeaderRow({ const [isHovered, setIsHovered] = useState(false); const handlePointerEnter = useCallback(() => setIsHovered(true), []); const handlePointerLeave = useCallback(() => setIsHovered(false), []); + // The button and its trailing slot are siblings, so the row owns the pressed fill for both. + const [isPressed, setIsPressed] = useState(false); + const handlePressIn = useCallback(() => setIsPressed(true), []); + const handlePressOut = useCallback(() => setIsPressed(false), []); const isHighlighted = isHovered || isActive; const iconSize = variant === "header" ? "md" : "sm"; @@ -65,12 +69,14 @@ export function SidebarHeaderRow({ return ( {label} {right === null ? null : ( - + {right} )} @@ -136,6 +149,10 @@ const styles = StyleSheet.create((theme) => ({ rowHighlighted: { backgroundColor: theme.colors.surfaceSidebarHover, }, + // The workspace rows' pressed fill. + rowPressed: { + backgroundColor: theme.colors.surface2, + }, button: { flex: 1, minWidth: 0, diff --git a/packages/app/src/components/sidebar/sidebar-help-menu.tsx b/packages/app/src/components/sidebar/sidebar-help-menu.tsx index 97ff3d2772..0eefa77468 100644 --- a/packages/app/src/components/sidebar/sidebar-help-menu.tsx +++ b/packages/app/src/components/sidebar/sidebar-help-menu.tsx @@ -15,6 +15,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { DensityIcon } from "@/components/ui/density-icon"; +import { SidebarFooterIconHighlight } from "./sidebar-footer-icon-highlight"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { useAppDiagnosticStore } from "@/diagnostics/store"; import { useKeyboardShortcutsAvailable } from "@/keyboard/availability"; @@ -108,12 +109,15 @@ export function SidebarHelpMenu() { accessibilityRole="button" accessibilityLabel={t("sidebar.help.trigger")} > - {({ hovered }) => ( - + {({ hovered, pressed, open: menuOpen }) => ( + <> + + + )} diff --git a/packages/app/src/components/ui/tooltip.tsx b/packages/app/src/components/ui/tooltip.tsx index 22796e7ffe..72803f06d0 100644 --- a/packages/app/src/components/ui/tooltip.tsx +++ b/packages/app/src/components/ui/tooltip.tsx @@ -41,10 +41,12 @@ interface Rect { height: number; } +type TooltipTriggerElement = View | HTMLElement; + interface TooltipContextValue { open: boolean; setOpen: (open: boolean) => void; - triggerRef: React.RefObject; + triggerRef: React.RefObject; enabled: boolean; openOnPress: boolean; delayDuration: number; @@ -120,12 +122,19 @@ function useControllableOpenState({ return [value, setValue]; } -function measureElement(element: View): Promise { - return new Promise((resolve) => { - element.measureInWindow((x, y, width, height) => { - resolve({ x, y, width, height }); +function measureElement(element: TooltipTriggerElement): Promise { + if (isWeb && element instanceof HTMLElement) { + const { x, y, width, height } = element.getBoundingClientRect(); + return Promise.resolve({ x, y, width, height }); + } + if ("measureInWindow" in element) { + return new Promise((resolve) => { + element.measureInWindow((x, y, width, height) => { + resolve({ x, y, width, height }); + }); }); - }); + } + throw new Error("A DOM tooltip trigger requires the web platform"); } function resolveActualSide(args: { @@ -240,7 +249,7 @@ export function Tooltip({ enabledOnDesktop?: boolean; enabledOnMobile?: boolean; }>): ReactElement { - const triggerRef = useRef(null); + const triggerRef = useRef(null); const [isOpen, setIsOpen] = useControllableOpenState({ open, defaultOpen, @@ -282,6 +291,12 @@ export function TooltipTrigger({ triggerRefProp?: string; }): ReactElement { const ctx = useTooltipContext("TooltipTrigger"); + const setTriggerRef = useCallback( + (node: View | null) => { + ctx.triggerRef.current = node; + }, + [ctx.triggerRef], + ); const openTimerRef = useRef | null>(null); const clearOpenTimer = useCallback(() => { @@ -411,7 +426,7 @@ export function TooltipTrigger({ }; const existingRefProp = Reflect.get(rawProps, triggerRefProp); - mergedProps[triggerRefProp] = (node: View) => { + mergedProps[triggerRefProp] = (node: TooltipTriggerElement) => { if (isCallable(existingRefProp)) { existingRefProp(node); } else if (existingRefProp && typeof existingRefProp === "object") { @@ -424,7 +439,7 @@ export function TooltipTrigger({ } return ( - + {children} ); diff --git a/packages/app/src/file-explorer/tree.ts b/packages/app/src/file-explorer/tree.ts index 472c3d7075..8b75ae7b25 100644 --- a/packages/app/src/file-explorer/tree.ts +++ b/packages/app/src/file-explorer/tree.ts @@ -1,6 +1,6 @@ import type { ExplorerDirectory, ExplorerEntry } from "@/stores/session-store"; import type { SortOption } from "@/stores/panel-store/state"; -import { filterVisibleExplorerEntries } from "./visibility"; +import { filterVisibleExplorerEntries, isHiddenExplorerPath } from "./visibility"; export const MAX_AUTO_EXPANDED_DIRECTORY_DEPTH = 5; @@ -201,3 +201,24 @@ function sortExplorerEntries(entries: ExplorerEntry[], sortOption: SortOption): }); return sorted; } + +/** Load and expand the path to a directory selected outside the tree. */ +export async function revealExplorerDirectory(input: { + directoryPath: string; + requestDirectoryListing: (path: string) => Promise; + expandPaths: (paths: string[]) => void; + showHiddenFiles: () => void; +}): Promise { + if (isHiddenExplorerPath(input.directoryPath)) input.showHiddenFiles(); + const paths = ["."]; + if (input.directoryPath !== ".") { + const segments = input.directoryPath.split("/").filter(Boolean); + for (let length = 1; length <= segments.length; length += 1) { + paths.push(segments.slice(0, length).join("/")); + } + } + for (const path of paths) { + if (!(await input.requestDirectoryListing(path))) return; + } + input.expandPaths(paths); +} diff --git a/packages/app/src/file-pane/markdown-preview/index.tsx b/packages/app/src/file-pane/markdown-preview/index.tsx index a79705323f..8a45b5c296 100644 --- a/packages/app/src/file-pane/markdown-preview/index.tsx +++ b/packages/app/src/file-pane/markdown-preview/index.tsx @@ -1,16 +1,9 @@ -import { useMemo } from "react"; +import { MarkdownRenderer } from "@/components/markdown/renderer"; import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; -import type { ASTNode, RenderRules } from "react-native-markdown-display"; +import { useMemo } from "react"; import { Text, View } from "react-native"; import { StyleSheet } from "react-native-unistyles"; -import { - createSharedMarkdownRules, - MarkdownRenderer, - type MarkdownStyles, -} from "@/components/markdown/renderer"; import { parseMarkdownPreviewDocument } from "./document"; -import { resolveMarkdownPreviewImageSource } from "./image-source"; -import { FileMarkdownPreviewImage } from "./preview-image"; export function FileMarkdownPreview({ source, @@ -26,34 +19,6 @@ export function FileMarkdownPreview({ serverId?: string; }) { const document = useMemo(() => parseMarkdownPreviewDocument(source), [source]); - const rules = useMemo( - () => ({ - ...createSharedMarkdownRules(), - image: (node: ASTNode, _children, _parent: ASTNode[], _styles: MarkdownStyles) => { - const resolved = resolveMarkdownPreviewImageSource({ - source: String(node.attributes?.src ?? ""), - markdownPath: filePath, - workspaceRoot, - }); - if (!resolved) { - return null; - } - - return ( - - ); - }, - }), - [client, filePath, serverId, workspaceRoot], - ); return ( @@ -79,7 +44,13 @@ export function FileMarkdownPreview({ ))} ) : null} - + ); diff --git a/packages/app/src/file-pane/markdown-preview/preview-image.tsx b/packages/app/src/file-pane/markdown-preview/preview-image.tsx deleted file mode 100644 index ef27c14978..0000000000 --- a/packages/app/src/file-pane/markdown-preview/preview-image.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import { useCallback, useMemo, useState } from "react"; -import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; -import { useTranslation } from "react-i18next"; -import { Image, Pressable, Text, View, type StyleProp, type ViewStyle } from "react-native"; -import { StyleSheet, withUnistyles } from "react-native-unistyles"; -import { useAssistantImage } from "@/assistant-image/use-assistant-image"; -import { AttachmentLightbox, type ImageLightboxSource } from "@/components/attachment-lightbox"; -import { LoadingSpinner } from "@/components/ui/loading-spinner"; -import type { Theme } from "@/styles/theme"; - -const ThemedLoadingSpinner = withUnistyles(LoadingSpinner); -const foregroundMutedColorMapping = (theme: Theme) => ({ - color: theme.colors.foregroundMuted, -}); - -const PREVIEW_IMAGE_MIN_HEIGHT = 160; - -export function FileMarkdownPreviewImage({ - source, - occurrenceKey, - alt, - client, - workspaceRoot, - serverId, -}: { - source: string; - occurrenceKey: string; - alt?: string; - client?: DaemonClient | null; - workspaceRoot?: string; - serverId?: string; -}) { - const { t } = useTranslation(); - const [viewerOpen, setViewerOpen] = useState(false); - const openViewer = useCallback(() => setViewerOpen(true), []); - const closeViewer = useCallback(() => setViewerOpen(false), []); - const image = useAssistantImage({ - source, - occurrenceKey, - client, - workspaceRoot, - serverId, - }); - const binding = image.status === "failed" ? null : image.binding; - const aspectRatio = image.status === "failed" ? null : image.aspectRatio; - const imageUri = binding?.uri ?? ""; - const imageSource = useMemo(() => ({ uri: imageUri }), [imageUri]); - const imageSizeStyle = useMemo(() => { - if (aspectRatio) { - return { aspectRatio }; - } - return { height: PREVIEW_IMAGE_MIN_HEIGHT }; - }, [aspectRatio]); - const surfaceStyle = useMemo>( - () => [styles.surface, imageSizeStyle], - [imageSizeStyle], - ); - const lightboxSource = useMemo(() => { - if (!viewerOpen || !imageUri) return null; - return { - type: "uri", - uri: imageUri, - contentSize: aspectRatio ? { width: aspectRatio, height: 1 } : undefined, - }; - }, [aspectRatio, imageUri, viewerOpen]); - - if (image.status === "failed") { - return ( - - {image.message} - - ); - } - - if (!binding) { - return ( - - - - ); - } - - return ( - - - - - {image.status === "loading" ? ( - - - - ) : null} - - - - - ); -} - -const styles = StyleSheet.create((theme) => ({ - frame: { - width: "100%", - marginBottom: theme.spacing[4], - }, - surface: { - width: "100%", - overflow: "hidden", - borderRadius: theme.borderRadius.md, - backgroundColor: theme.colors.surface2, - }, - image: { - width: "100%", - height: "100%", - }, - state: { - minHeight: PREVIEW_IMAGE_MIN_HEIGHT, - alignItems: "center", - justifyContent: "center", - paddingHorizontal: theme.spacing[4], - paddingVertical: theme.spacing[6], - backgroundColor: theme.colors.surface2, - borderRadius: theme.borderRadius.md, - }, - loadingOverlay: { - position: "absolute", - top: 0, - right: 0, - bottom: 0, - left: 0, - alignItems: "center", - justifyContent: "center", - }, - errorText: { - color: theme.colors.foregroundMuted, - fontSize: theme.fontSize.base, - textAlign: "center", - }, -})); diff --git a/packages/app/src/hooks/sidebar-workspaces-view-model.test.ts b/packages/app/src/hooks/sidebar-workspaces-view-model.test.ts index 637d3a035b..1bc4cf2135 100644 --- a/packages/app/src/hooks/sidebar-workspaces-view-model.test.ts +++ b/packages/app/src/hooks/sidebar-workspaces-view-model.test.ts @@ -403,6 +403,57 @@ describe("shared sidebar workspace model", () => { expect(model.projectNamesByViewKey).toEqual(new Map([["getpaseo/paseo", "getpaseo/paseo"]])); }); + it("preserves the collection when a retained sidebar reconnects to unchanged entries", () => { + const model = buildSidebarWorkspacePlacementModel({ + projects: [project({ projectKey: "project", workspaceKeys: ["srv:one", "srv:two"] })], + }); + const one = workspace({ + id: "one", + name: "one", + projectId: "project", + projectDisplayName: "project", + }); + const two = workspace({ + id: "two", + name: "two", + projectId: "project", + projectDisplayName: "project", + }); + const sessions = [ + { + serverId: "srv", + workspaceAgentActivity: new Map(), + workspaces: new Map([ + ["one", one], + ["two", two], + ]), + }, + ]; + const previousEntries = buildSidebarWorkspaceEntries({ + placements: model.workspaces, + sessions, + }); + expect( + buildSidebarWorkspaceEntries({ + placements: model.workspaces, + sessions, + previousEntries, + }), + ).toBe(previousEntries); + const reordered = buildSidebarWorkspaceEntries({ + placements: model.workspaces.toReversed(), + sessions, + previousEntries, + }); + expect([...reordered.keys()]).toEqual(["srv:two", "srv:one"]); + const removed = buildSidebarWorkspaceEntries({ + placements: model.workspaces.slice(0, 1), + sessions, + previousEntries, + }); + expect([...removed.keys()]).toEqual(["srv:one"]); + }); + it("preserves unchanged row identities when another workspace updates", () => { const model = buildSidebarWorkspacePlacementModel({ projects: [project({ projectKey: "project", workspaceKeys: ["srv:one", "srv:two"] })], diff --git a/packages/app/src/hooks/sidebar-workspaces-view-model.ts b/packages/app/src/hooks/sidebar-workspaces-view-model.ts index 24ecc34461..e004c3fb89 100644 --- a/packages/app/src/hooks/sidebar-workspaces-view-model.ts +++ b/packages/app/src/hooks/sidebar-workspaces-view-model.ts @@ -369,13 +369,15 @@ export function buildSidebarWorkspaceEntries(input: { sessions: SidebarWorkspaceSession[]; pendingCreateAttempts?: Record; previousEntries?: ReadonlyMap; -}): Map { +}): ReadonlyMap { if (input.placements.length === 0 || input.sessions.length === 0) { return new Map(); } const sessionByServerId = new Map(input.sessions.map((session) => [session.serverId, session])); const entries = new Map(); + const previousKeys = input.previousEntries?.keys(); + let unchanged = input.previousEntries !== undefined; for (const placement of input.placements) { const session = sessionByServerId.get(placement.serverId); @@ -395,15 +397,22 @@ export function buildSidebarWorkspaceEntries(input: { workspaceAgentActivity: session.workspaceAgentActivity, }); const previousEntry = input.previousEntries?.get(placement.workspaceKey); - entries.set( - placement.workspaceKey, + const nextEntry = previousEntry && areSidebarWorkspaceEntriesEqual(previousEntry, entry) ? previousEntry - : entry, - ); + : entry; + entries.set(placement.workspaceKey, nextEntry); + unchanged = + previousKeys?.next().value === placement.workspaceKey && + previousEntry === nextEntry && + unchanged; } - return entries; + // Resuming a retained sidebar must not invalidate its project/filter projections + // when the directory has not changed while it was hidden. + return unchanged && input.previousEntries?.size === entries.size + ? input.previousEntries + : entries; } function areSidebarWorkspaceEntriesEqual( diff --git a/packages/app/src/hooks/use-dictation-audio-source.native.ts b/packages/app/src/hooks/use-dictation-audio-source.native.ts index 293412fe1a..c34f9bbd02 100644 --- a/packages/app/src/hooks/use-dictation-audio-source.native.ts +++ b/packages/app/src/hooks/use-dictation-audio-source.native.ts @@ -14,6 +14,7 @@ export function useDictationAudioSource(config: DictationAudioSourceConfig): Dic const onErrorRef = useRef(config.onError); const onInterruptionRef = useRef(config.onInterruption); const [volume, setVolume] = useState(0); + const [isPaused, setIsPaused] = useState(false); const engineRef = useRef | null>(null); const getOrCreateEngine = useCallback(() => { @@ -21,20 +22,24 @@ export function useDictationAudioSource(config: DictationAudioSourceConfig): Dic return engineRef.current; } - engineRef.current = createAudioEngine({ - onCaptureData: (pcm) => { - onPcmSegmentRef.current(Buffer.from(pcm).toString("base64")); + engineRef.current = createAudioEngine( + { + onCaptureData: (pcm) => { + onPcmSegmentRef.current(Buffer.from(pcm).toString("base64")); + }, + onVolumeLevel: (level) => { + setVolume(level); + }, + onError: (error) => { + onErrorRef.current?.(error); + }, + onInterruption: () => { + onInterruptionRef.current?.(); + }, + onCaptureAvailabilityChange: (available) => setIsPaused(!available), }, - onVolumeLevel: (level) => { - setVolume(level); - }, - onError: (error) => { - onErrorRef.current?.(error); - }, - onInterruption: () => { - onInterruptionRef.current?.(); - }, - }); + { captureMode: "dictation" }, + ); return engineRef.current; }, []); @@ -67,5 +72,6 @@ export function useDictationAudioSource(config: DictationAudioSourceConfig): Dic start, stop, volume, + isPaused, }; } diff --git a/packages/app/src/hooks/use-dictation-audio-source.types.ts b/packages/app/src/hooks/use-dictation-audio-source.types.ts index 711cf690c8..8fe82ef63e 100644 --- a/packages/app/src/hooks/use-dictation-audio-source.types.ts +++ b/packages/app/src/hooks/use-dictation-audio-source.types.ts @@ -8,4 +8,5 @@ export interface DictationAudioSource { start: () => Promise; stop: () => Promise; volume: number; + isPaused: boolean; } diff --git a/packages/app/src/hooks/use-dictation-audio-source.web.ts b/packages/app/src/hooks/use-dictation-audio-source.web.ts index c482bece18..6b71ca7181 100644 --- a/packages/app/src/hooks/use-dictation-audio-source.web.ts +++ b/packages/app/src/hooks/use-dictation-audio-source.web.ts @@ -442,6 +442,7 @@ export function useDictationAudioSource(config: DictationAudioSourceConfig): Dic }, stop, volume, + isPaused: false, }), [start, stop, volume], ); diff --git a/packages/app/src/hooks/use-dictation.shared.ts b/packages/app/src/hooks/use-dictation.shared.ts index 275c9d93d7..fee3da46c1 100644 --- a/packages/app/src/hooks/use-dictation.shared.ts +++ b/packages/app/src/hooks/use-dictation.shared.ts @@ -1,6 +1,6 @@ import { i18n } from "@/i18n/i18next"; -export type DictationStatus = "idle" | "recording" | "uploading" | "failed"; +export type DictationStatus = "idle" | "recording" | "paused" | "uploading" | "failed"; export interface UseDictationOptions { client: import("@getpaseo/client/internal/daemon-client").DaemonClient | null; diff --git a/packages/app/src/hooks/use-dictation.ts b/packages/app/src/hooks/use-dictation.ts index dde72964bc..a2a3f46bcc 100644 --- a/packages/app/src/hooks/use-dictation.ts +++ b/packages/app/src/hooks/use-dictation.ts @@ -461,7 +461,7 @@ export function useDictation(options: UseDictationOptions): UseDictationResult { volume: audio.volume, duration, error, - status, + status: status === "recording" && audio.isPaused ? "paused" : status, startDictation, cancelDictation, confirmDictation, diff --git a/packages/app/src/hooks/use-file-explorer-actions.ts b/packages/app/src/hooks/use-file-explorer-actions.ts index c29feb5241..ca3c99649b 100644 --- a/packages/app/src/hooks/use-file-explorer-actions.ts +++ b/packages/app/src/hooks/use-file-explorer-actions.ts @@ -6,6 +6,8 @@ import { type ExplorerDirectory, } from "@/stores/session-store"; import { explorerFileFromReadResult } from "@/file-explorer/read-result"; +import { usePanelStore } from "@/stores/panel-store"; +import { revealExplorerDirectory } from "@/file-explorer/tree"; import { parentExplorerPath } from "@/utils/explorer-paths"; function createExplorerState(): AgentFileExplorerState { @@ -172,6 +174,27 @@ export function useFileExplorerActions(params: { serverId: string } & FileExplor [client, normalizedWorkspaceRoot, t, updateExplorerState, workspaceStateKey], ); + const revealDirectory = useCallback( + async (directoryPath: string) => { + if (!workspaceStateKey) return; + await revealExplorerDirectory({ + directoryPath, + requestDirectoryListing, + showHiddenFiles: () => { + const panel = usePanelStore.getState(); + if (!panel.explorerShowHiddenFiles) panel.toggleExplorerShowHiddenFiles(); + }, + expandPaths: (paths) => + usePanelStore + .getState() + .setExpandedPathsForWorkspace(workspaceStateKey, (current) => + Array.from(new Set([...current, ...paths])), + ), + }); + }, + [requestDirectoryListing, workspaceStateKey], + ); + const requestFilePreview = useCallback( async (path: string) => { if (!workspaceStateKey) { @@ -354,6 +377,7 @@ export function useFileExplorerActions(params: { serverId: string } & FileExplor return { workspaceStateKey, + revealDirectory, requestDirectoryListing, requestFilePreview, requestFileDownloadToken, diff --git a/packages/app/src/hooks/use-revealed-text.ts b/packages/app/src/hooks/use-revealed-text.ts index d3e1c905ab..6d7130927d 100644 --- a/packages/app/src/hooks/use-revealed-text.ts +++ b/packages/app/src/hooks/use-revealed-text.ts @@ -11,7 +11,7 @@ import { type TextRevealState, visibleRevealedText, } from "@/agent-stream/text-reveal"; -import type { MarkdownPhase } from "@/components/markdown/fence/types"; +export type MarkdownPhase = "streaming" | "complete"; /** * Binds the paced reveal in @/agent-stream/text-reveal to a frame clock. diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts index 5e09ac5957..2f1fd7397f 100644 --- a/packages/app/src/i18n/resources/ar.ts +++ b/packages/app/src/i18n/resources/ar.ts @@ -360,6 +360,7 @@ export const ar: TranslationResources = { details: "تفاصيل", }, dictation: { + paused: "تمت مقاطعة الميكروفون. في انتظار الاستئناف…", start: "بدء الإملاء الصوتي", cancel: "إلغاء الإملاء", retry: "أعد محاولة الإملاء", @@ -369,6 +370,7 @@ export const ar: TranslationResources = { failedRetry: "فشل الإملاء. اضغط على إعادة المحاولة.", }, question: { + responseFailed: "تعذر إرسال ردك. حاول مرة أخرى.", submit: "يُقدِّم", next: "التالي", answerPlaceholder: "اكتب إجابتك...", diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts index 8c6a870580..14cde732e7 100644 --- a/packages/app/src/i18n/resources/en.ts +++ b/packages/app/src/i18n/resources/en.ts @@ -357,6 +357,7 @@ export const en = { details: "Details", }, dictation: { + paused: "Microphone interrupted. Waiting to resume…", start: "Start voice dictation", cancel: "Cancel dictation", retry: "Retry dictation", @@ -366,6 +367,7 @@ export const en = { failedRetry: "Dictation failed. Tap retry.", }, question: { + responseFailed: "Could not send your response. Try again.", submit: "Submit", next: "Next", answerPlaceholder: "Type your answer...", diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts index 62667ba9f9..18eb17f291 100644 --- a/packages/app/src/i18n/resources/es.ts +++ b/packages/app/src/i18n/resources/es.ts @@ -365,6 +365,7 @@ export const es: TranslationResources = { details: "Detalles", }, dictation: { + paused: "Micrófono interrumpido. Esperando para reanudar…", start: "Iniciar dictado de voz", cancel: "Cancelar dictado", retry: "Reintentar el dictado", @@ -374,6 +375,7 @@ export const es: TranslationResources = { failedRetry: "El dictado falló. Toca reintentar.", }, question: { + responseFailed: "No se pudo enviar tu respuesta. Inténtalo de nuevo.", submit: "Entregar", next: "Siguiente", answerPlaceholder: "Escribe tu respuesta...", diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts index 8e70ece6da..2bae7b8647 100644 --- a/packages/app/src/i18n/resources/fr.ts +++ b/packages/app/src/i18n/resources/fr.ts @@ -365,6 +365,7 @@ export const fr: TranslationResources = { details: "Détails", }, dictation: { + paused: "Microphone interrompu. En attente de reprise…", start: "Démarrer la dictée vocale", cancel: "Annuler la dictée", retry: "Relancer la dictée", @@ -374,6 +375,7 @@ export const fr: TranslationResources = { failedRetry: "La dictée a échoué. Appuyez sur Réessayer.", }, question: { + responseFailed: "Impossible d’envoyer votre réponse. Réessayez.", submit: "Envoyer", next: "Suivant", answerPlaceholder: "Saisissez votre réponse…", diff --git a/packages/app/src/i18n/resources/ja.ts b/packages/app/src/i18n/resources/ja.ts index d7b08a9c84..58e2706b95 100644 --- a/packages/app/src/i18n/resources/ja.ts +++ b/packages/app/src/i18n/resources/ja.ts @@ -365,6 +365,7 @@ export const ja: TranslationResources = { details: "詳細", }, dictation: { + paused: "マイクが中断されました。再開を待っています…", start: "音声入力を開始", cancel: "音声入力をキャンセル", retry: "音声入力を再試行", @@ -374,6 +375,7 @@ export const ja: TranslationResources = { failedRetry: "音声入力に失敗しました。再試行をタップしてください。", }, question: { + responseFailed: "回答を送信できませんでした。もう一度お試しください。", submit: "送信", next: "次へ", answerPlaceholder: "回答を入力...", diff --git a/packages/app/src/i18n/resources/ko.ts b/packages/app/src/i18n/resources/ko.ts index c82240f0c4..fa962a5562 100644 --- a/packages/app/src/i18n/resources/ko.ts +++ b/packages/app/src/i18n/resources/ko.ts @@ -361,6 +361,7 @@ export const ko: TranslationResources = { details: "세부 정보", }, dictation: { + paused: "마이크가 중단되었습니다. 재개를 기다리는 중…", start: "음성 받아쓰기 시작", cancel: "받아쓰기 취소", retry: "받아쓰기 다시 시도", @@ -370,6 +371,7 @@ export const ko: TranslationResources = { failedRetry: "받아쓰기에 실패했습니다. 다시 시도하려면 누르세요.", }, question: { + responseFailed: "응답을 보내지 못했습니다. 다시 시도해 주세요.", submit: "제출", next: "다음", answerPlaceholder: "답변을 입력하세요...", diff --git a/packages/app/src/i18n/resources/pt-BR.ts b/packages/app/src/i18n/resources/pt-BR.ts index 622c13e798..c5c6607ca6 100644 --- a/packages/app/src/i18n/resources/pt-BR.ts +++ b/packages/app/src/i18n/resources/pt-BR.ts @@ -364,6 +364,7 @@ export const ptBR: TranslationResources = { details: "Detalhes", }, dictation: { + paused: "Microfone interrompido. Aguardando para retomar…", start: "Iniciar ditado por voz", cancel: "Cancelar ditado", retry: "Tentar ditado novamente", @@ -373,6 +374,7 @@ export const ptBR: TranslationResources = { failedRetry: "Falha no ditado. Toque para tentar novamente.", }, question: { + responseFailed: "Não foi possível enviar sua resposta. Tente novamente.", submit: "Enviar", next: "Próximo", answerPlaceholder: "Digite sua resposta...", diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts index 733596de39..4df3f721bb 100644 --- a/packages/app/src/i18n/resources/ru.ts +++ b/packages/app/src/i18n/resources/ru.ts @@ -364,6 +364,7 @@ export const ru: TranslationResources = { details: "Подробности", }, dictation: { + paused: "Микрофон прерван. Ожидание возобновления…", start: "Начать диктовку голосом", cancel: "Отменить диктовку", retry: "Повторить диктовку", @@ -373,6 +374,7 @@ export const ru: TranslationResources = { failedRetry: "Диктовка не удалась. Нажмите «Повторить».", }, question: { + responseFailed: "Не удалось отправить ответ. Попробуйте ещё раз.", submit: "Отправить", next: "Далее", answerPlaceholder: "Введите ответ...", diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts index c8627ca769..bad9cc4690 100644 --- a/packages/app/src/i18n/resources/zh-CN.ts +++ b/packages/app/src/i18n/resources/zh-CN.ts @@ -360,6 +360,7 @@ export const zhCN: TranslationResources = { details: "详情", }, dictation: { + paused: "麦克风已中断,正在等待恢复…", start: "开始语音听写", cancel: "取消听写", retry: "重试听写", @@ -369,6 +370,7 @@ export const zhCN: TranslationResources = { failedRetry: "听写失败。点按重试。", }, question: { + responseFailed: "未能发送你的回复,请重试。", submit: "提交", next: "下一步", answerPlaceholder: "输入你的回答...", diff --git a/packages/app/src/keyboard/shift/internal/translate.ios.tsx b/packages/app/src/keyboard/shift/internal/translate.ios.tsx index 348fe828d0..1fea4fab0c 100644 --- a/packages/app/src/keyboard/shift/internal/translate.ios.tsx +++ b/packages/app/src/keyboard/shift/internal/translate.ios.tsx @@ -1,5 +1,5 @@ -import { useMemo, type ReactNode } from "react"; -import { Animated, type ViewProps } from "react-native"; +import { useMemo, useEffect, type ReactNode } from "react"; +import { Animated, useAnimatedValue, type ViewProps } from "react-native"; import { useKeyboardAnimation } from "react-native-keyboard-controller"; import { useSafeAreaInsets } from "react-native-safe-area-context"; @@ -32,9 +32,23 @@ export function KeyboardTranslateView({ }), [bottomClearance, height, insets.bottom, progress], ); + // Fullscreen resizes this view while disabling translation. iOS Fabric can + // reapply its previous transform during that layout commit. Keep the graph + // attached and apply the mode on the native animation frame; a static zero or + // setValue during the React commit gets overwritten by the resize. + const enabledValue = useAnimatedValue(enabled ? 1 : 0); + useEffect(() => { + const animation = Animated.timing(enabledValue, { + toValue: enabled ? 1 : 0, + duration: 0, + useNativeDriver: true, + }); + animation.start(); + return () => animation.stop(); + }, [enabled, enabledValue]); const keyboardStyle = useMemo( - () => ({ transform: [{ translateY: enabled ? translateY : 0 }] }), - [enabled, translateY], + () => ({ transform: [{ translateY: Animated.multiply(translateY, enabledValue) }] }), + [enabledValue, translateY], ); return ( diff --git a/packages/app/src/review/feedback/index.test.ts b/packages/app/src/review/feedback/index.test.ts index b423855b75..6ef6d74287 100644 --- a/packages/app/src/review/feedback/index.test.ts +++ b/packages/app/src/review/feedback/index.test.ts @@ -172,8 +172,7 @@ describe("review feedback", () => { makeComment("added"), ]); expect(feedback.model.getState().phase).toEqual({ - kind: "sent", - recipient: makeRecipient("one"), + kind: "idle", }); }); it("clears unchanged sent comments while preserving additions", async () => { diff --git a/packages/app/src/review/feedback/index.ts b/packages/app/src/review/feedback/index.ts index 69e0592fe8..3aaba5c97d 100644 --- a/packages/app/src/review/feedback/index.ts +++ b/packages/app/src/review/feedback/index.ts @@ -23,8 +23,7 @@ export type FeedbackPhase = | { kind: "idle" } | { kind: "choosing" } | { kind: "sending"; recipient: FeedbackRecipient; commentCount: number } - | { kind: "failed"; message: string } - | { kind: "sent"; recipient: FeedbackRecipient }; + | { kind: "failed"; message: string }; export interface ReviewFeedbackState { phase: FeedbackPhase; commentCount: number; @@ -115,7 +114,7 @@ function createFeedbackView( try { await ports.send(recipient, attachment); ports.clearSent(sentComments); - workflow.changePhase({ kind: "sent", recipient }); + workflow.changePhase({ kind: "idle" }); } catch (error) { workflow.changePhase({ kind: "failed", diff --git a/packages/app/src/review/feedback/surface.tsx b/packages/app/src/review/feedback/surface.tsx index 7f87f46d88..fac499152e 100644 --- a/packages/app/src/review/feedback/surface.tsx +++ b/packages/app/src/review/feedback/surface.tsx @@ -80,17 +80,6 @@ function OpenReviewFeedbackAction({ let notice: ReactNode = unavailableNotice; if (state.phase.kind === "failed") notice = {state.phase.message}; - else if (state.phase.kind === "sent") - notice = ( - <> - - {unavailableNotice} - - ); return ( <> {state.commentCount > 0 ? ( @@ -229,7 +218,7 @@ function RecipientRow({ ); } -function SentNotice({ +export function FeedbackSentMessage({ recipient, serverId, workspaceId, @@ -241,7 +230,7 @@ function SentNotice({ const { t } = useTranslation(); const render = useCallback( (presentation: WorkspaceTabPresentation) => ( - + {t("review.feedback.sent", { recipient: presentation.label })} ), @@ -267,6 +256,10 @@ const styles = StyleSheet.create((theme) => ({ left: theme.spacing[4], alignItems: "flex-end", }, + sentMessage: { + color: theme.colors.foreground, + fontSize: theme.fontSize.sm, + }, text: { color: theme.colors.foreground, fontSize: theme.fontSize.sm, diff --git a/packages/app/src/review/feedback/use-feedback.ts b/packages/app/src/review/feedback/use-feedback.ts index e2b151953e..0596f3c55f 100644 --- a/packages/app/src/review/feedback/use-feedback.ts +++ b/packages/app/src/review/feedback/use-feedback.ts @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useSyncExternalStore } from "react"; +import { createElement, useEffect, useMemo, useRef, useSyncExternalStore } from "react"; import { useReviewDraftComments, getReviewDraftComments, @@ -22,6 +22,8 @@ import { type FeedbackAttachment, type FeedbackRecipient, } from "./index"; +import { useToast } from "@/contexts/toast-context"; +import { FeedbackSentMessage } from "./surface"; const feedbackScope = createReviewFeedbackScope(); @@ -32,6 +34,7 @@ export function useReviewFeedback(input: { attachment: FeedbackAttachment | null; }) { const { serverId, reviewDraftKey, workspaceId } = input; + const toast = useToast(); const isCompact = useIsCompactFormFactor(); const workspaceKey = workspaceId ? buildWorkspaceTabPersistenceKey({ serverId, workspaceId }) @@ -49,12 +52,14 @@ export function useReviewFeedback(input: { connected, attachment: input.attachment, sendBehavior: settings.sendBehavior, + toast, }); live.current = { client, connected, attachment: input.attachment, sendBehavior: settings.sendBehavior, + toast, }; const model = useMemo( () => @@ -85,12 +90,20 @@ export function useReviewFeedback(input: { ).turnId ?? undefined) : undefined, }); + live.current.toast.show( + createElement(FeedbackSentMessage, { + recipient, + serverId, + workspaceId: workspaceId ?? "", + }), + { variant: "success" }, + ); }, clearSent: (sentComments) => clearSentReviewDraftComments({ key: reviewDraftKey, comments: sentComments }), failedMessage: () => i18n.t("review.feedback.failed"), }), - [reviewDraftKey, serverId, workspaceKey], + [reviewDraftKey, serverId, workspaceKey, workspaceId], ); useEffect(() => { model.refresh(); diff --git a/packages/app/src/runtime/replica-cache/index.test.ts b/packages/app/src/runtime/replica-cache/index.test.ts index 15adc841fb..c48777a3e2 100644 --- a/packages/app/src/runtime/replica-cache/index.test.ts +++ b/packages/app/src/runtime/replica-cache/index.test.ts @@ -255,7 +255,93 @@ function deleteDirectory(cache: ReplicaCache, serverId: string): void { ]); } +async function cacheWithoutBackgroundVisibility() { + const storage = new MemoryStorage(); + const writer = createCache(storage); + const checkpoint = { + agents: { generation: "g", afterSeq: 12 }, + projects: { generation: "g", afterSeq: 4 }, + workspaces: { generation: "g", afterSeq: 8 }, + }; + const baseline = directory(checkpoint); + commitDirectory(writer, SERVER_ID, baseline); + writer.commitTimeline(SERVER_ID, "agent-1", timeline()); + await writer.flush(); + const key = `${SERVER_ID}:workspace:workspace-1`; + const row = storage.rows.get(key)!; + const payload = JSON.parse(row.payload) as Record; + delete payload.background; + storage.rows.set(key, { ...row, payload: JSON.stringify(payload) }); + + return { storage, baseline, reader: createCache(storage) }; +} + +async function expectLegacyWorkspaceRefresh({ + storage, + baseline, + reader, +}: Awaited>): Promise { + const restored = await reader.readDirectory(SERVER_ID); + expect(restored.workspaces.size).toBe(0); + expect(restored.checkpoint).toEqual({ + agents: baseline.checkpoint.agents, + projects: baseline.checkpoint.projects, + }); + expect(restored.agents).toEqual(baseline.agents); + expect(restored.projects).toEqual(baseline.projects); + expect(await reader.readTimeline(SERVER_ID, "agent-1")).toEqual(timeline()); + expect((await createCache(storage).readDirectory(SERVER_ID)).checkpoint).toEqual( + restored.checkpoint, + ); + + baseline.workspaces.set( + "workspace-1", + normalizeWorkspaceDescriptor({ ...workspacePayload(), background: true }), + ); + reader.replaceDirectoryBaseline(SERVER_ID, baseline); + await reader.flush(); + const refreshed = await createCache(storage).readDirectory(SERVER_ID); + expect(refreshed.workspaces.get("workspace-1")?.background).toBe(true); + expect(refreshed.checkpoint).toEqual(baseline.checkpoint); +} + describe("ReplicaCache", () => { + it.each([true, false, undefined])( + "preserves background visibility (%s) when reopening a cached workspace", + async (background) => { + const storage = new MemoryStorage(); + const writer = createCache(storage); + const baseline = directory(); + baseline.workspaces.set( + "workspace-1", + normalizeWorkspaceDescriptor({ ...workspacePayload(), background }), + ); + commitDirectory(writer, SERVER_ID, baseline); + await writer.flush(); + + const reader = createCache(storage); + expect( + (await reader.readDirectory(SERVER_ID)).workspaces.get("workspace-1")?.background, + ).toBe(background ?? false); + expect((await reader.readWorkspace(SERVER_ID, "workspace-1"))?.workspace.background).toBe( + background ?? false, + ); + }, + ); + + it("refetches workspaces cached without background visibility after a directory read", async () => { + const cached = await cacheWithoutBackgroundVisibility(); + + await expectLegacyWorkspaceRefresh(cached); + }); + + it("refetches workspaces cached without background visibility after a workspace read", async () => { + const cached = await cacheWithoutBackgroundVisibility(); + + expect(await cached.reader.readWorkspace(SERVER_ID, "workspace-1")).toBeUndefined(); + await expectLegacyWorkspaceRefresh(cached); + }); + it("rejects pre-baseline-fix checkpoints without discarding cached rows or timelines", async () => { const storage = new MemoryStorage(); const writer = createCache(storage); diff --git a/packages/app/src/runtime/replica-cache/index.ts b/packages/app/src/runtime/replica-cache/index.ts index 94937d4dc5..681d11a2c4 100644 --- a/packages/app/src/runtime/replica-cache/index.ts +++ b/packages/app/src/runtime/replica-cache/index.ts @@ -306,6 +306,8 @@ const StoredWorkspaceSchema = z.strictObject({ worktreeSlug: z.string().optional(), projectKind: z.enum(["git", "non_git", "directory"]), workspaceKind: z.enum(["directory", "local_checkout", "checkout", "worktree"]), + // Require the flag so older rows that lost background visibility invalidate the workspace cursor. + background: z.boolean(), name: z.string(), title: z.string().nullable(), pinnedAt: z.string().nullable(), @@ -698,6 +700,7 @@ function serializeWorkspace(workspace: WorkspaceDescriptor): StoredWorkspace { worktreeSlug: workspace.worktreeSlug, projectKind: workspace.projectKind, workspaceKind: workspace.workspaceKind, + background: workspace.background ?? false, name: workspace.name, title: workspace.title ?? null, pinnedAt: workspace.pinnedAt ?? null, diff --git a/packages/app/src/screens/open-project-screen.tsx b/packages/app/src/screens/open-project-screen.tsx index 4c340e0f15..4fcbb4d2fc 100644 --- a/packages/app/src/screens/open-project-screen.tsx +++ b/packages/app/src/screens/open-project-screen.tsx @@ -1,7 +1,7 @@ import { useHosts, useHostRuntimeLastError } from "@/runtime/host-runtime"; import { useCallback, useEffect, useState, type ComponentType } from "react"; import { useTranslation } from "react-i18next"; -import { View, Text, Pressable } from "react-native"; +import { View, Text, Pressable, ScrollView } from "react-native"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { useRouter } from "expo-router"; import { FolderOpen, Inbox, Plug, Smartphone } from "lucide-react-native"; @@ -18,11 +18,12 @@ import { HEADER_INNER_HEIGHT_MOBILE, HEADER_TOP_PADDING_MOBILE, } from "@/constants/layout"; -import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region"; import { useLocalDaemonServerId } from "@/hooks/use-is-local-daemon"; import { PairDeviceModal } from "@/desktop/components/pair-device-modal"; import { buildSettingsHostSectionRoute } from "@/utils/host-routes"; +const WINDOW_CONTENT_DATASET = { "window-content": "" }; + export function OpenProjectScreen() { const { t } = useTranslation(); const hosts = useHosts(); @@ -61,51 +62,56 @@ export function OpenProjectScreen() { return ( - - - - - - {hosts.map((host) => ( - - ))} - - - - - {localServerId ? ( + + + + + + {hosts.map((host) => ( + + ))} + - ) : null} + + + {localServerId ? ( + + ) : null} + - - - - + + + + ({ backgroundColor: theme.colors.surface0, userSelect: "none", }, + scroll: { + flex: 1, + }, + scrollContent: { + flexGrow: 1, + }, content: { position: "relative", - flex: 1, + flexGrow: 1, + flexShrink: 0, justifyContent: { xs: "flex-start", md: "center" }, alignItems: "center", gap: 0, padding: theme.spacing[6], paddingTop: { xs: theme.spacing[12], md: theme.spacing[6] }, - paddingBottom: { - xs: HEADER_INNER_HEIGHT_MOBILE + HEADER_TOP_PADDING_MOBILE + theme.spacing[6], - md: HEADER_INNER_HEIGHT + theme.spacing[6], - }, }, logo: { marginBottom: theme.spacing[8], @@ -238,10 +247,8 @@ const styles = StyleSheet.create((theme) => ({ lineHeight: 18, }, communityRow: { - position: "absolute", - left: 0, - right: 0, - bottom: { + flexShrink: 0, + paddingBottom: { xs: HEADER_INNER_HEIGHT_MOBILE + HEADER_TOP_PADDING_MOBILE + theme.spacing[2], md: HEADER_INNER_HEIGHT + theme.spacing[2], }, diff --git a/packages/app/src/screens/settings-screen.tsx b/packages/app/src/screens/settings-screen.tsx index 3950c0778e..31f5058940 100644 --- a/packages/app/src/screens/settings-screen.tsx +++ b/packages/app/src/screens/settings-screen.tsx @@ -275,15 +275,26 @@ function renderHostSettingsContent( // Trigger + sidebar style helpers // --------------------------------------------------------------------------- -function sidebarItemStyle({ hovered }: PressableStateCallbackType & { hovered?: boolean }) { - return [sidebarStyles.item, Boolean(hovered) && sidebarStyles.itemHovered]; +function sidebarItemStyle({ + hovered, + pressed, +}: PressableStateCallbackType & { hovered?: boolean }) { + return [ + sidebarStyles.item, + Boolean(hovered) && sidebarStyles.itemHovered, + pressed && sidebarStyles.itemPressed, + ]; } -function selectedSidebarItemStyle({ hovered }: PressableStateCallbackType & { hovered?: boolean }) { +function selectedSidebarItemStyle({ + hovered, + pressed, +}: PressableStateCallbackType & { hovered?: boolean }) { return [ sidebarStyles.item, Boolean(hovered) && sidebarStyles.itemHovered, sidebarStyles.itemSelected, + pressed && sidebarStyles.itemPressed, ]; } @@ -865,9 +876,10 @@ function HostPicker({ [], ); const triggerStyle = useCallback( - ({ hovered = false }: PressableStateCallbackType & { hovered?: boolean }) => [ + ({ hovered = false, pressed }: PressableStateCallbackType & { hovered?: boolean }) => [ sidebarStyles.pickerTrigger, hovered && sidebarStyles.pickerTriggerHovered, + pressed && sidebarStyles.itemPressed, ], [], ); @@ -1615,6 +1627,10 @@ const sidebarStyles = StyleSheet.create((theme) => ({ itemSelected: { backgroundColor: theme.colors.surfaceSidebarHover, }, + // The workspace rows' pressed fill. + itemPressed: { + backgroundColor: theme.colors.surface2, + }, label: { fontSize: { xs: theme.fontSize.lg, md: theme.fontSize.base }, color: theme.colors.foregroundMuted, diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index d8a37b7156..0872c0dc92 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -1,3 +1,4 @@ +import { AssistantFileLinkResolverProvider, type InlinePathTarget } from "@/assistant-file-links"; import { WorkspaceComposerEditingSessionsProvider, useComposerTabFullscreen, @@ -3657,6 +3658,10 @@ function WorkspaceScreenContent({ ); + const openMarkdownFile = useStableEvent((target: InlinePathTarget) => { + handleOpenPreferredAssistantFile({ location: target, parentTabId: activeTabId }); + }); + const renderedWorkspaceScreen = ( @@ -3696,7 +3701,15 @@ function WorkspaceScreenContent({ } return ( - {renderedWorkspaceScreen} + + {renderedWorkspaceScreen} + ); } diff --git a/packages/app/src/styles/markdown-styles.test.ts b/packages/app/src/styles/markdown-styles.test.ts deleted file mode 100644 index 061468d785..0000000000 --- a/packages/app/src/styles/markdown-styles.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { createCompactMarkdownStyles, createMarkdownStyles } from "./markdown-styles"; -import { darkTheme } from "./theme"; - -describe("createMarkdownStyles", () => { - it("uses the content size for conversation prose and list markers", () => { - const styles = createMarkdownStyles(darkTheme); - const proseLineHeight = Math.round(darkTheme.fontSize.content * 1.4); - - expect(styles.body).toMatchObject({ - fontSize: darkTheme.fontSize.content, - lineHeight: proseLineHeight, - }); - expect(styles.bullet_list_icon).toMatchObject({ - fontSize: darkTheme.fontSize.content, - lineHeight: proseLineHeight, - }); - expect(styles.ordered_list_icon).toMatchObject({ - fontSize: darkTheme.fontSize.content, - lineHeight: proseLineHeight, - }); - }); - - it("applies shrink-and-wrap constraints to long markdown text and links", () => { - const styles = createMarkdownStyles(darkTheme); - - expect(styles.body).toMatchObject({ - flexShrink: 1, - minWidth: 0, - width: "100%", - }); - - expect(styles.paragraph).toMatchObject({ - flexShrink: 1, - minWidth: 0, - width: "100%", - flexWrap: "wrap", - }); - - expect(styles.text).toMatchObject({ - flexShrink: 1, - minWidth: 0, - overflowWrap: "anywhere", - }); - - expect(styles.link).toMatchObject({ - flexShrink: 1, - minWidth: 0, - overflowWrap: "anywhere", - }); - - expect(styles.blocklink).toMatchObject({ - flexShrink: 1, - minWidth: 0, - overflowWrap: "anywhere", - }); - }); - - it("keeps assistant markdown text selectable on web", () => { - const styles = createMarkdownStyles(darkTheme); - - expect(styles.body).toMatchObject({ - userSelect: "text", - }); - expect(styles.text).toMatchObject({ - userSelect: "text", - }); - expect(styles.heading1).toMatchObject({ - userSelect: "text", - }); - expect(styles.link).toMatchObject({ - userSelect: "text", - }); - expect(styles.code_inline).toMatchObject({ - userSelect: "text", - }); - expect(styles.code_block).toMatchObject({ - userSelect: "text", - }); - expect(styles.fence).toMatchObject({ - userSelect: "text", - }); - expect(styles.bullet_list_icon).toMatchObject({ - userSelect: "text", - }); - expect(styles.ordered_list_icon).toMatchObject({ - userSelect: "text", - }); - }); - - it("uses the mono font-size token directly for inline and block code", () => { - const styles = createMarkdownStyles(darkTheme); - const compactStyles = createCompactMarkdownStyles(darkTheme); - - expect(styles.code_inline).toMatchObject({ - fontFamily: darkTheme.fontFamily.mono, - fontSize: darkTheme.fontSize.code, - }); - expect(styles.code_inline).not.toHaveProperty("lineHeight"); - expect(styles.code_block).toMatchObject({ - fontFamily: darkTheme.fontFamily.mono, - fontSize: darkTheme.fontSize.code, - }); - expect(styles.fence).toMatchObject({ - fontFamily: darkTheme.fontFamily.mono, - fontSize: darkTheme.fontSize.code, - }); - expect(compactStyles.code_inline).toMatchObject({ - fontFamily: darkTheme.fontFamily.mono, - fontSize: darkTheme.fontSize.code, - }); - expect(compactStyles.code_inline).not.toHaveProperty("lineHeight"); - }); - - it("scales Markdown headings from content size with safe line heights", () => { - const largeContentTheme = { - ...darkTheme, - fontSize: { ...darkTheme.fontSize, content: 21 }, - }; - const styles = createMarkdownStyles(largeContentTheme); - - expect(styles.heading1.lineHeight).toBeGreaterThan(styles.heading1.fontSize); - expect(styles.heading2.lineHeight).toBeGreaterThan(styles.heading2.fontSize); - expect(styles.heading3.lineHeight).toBeGreaterThan(styles.heading3.fontSize); - }); - - it("keeps blockquotes quiet with a square left edge", () => { - const styles = createMarkdownStyles(darkTheme); - - expect(styles.blockquote).toMatchObject({ - backgroundColor: darkTheme.colors.surface1, - color: `${darkTheme.colors.foreground}cc`, - borderLeftColor: darkTheme.colors.surface2, - paddingTop: darkTheme.spacing[3], - paddingBottom: 0, - borderTopLeftRadius: 0, - borderBottomLeftRadius: 0, - }); - expect(styles.paragraph.marginBottom).toBe(darkTheme.spacing[3]); - expect(styles.text).not.toHaveProperty("color"); - }); -}); diff --git a/packages/app/src/styles/markdown-styles.ts b/packages/app/src/styles/markdown-styles.ts deleted file mode 100644 index 5d10624136..0000000000 --- a/packages/app/src/styles/markdown-styles.ts +++ /dev/null @@ -1,422 +0,0 @@ -import { FONT_SIZE, type Theme } from "./theme"; -import { isWeb } from "@/constants/platform"; - -const webSelectableTextStyle = isWeb ? { userSelect: "text" as const } : {}; - -function contentHeadingSize(contentSize: number, tier: keyof typeof FONT_SIZE): number { - return Math.round(contentSize * (FONT_SIZE[tier] / FONT_SIZE.base)); -} - -function contentHeadingLineHeight(contentSize: number, tier: keyof typeof FONT_SIZE): number { - return Math.round(contentHeadingSize(contentSize, tier) * 1.3); -} - -/** - * Creates comprehensive markdown styles for react-native-markdown-display. - * - * Usage: - * const markdownStyles = useMemo(() => createMarkdownStyles(theme), [theme]); - * {content} - * - * Always pass `markdownit` from `@/utils/markdown-parser`. Omit it and - * react-native-markdown-display builds its own parser with `typographer: true`, - * which rewrites a literal `(c)` as ©. - */ -export function createMarkdownStyles(theme: Theme) { - return { - // ========================================================================= - // BASE STYLES - // ========================================================================= - - body: { - ...webSelectableTextStyle, - color: theme.colors.foreground, - fontSize: theme.fontSize.content, - // Prose line-height scales with the content size, not the - // code-size-coupled lineHeight.diff token used by code/diff surfaces. - lineHeight: Math.round(theme.fontSize.content * 1.4), - flexShrink: 1, - minWidth: 0, - width: "100%" as const, - }, - - text: { - ...webSelectableTextStyle, - flexShrink: 1, - minWidth: 0, - overflowWrap: "anywhere" as const, - }, - - paragraph: { - marginTop: 0, - marginBottom: theme.spacing[3], - flexWrap: "wrap" as const, - flexDirection: "row" as const, - alignItems: "flex-start" as const, - justifyContent: "flex-start" as const, - flexShrink: 1, - minWidth: 0, - width: "100%" as const, - }, - - // ========================================================================= - // HEADINGS - // ========================================================================= - - heading1: { - ...webSelectableTextStyle, - fontSize: contentHeadingSize(theme.fontSize.content, "4xl"), - fontWeight: theme.fontWeight.bold, - color: theme.colors.foreground, - marginTop: theme.spacing[6], - marginBottom: theme.spacing[3], - lineHeight: contentHeadingLineHeight(theme.fontSize.content, "4xl"), - borderBottomWidth: 1, - borderBottomColor: theme.colors.border, - paddingBottom: theme.spacing[2], - }, - - heading2: { - ...webSelectableTextStyle, - fontSize: contentHeadingSize(theme.fontSize.content, "3xl"), - fontWeight: theme.fontWeight.bold, - color: theme.colors.foreground, - marginTop: theme.spacing[6], - marginBottom: theme.spacing[3], - lineHeight: contentHeadingLineHeight(theme.fontSize.content, "3xl"), - borderBottomWidth: 1, - borderBottomColor: theme.colors.border, - paddingBottom: theme.spacing[2], - }, - - heading3: { - ...webSelectableTextStyle, - fontSize: contentHeadingSize(theme.fontSize.content, "2xl"), - fontWeight: theme.fontWeight.semibold, - color: theme.colors.foreground, - marginTop: theme.spacing[4], - marginBottom: theme.spacing[2], - lineHeight: contentHeadingLineHeight(theme.fontSize.content, "2xl"), - }, - - heading4: { - ...webSelectableTextStyle, - fontSize: contentHeadingSize(theme.fontSize.content, "xl"), - fontWeight: theme.fontWeight.semibold, - color: theme.colors.foreground, - marginTop: theme.spacing[4], - marginBottom: theme.spacing[2], - lineHeight: contentHeadingLineHeight(theme.fontSize.content, "xl"), - }, - - heading5: { - ...webSelectableTextStyle, - fontSize: contentHeadingSize(theme.fontSize.content, "lg"), - fontWeight: theme.fontWeight.semibold, - color: theme.colors.foreground, - marginTop: theme.spacing[3], - marginBottom: theme.spacing[1], - lineHeight: contentHeadingLineHeight(theme.fontSize.content, "lg"), - }, - - heading6: { - ...webSelectableTextStyle, - fontSize: contentHeadingSize(theme.fontSize.content, "lg"), - fontWeight: theme.fontWeight.semibold, - color: theme.colors.foregroundMuted, - marginTop: theme.spacing[3], - marginBottom: theme.spacing[1], - lineHeight: contentHeadingLineHeight(theme.fontSize.content, "lg"), - textTransform: "uppercase" as const, - letterSpacing: 0.5, - }, - - // ========================================================================= - // TEXT FORMATTING - // ========================================================================= - - strong: { - ...webSelectableTextStyle, - fontWeight: theme.fontWeight.medium, - }, - - em: { - ...webSelectableTextStyle, - fontStyle: "italic" as const, - }, - - s: { - ...webSelectableTextStyle, - textDecorationLine: "line-through" as const, - color: theme.colors.foregroundMuted, - }, - - link: { - ...webSelectableTextStyle, - color: theme.colors.accentBright, - textDecorationLine: "none" as const, - flexShrink: 1, - minWidth: 0, - overflowWrap: "anywhere" as const, - }, - - blocklink: { - ...webSelectableTextStyle, - color: theme.colors.accentBright, - textDecorationLine: "none" as const, - flexShrink: 1, - minWidth: 0, - overflowWrap: "anywhere" as const, - }, - - // ========================================================================= - // CODE - // ========================================================================= - - code_inline: { - ...webSelectableTextStyle, - backgroundColor: theme.colors.surface2, - color: theme.colors.foreground, - paddingHorizontal: theme.spacing[1], - paddingVertical: 2, - borderRadius: theme.borderRadius.md, - borderWidth: 0, - fontFamily: theme.fontFamily.mono, - fontSize: theme.fontSize.code, - }, - - code_block: { - ...webSelectableTextStyle, - backgroundColor: theme.colors.surface2, - color: theme.colors.foreground, - padding: theme.spacing[3], - borderRadius: theme.borderRadius.md, - fontFamily: theme.fontFamily.mono, - fontSize: theme.fontSize.code, - marginVertical: theme.spacing[2], - }, - - fence: { - ...webSelectableTextStyle, - backgroundColor: theme.colors.surface2, - color: theme.colors.foreground, - padding: theme.spacing[3], - borderRadius: theme.borderRadius.md, - borderWidth: 1, - borderColor: theme.colors.border, - fontFamily: theme.fontFamily.mono, - fontSize: theme.fontSize.code, - marginVertical: theme.spacing[3], - }, - - pre: { - marginVertical: theme.spacing[2], - }, - - // ========================================================================= - // TABLES - // ========================================================================= - - table: { - borderWidth: 1, - borderColor: theme.colors.border, - borderRadius: theme.borderRadius.md, - marginVertical: theme.spacing[3], - }, - - thead: { - backgroundColor: theme.colors.surface2, - }, - - tbody: {}, - - th: { - ...webSelectableTextStyle, - padding: theme.spacing[2], - borderBottomWidth: 1, - borderRightWidth: 1, - borderColor: theme.colors.border, - backgroundColor: theme.colors.surface2, - fontWeight: theme.fontWeight.semibold, - color: theme.colors.foreground, - fontSize: theme.fontSize.content, - textAlign: "left" as const, - }, - - tr: { - borderBottomWidth: 1, - borderColor: theme.colors.border, - flexDirection: "row" as const, - }, - - td: { - ...webSelectableTextStyle, - padding: theme.spacing[2], - borderRightWidth: 1, - borderColor: theme.colors.border, - color: theme.colors.foreground, - fontSize: theme.fontSize.content, - flex: 1, - }, - - // ========================================================================= - // LISTS - // ========================================================================= - - bullet_list: { - paddingLeft: 0, - width: "100%" as const, - }, - - ordered_list: { - paddingLeft: 0, - width: "100%" as const, - }, - - list_item: { - marginBottom: theme.spacing[1], - flexDirection: "row" as const, - alignItems: "flex-start" as const, - flexShrink: 1, - }, - - bullet_list_content: { - flex: 1, - flexShrink: 1, - }, - - ordered_list_content: { - flex: 1, - flexShrink: 1, - }, - - bullet_list_icon: { - ...webSelectableTextStyle, - color: theme.colors.foregroundMuted, - marginRight: 4, - fontSize: theme.fontSize.content, - lineHeight: Math.round(theme.fontSize.content * 1.4), - }, - - ordered_list_icon: { - ...webSelectableTextStyle, - color: theme.colors.foregroundMuted, - marginRight: 4, - fontSize: theme.fontSize.content, - fontWeight: theme.fontWeight.normal, - lineHeight: Math.round(theme.fontSize.content * 1.4), - minWidth: 12, - }, - - // ========================================================================= - // BLOCKQUOTE - // ========================================================================= - - blockquote: { - backgroundColor: theme.colors.surface1, - color: `${theme.colors.foreground}cc`, - borderLeftWidth: 4, - borderLeftColor: theme.colors.surface2, - paddingHorizontal: theme.spacing[4], - paddingTop: theme.spacing[3], - paddingBottom: 0, - marginVertical: theme.spacing[3], - borderRadius: theme.borderRadius.md, - borderTopLeftRadius: 0, - borderBottomLeftRadius: 0, - }, - - // ========================================================================= - // HORIZONTAL RULE - // ========================================================================= - - hr: { - backgroundColor: theme.colors.border, - height: 1, - marginVertical: 10, - }, - - // ========================================================================= - // IMAGES - // ========================================================================= - - image: { - borderRadius: theme.borderRadius.md, - marginVertical: theme.spacing[2], - }, - - // ========================================================================= - // BREAKS - // ========================================================================= - - hardbreak: { - height: theme.spacing[2], - }, - - softbreak: {}, - }; -} - -/** - * Creates a smaller variant of markdown styles for compact UI elements - * like thought bubbles, tooltips, or side panels. - */ -export function createCompactMarkdownStyles(theme: Theme) { - const baseStyles = createMarkdownStyles(theme); - - return { - ...baseStyles, - - body: { - ...baseStyles.body, - fontSize: theme.fontSize.content, - lineHeight: Math.round(theme.fontSize.content * 1.4), - }, - - heading1: { - ...baseStyles.heading1, - fontSize: contentHeadingSize(theme.fontSize.content, "2xl"), - marginTop: theme.spacing[4], - marginBottom: theme.spacing[2], - lineHeight: contentHeadingLineHeight(theme.fontSize.content, "2xl"), - }, - - heading2: { - ...baseStyles.heading2, - fontSize: contentHeadingSize(theme.fontSize.content, "xl"), - marginTop: theme.spacing[3], - marginBottom: theme.spacing[2], - lineHeight: contentHeadingLineHeight(theme.fontSize.content, "xl"), - }, - - heading3: { - ...baseStyles.heading3, - fontSize: contentHeadingSize(theme.fontSize.content, "lg"), - marginTop: theme.spacing[3], - marginBottom: theme.spacing[1], - lineHeight: contentHeadingLineHeight(theme.fontSize.content, "lg"), - }, - - paragraph: { - ...baseStyles.paragraph, - marginBottom: theme.spacing[2], - }, - - code_inline: { - ...baseStyles.code_inline, - fontSize: theme.fontSize.code, - }, - - code_block: { - ...baseStyles.code_block, - fontSize: theme.fontSize.code, - padding: theme.spacing[2], - }, - - fence: { - ...baseStyles.fence, - fontSize: theme.fontSize.code, - padding: theme.spacing[2], - }, - }; -} diff --git a/packages/app/src/types/react-native-markdown-display.d.ts b/packages/app/src/types/react-native-markdown-display.d.ts deleted file mode 100644 index 1b9f8044ac..0000000000 --- a/packages/app/src/types/react-native-markdown-display.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// The runtime sets `sourceInfo` on fence/code nodes (tokensToAST.js sets -// `sourceInfo: token.info`), but the shipped .d.ts omits the field. The -// re-export is what marks this file as a module so the `declare module` -// below augments the package instead of shadowing it. -export type { ASTNode } from "react-native-markdown-display"; - -declare module "react-native-markdown-display" { - interface ASTNode { - sourceInfo?: string; - } -} diff --git a/packages/app/src/types/stream.ts b/packages/app/src/types/stream.ts index 56cf14558b..9425cabbed 100644 --- a/packages/app/src/types/stream.ts +++ b/packages/app/src/types/stream.ts @@ -712,8 +712,6 @@ export interface AssistantMessageItem { text: string; timestamp: Date; /** Display-only fields, assigned after source-item plugin transforms. */ - blockGroupId?: string; - blockIndex?: number; } export interface TimelinePosition { diff --git a/packages/app/src/utils/__tests__/split-markdown-blocks.test.ts b/packages/app/src/utils/__tests__/split-markdown-blocks.test.ts deleted file mode 100644 index 22eddf8152..0000000000 --- a/packages/app/src/utils/__tests__/split-markdown-blocks.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { splitMarkdownBlocks } from "../split-markdown-blocks"; - -describe("splitMarkdownBlocks", () => { - it("returns a single block for a single paragraph", () => { - expect(splitMarkdownBlocks("Hello world")).toEqual(["Hello world"]); - }); - - it("splits two paragraphs separated by a double newline", () => { - expect(splitMarkdownBlocks("First paragraph\n\nSecond paragraph")).toEqual([ - "First paragraph", - "Second paragraph", - ]); - }); - - it("keeps a fenced code block with internal double newlines as one block", () => { - expect(splitMarkdownBlocks("```ts\nconst a = 1;\n\nconst b = 2;\n```")).toEqual([ - "```ts\nconst a = 1;\n\nconst b = 2;\n```", - ]); - }); - - it("does not treat 4-space-indented backticks as a fence", () => { - expect(splitMarkdownBlocks("Before\n\n ```\n code\n ```\n\nAfter")).toEqual([ - "Before", - " ```\n code\n ```", - "After", - ]); - }); - - it("handles tilde fences", () => { - expect(splitMarkdownBlocks("Before\n\n~~~\ncode\n~~~\n\nAfter")).toEqual([ - "Before", - "~~~\ncode\n~~~", - "After", - ]); - }); - - it("splits mixed paragraph, code fence, and paragraph content into three blocks", () => { - expect( - splitMarkdownBlocks( - "Intro paragraph\n\n```ts\nconst a = 1;\n\nconst b = 2;\n```\n\nOutro paragraph", - ), - ).toEqual(["Intro paragraph", "```ts\nconst a = 1;\n\nconst b = 2;\n```", "Outro paragraph"]); - }); - - it("keeps everything from an unclosed fence start as one block for streaming content", () => { - expect(splitMarkdownBlocks("Before fence\n\n```ts\nconst a = 1;\n\nconst b = 2;")).toEqual([ - "Before fence", - "```ts\nconst a = 1;\n\nconst b = 2;", - ]); - }); - - it("returns an empty array for empty input", () => { - expect(splitMarkdownBlocks("")).toEqual([]); - }); - - it("splits a heading followed by a paragraph into two blocks", () => { - expect(splitMarkdownBlocks("# Heading\n\nParagraph text")).toEqual([ - "# Heading", - "Paragraph text", - ]); - }); - - it("keeps consecutive list items together when there is no double newline", () => { - expect(splitMarkdownBlocks("- First item\n- Second item\n- Third item")).toEqual([ - "- First item\n- Second item\n- Third item", - ]); - }); - - it("keeps a loose nested list in its outer list block", () => { - expect( - splitMarkdownBlocks( - "Before\n\n3. Outer three\n\n 7. Inner seven\n 8. Inner eight\n\nAfter", - ), - ).toEqual(["Before", "3. Outer three\n\n 7. Inner seven\n 8. Inner eight", "After"]); - }); - - it("keeps a link reference definition with the paragraph that uses it", () => { - expect(splitMarkdownBlocks("See the [docs][d].\n\n[d]: https://example.com")).toEqual([ - "See the [docs][d].\n\n[d]: https://example.com", - ]); - }); - - it("folds a leading definition-only block into the block below it", () => { - expect(splitMarkdownBlocks('[d]: https://example.com "Docs"\n\nSee the [docs][d].')).toEqual([ - '[d]: https://example.com "Docs"\n\nSee the [docs][d].', - ]); - }); - - it("folds several definition lines and several definition blocks into one block", () => { - expect( - splitMarkdownBlocks( - "See [one][a] and [two][b].\n\n[a]: https://example.com/a\n[b]: \n\n[c]: https://example.com/c 'Third'\n\nAfter", - ), - ).toEqual([ - "See [one][a] and [two][b].\n\n[a]: https://example.com/a\n[b]: \n\n[c]: https://example.com/c 'Third'", - "After", - ]); - }); - - it("recognizes every destination the renderer accepts, including escaped spaces", () => { - expect(splitMarkdownBlocks("See [docs].\n\n[docs]: docs\\ folder/readme")).toEqual([ - "See [docs].\n\n[docs]: docs\\ folder/readme", - ]); - expect(splitMarkdownBlocks("See [docs].\n\n[docs]: 'Title'")).toEqual([ - "See [docs].\n\n[docs]: 'Title'", - ]); - }); - - it("leaves a definition-only message as its own block", () => { - expect(splitMarkdownBlocks("[d]: https://example.com")).toEqual(["[d]: https://example.com"]); - }); - - it("does not fold a paragraph that merely starts with a bracketed link", () => { - expect(splitMarkdownBlocks("Intro\n\n[Link](https://example.com) and more prose")).toEqual([ - "Intro", - "[Link](https://example.com) and more prose", - ]); - }); - - it("treats triple newlines as a split point and filters empty blocks", () => { - expect(splitMarkdownBlocks("First paragraph\n\n\nSecond paragraph")).toEqual([ - "First paragraph", - "Second paragraph", - ]); - }); -}); diff --git a/packages/app/src/utils/assistant-image-metadata.test.ts b/packages/app/src/utils/assistant-image-metadata.test.ts deleted file mode 100644 index 3ebee656dd..0000000000 --- a/packages/app/src/utils/assistant-image-metadata.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { beforeEach, describe, expect, it } from "vitest"; -import { DEFAULT_CONTENT_MAX_WIDTH } from "@/styles/theme"; -import { - clearAssistantImageMetadataCache, - estimateAssistantMessageHeightFromCache, - extractAssistantImageSources, - getAssistantImageMetadata, - setAssistantImageMetadata, -} from "./assistant-image-metadata"; - -describe("assistant image metadata", () => { - beforeEach(() => { - clearAssistantImageMetadataCache(); - }); - - it("extracts markdown image sources", () => { - expect( - extractAssistantImageSources( - 'Before\n\n![local](/tmp/paseo.png)\n\n![remote](https://example.com/test.png "Remote")', - ), - ).toEqual(["/tmp/paseo.png", "https://example.com/test.png"]); - }); - - it("keeps local image metadata scoped to its server and workspace", () => { - const image = { - source: "screenshot.png", - workspaceRoot: "/workspace/one", - serverId: "server-1", - }; - setAssistantImageMetadata(image, { width: 1200, height: 800 }); - - expect(getAssistantImageMetadata({ ...image, workspaceRoot: "/workspace/two" })).toBeNull(); - expect(getAssistantImageMetadata({ ...image, serverId: "server-2" })).toBeNull(); - expect(getAssistantImageMetadata({ source: image.source })).toBeNull(); - expect(getAssistantImageMetadata(image)).toEqual({ - width: 1200, - height: 800, - aspectRatio: 1.5, - }); - - setAssistantImageMetadata( - { ...image, workspaceRoot: "/workspace/two" }, - { width: 800, height: 1200 }, - ); - expect(getAssistantImageMetadata(image)?.aspectRatio).toBe(1.5); - expect( - getAssistantImageMetadata({ ...image, workspaceRoot: "/workspace/two" })?.aspectRatio, - ).toBe(2 / 3); - }); - - it("reuses direct image metadata without workspace context", () => { - const source = "https://example.com/shared.png"; - setAssistantImageMetadata( - { source, workspaceRoot: "/workspace/one", serverId: "server-1" }, - { width: 1200, height: 800 }, - ); - expect(getAssistantImageMetadata({ source })).toEqual({ - width: 1200, - height: 800, - aspectRatio: 1.5, - }); - }); - - it("reserves the default image aspect ratio before metadata arrives", () => { - expect( - estimateAssistantMessageHeightFromCache({ - markdown: "![Pending](https://example.com/pending.png)", - contentMaxWidth: 608, - }), - ).toBe(464); - }); - - it("estimates assistant message height from cached image metadata", () => { - setAssistantImageMetadata( - { - source: "https://example.com/landscape.png", - }, - { width: 1200, height: 800 }, - ); - - expect( - estimateAssistantMessageHeightFromCache({ - markdown: "Here is the screenshot\n\n![Screenshot](https://example.com/landscape.png)", - contentMaxWidth: DEFAULT_CONTENT_MAX_WIDTH, - }), - ).toBeGreaterThan(220); - }); - - it("estimates image-only data-image markdown without caching the full payload as text", () => { - const source = `data:image/png;base64,${"a".repeat(512)}`; - setAssistantImageMetadata({ source }, { width: 1200, height: 800 }); - - const imageOnlyHeight = estimateAssistantMessageHeightFromCache({ - markdown: `![Screenshot](${source})`, - contentMaxWidth: DEFAULT_CONTENT_MAX_WIDTH, - }); - const mixedHeight = estimateAssistantMessageHeightFromCache({ - markdown: `Text\n\n![Screenshot](${source})`, - contentMaxWidth: DEFAULT_CONTENT_MAX_WIDTH, - }); - - expect(imageOnlyHeight).toBeGreaterThan(220); - expect(mixedHeight).toBeGreaterThan(imageOnlyHeight ?? 0); - }); -}); diff --git a/packages/app/src/utils/assistant-image-metadata.ts b/packages/app/src/utils/assistant-image-metadata.ts deleted file mode 100644 index ac7bd217b2..0000000000 --- a/packages/app/src/utils/assistant-image-metadata.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { resolveAssistantImageSource } from "@/utils/assistant-image-source"; -import { createImageSourceCacheKey } from "@/attachments/utils"; - -export interface AssistantImageContext { - serverId?: string; - workspaceRoot?: string; -} - -export interface AssistantImageMetadata { - width: number; - height: number; - aspectRatio: number; -} - -const assistantImageMetadataCache = new Map(); -const assistantImageParseCache = new Map(); -const ASSISTANT_IMAGE_METADATA_CACHE_LIMIT = 500; -const ASSISTANT_IMAGE_PARSE_CACHE_LIMIT = 500; - -const MARKDOWN_IMAGE_PATTERN = /!\[[^\]]*]\((<[^>]+>|[^)\n]+)\)/g; -export const ASSISTANT_IMAGE_DEFAULT_ASPECT_RATIO = 3 / 2; -const ASSISTANT_IMAGE_INSET = 8; -const ASSISTANT_IMAGE_MIN_HEIGHT = 160; -const ASSISTANT_IMAGE_BLOCK_GAP = 24; -const ASSISTANT_MESSAGE_BASE_HEIGHT = 96; -const ASSISTANT_MESSAGE_MIN_HEIGHT = 220; -const ASSISTANT_MESSAGE_IMAGE_ONLY_BASE_HEIGHT = 40; - -function touchCacheEntry(cache: Map, key: K, value: V, limit: number): void { - cache.delete(key); - cache.set(key, value); - if (cache.size <= limit) { - return; - } - const oldestKey = cache.keys().next().value; - if (oldestKey !== undefined) { - cache.delete(oldestKey); - } -} - -function normalizeAssistantImageSourceToken(value: string): string | null { - const trimmed = value.trim(); - if (!trimmed) { - return null; - } - - if (trimmed.startsWith("<") && trimmed.endsWith(">")) { - const inner = trimmed.slice(1, -1).trim(); - return inner || null; - } - - const titleMatch = /^(.*?)(?:\s+(['"]).*?\2)?$/.exec(trimmed); - const source = titleMatch?.[1]?.trim() ?? trimmed; - return source || null; -} - -function parseAssistantImageMarkdown(markdown: string): { - sources: string[]; - hasNonImageText: boolean; -} { - const sources: string[] = []; - for (const match of markdown.matchAll(MARKDOWN_IMAGE_PATTERN)) { - const normalized = normalizeAssistantImageSourceToken(match[1] ?? ""); - if (normalized) { - sources.push(normalized); - } - } - return { - sources, - hasNonImageText: markdown.replace(MARKDOWN_IMAGE_PATTERN, "").trim().length > 0, - }; -} - -function getAssistantImageMetadataKey( - input: AssistantImageContext & { source: string }, -): string | null { - const resolution = resolveAssistantImageSource({ - source: input.source, - workspaceRoot: input.workspaceRoot, - }); - if (!resolution) { - return null; - } - if (resolution.kind === "direct") { - return `direct:${createImageSourceCacheKey(resolution.uri)}`; - } - // A file path identifies an image only within its daemon and workspace. - // Callers without that context must use the default aspect ratio. - if (!input.serverId) { - return null; - } - return JSON.stringify(["file", input.serverId, resolution.cwd, resolution.path]); -} - -export function getAssistantImageMetadata( - input: AssistantImageContext & { source: string }, -): AssistantImageMetadata | null { - const key = getAssistantImageMetadataKey(input); - const metadata = key ? assistantImageMetadataCache.get(key) : undefined; - if (!key || !metadata) { - return null; - } - touchCacheEntry(assistantImageMetadataCache, key, metadata, ASSISTANT_IMAGE_METADATA_CACHE_LIMIT); - return metadata; -} - -export function setAssistantImageMetadata( - input: AssistantImageContext & { source: string }, - dimensions: { width: number; height: number }, -): AssistantImageMetadata | null { - const { width, height } = dimensions; - if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) { - return null; - } - - const metadata: AssistantImageMetadata = { - width, - height, - aspectRatio: width / height, - }; - - const key = getAssistantImageMetadataKey(input); - if (key) { - touchCacheEntry( - assistantImageMetadataCache, - key, - metadata, - ASSISTANT_IMAGE_METADATA_CACHE_LIMIT, - ); - } - - return metadata; -} - -export function extractAssistantImageSources(markdown: string): string[] { - const shouldCacheParse = !/data:image\//i.test(markdown); - const cachedParse = shouldCacheParse ? assistantImageParseCache.get(markdown) : undefined; - if (cachedParse) { - touchCacheEntry( - assistantImageParseCache, - markdown, - cachedParse, - ASSISTANT_IMAGE_PARSE_CACHE_LIMIT, - ); - return cachedParse.sources; - } - - const parsed = parseAssistantImageMarkdown(markdown); - if (shouldCacheParse) { - touchCacheEntry(assistantImageParseCache, markdown, parsed, ASSISTANT_IMAGE_PARSE_CACHE_LIMIT); - } - return parsed.sources; -} - -export interface AssistantMessageHeightEstimateInput { - markdown: string; - contentMaxWidth: number; - imageContext?: AssistantImageContext; -} - -export function estimateAssistantMessageHeightFromCache({ - markdown, - contentMaxWidth, - imageContext, -}: AssistantMessageHeightEstimateInput): number | null { - const parsed = assistantImageParseCache.get(markdown) ?? parseAssistantImageMarkdown(markdown); - if (parsed.sources.length === 0) { - return null; - } - - const imageHeights = parsed.sources.map((source) => { - const aspectRatio = - getAssistantImageMetadata({ source, ...imageContext })?.aspectRatio ?? - ASSISTANT_IMAGE_DEFAULT_ASPECT_RATIO; - return Math.max( - ASSISTANT_IMAGE_MIN_HEIGHT, - Math.round((contentMaxWidth - ASSISTANT_IMAGE_INSET) / aspectRatio), - ); - }); - - const baseHeight = parsed.hasNonImageText - ? ASSISTANT_MESSAGE_BASE_HEIGHT - : ASSISTANT_MESSAGE_IMAGE_ONLY_BASE_HEIGHT; - - const estimatedHeight = - baseHeight + - imageHeights.reduce((sum, height) => sum + height, 0) + - ASSISTANT_IMAGE_BLOCK_GAP * imageHeights.length; - - return Math.max(ASSISTANT_MESSAGE_MIN_HEIGHT, estimatedHeight); -} - -export function clearAssistantImageMetadataCache(): void { - assistantImageMetadataCache.clear(); - assistantImageParseCache.clear(); -} diff --git a/packages/app/src/utils/assistant-markdown-parser.test.ts b/packages/app/src/utils/assistant-markdown-parser.test.ts index 05b1366e1c..f22307239a 100644 --- a/packages/app/src/utils/assistant-markdown-parser.test.ts +++ b/packages/app/src/utils/assistant-markdown-parser.test.ts @@ -2,141 +2,6 @@ import { describe, expect, it } from "vitest"; import { createAssistantMarkdownParser } from "./assistant-markdown-parser"; describe("createAssistantMarkdownParser", () => { - it("keeps bold text bold through every partial closing marker", () => { - const parser = createAssistantMarkdownParser({ streaming: true }); - - for (const source of ["**bold", "**bold*", "**bold**"]) { - expect(parser.renderInline(source)).toBe("bold"); - } - }); - - it("shows the growing link label and only links a complete destination", () => { - const parser = createAssistantMarkdownParser({ streaming: true }); - const source = "[docs](https://example.com/path)"; - for (let length = 1; length < source.length; length++) { - expect(parser.renderInline(source.slice(0, length))).toBe( - "docs".slice(0, Math.max(0, length - 1)), - ); - } - expect(parser.renderInline(source)).toBe('docs'); - }); - - it.each([ - ["**", "strong"], - ["__", "strong"], - ["*", "em"], - ["_", "em"], - ["~~", "s"], - ["`", "code"], - ["``", "code"], - ])("keeps %s formatting stable as text and closing markers arrive", (marker, tag) => { - const parser = createAssistantMarkdownParser({ streaming: true }); - for (let length = 1; length <= 4; length++) { - expect(parser.renderInline(marker + "text".slice(0, length))).toBe( - `<${tag}>${"text".slice(0, length)}`, - ); - } - for (let length = 0; length <= marker.length; length++) { - expect(parser.renderInline(marker + "text" + marker.slice(0, length))).toBe( - `<${tag}>text`, - ); - } - }); - - it("keeps combined and nested emphasis stable through closing markers", () => { - const parser = createAssistantMarkdownParser({ streaming: true }); - for (const closing of ["", "*", "**", "***"]) { - expect(parser.renderInline("***both" + closing)).toBe("both"); - } - expect(parser.renderInline("**bold and *italic")).toBe( - "bold and italic", - ); - expect(parser.renderInline("*italic and **bold")).toBe( - "italic and bold", - ); - }); - - it.each(["*", "**", "***", "_", "__", "~~", "`", "``"])( - "hides an opening %s while waiting for its text", - (marker) => { - expect( - createAssistantMarkdownParser({ streaming: true }).renderInline("hello " + marker), - ).toBe("hello "); - }, - ); - - it.each([ - "[docs](https://example.com/a(b)c)", - '[docs](https://example.com "a title)")', - "[docs]()", - "[docs](file:///tmp/example.ts)", - ])("waits for the entire destination of %s", (source) => { - const parser = createAssistantMarkdownParser({ streaming: true }); - for (let length = 6; length < source.length; length++) { - expect(parser.renderInline(source.slice(0, length))).toBe("docs"); - } - expect(parser.renderInline(source)).toBe(createAssistantMarkdownParser().renderInline(source)); - }); - - it("preserves formatting in incomplete labels and around incomplete links", () => { - const parser = createAssistantMarkdownParser({ streaming: true }); - expect(parser.renderInline("[**bold")).toBe("bold"); - expect(parser.renderInline("[**bold**](https://exam")).toBe("bold"); - expect(parser.renderInline("**see [docs](https://exam")).toBe("see docs"); - }); - - it("does not auto-link a URL label before the destination is complete", () => { - const parser = createAssistantMarkdownParser({ streaming: true }); - expect(parser.renderInline("Read [example.com](https://exam")).toBe("Read example.com"); - expect(parser.renderInline("Read [example.com](https://example.org)")).toBe( - 'Read example.com', - ); - }); - - it.each([ - "\\*literal", - "\\[literal", - "some_identifier", - "some__identifier", - "`**literal [link](url`", - "``a ` b``", - "**already closed** after", - "[bad](javascript:alert(1))", - " { - expect(createAssistantMarkdownParser({ streaming: true }).renderInline(source)).toBe( - createAssistantMarkdownParser().renderInline(source), - ); - }); - - it.each([ - "```md\n**literal [link](url", - "~~~md\n**literal [link](url", - " **literal [link](url", - "**earlier\n\nplain tail", - "- **earlier\n- plain tail", - ])("leaves literal code and earlier blocks alone: %s", (source) => { - expect(createAssistantMarkdownParser({ streaming: true }).render(source)).toBe( - createAssistantMarkdownParser().render(source), - ); - }); - - it("completes inline formatting inside the final list item", () => { - expect(createAssistantMarkdownParser({ streaming: true }).render("- first\n- **bold")).toBe( - "
    \n
  • first
  • \n
  • bold
  • \n
\n", - ); - }); - - it("hides incomplete images until their source is complete", () => { - const parser = createAssistantMarkdownParser({ streaming: true }); - expect(parser.renderInline("before ![alt](https://exam")).toBe("before "); - expect(parser.renderInline("before ![alt](https://example.com/image.png)")).toBe( - 'before alt', - ); - }); - it("keeps ordinary parsing for completed messages", () => { const parser = createAssistantMarkdownParser(); expect(parser.renderInline("**unfinished")).toBe("**unfinished"); diff --git a/packages/app/src/utils/assistant-markdown-parser.ts b/packages/app/src/utils/assistant-markdown-parser.ts index e1e8f6f38a..1007fc5c93 100644 --- a/packages/app/src/utils/assistant-markdown-parser.ts +++ b/packages/app/src/utils/assistant-markdown-parser.ts @@ -1,19 +1,13 @@ import type MarkdownIt from "markdown-it"; import { createMarkdownParser } from "@/utils/markdown-parser"; -import { enableStreamingMarkdown } from "@/utils/streaming-markdown"; -export function createAssistantMarkdownParser({ streaming = false } = {}): MarkdownIt { +export function createAssistantMarkdownParser(): MarkdownIt { const parser = createMarkdownParser({ linkify: true }); const defaultValidateLink = parser.validateLink.bind(parser); - // Assistant messages are the only surface allowed to link into the - // filesystem. Every other parser keeps markdown-it's stricter default. + // Rich clipboard HTML preserves workspace file links. parser.validateLink = (url: string) => url.trim().toLowerCase().startsWith("file://") || defaultValidateLink(url); - if (streaming) { - enableStreamingMarkdown(parser); - } - return parser; } diff --git a/packages/app/src/utils/assistant-message-height-estimate.test.ts b/packages/app/src/utils/assistant-message-height-estimate.test.ts index d4e3a2ff94..545704a57d 100644 --- a/packages/app/src/utils/assistant-message-height-estimate.test.ts +++ b/packages/app/src/utils/assistant-message-height-estimate.test.ts @@ -3,69 +3,30 @@ import { DEFAULT_CONTENT_MAX_WIDTH } from "@/styles/theme"; import { clearAssistantMessageHeightEstimateCache, estimateAssistantMessageHeightFromCache, - setAssistantMarkdownBlockHeight, + setAssistantMessageHeight, } from "./assistant-message-height-estimate"; -import { - clearAssistantImageMetadataCache, - setAssistantImageMetadata, -} from "./assistant-image-metadata"; - describe("assistant message height estimate", () => { beforeEach(() => { clearAssistantMessageHeightEstimateCache(); - clearAssistantImageMetadataCache(); }); - it("estimates assistant message height from measured markdown block heights", () => { - setAssistantMarkdownBlockHeight({ - block: "First paragraph", - width: 804, - height: 18.2, + it("uses a whole-message measurement only at the measured width", () => { + setAssistantMessageHeight({ + markdown: "First\n\nSecond", + width: DEFAULT_CONTENT_MAX_WIDTH, + height: 97, }); - setAssistantMarkdownBlockHeight({ - block: "Second paragraph", - width: 804, - height: 41.1, - }); - expect( estimateAssistantMessageHeightFromCache({ - markdown: "First paragraph\n\nSecond paragraph", + markdown: "First\n\nSecond", contentMaxWidth: DEFAULT_CONTENT_MAX_WIDTH, }), ).toBe(97); - }); - - it("reads block heights measured at the configured content width", () => { - setAssistantMarkdownBlockHeight({ block: "Wide paragraph", width: 1584, height: 20 }); - expect( estimateAssistantMessageHeightFromCache({ - markdown: "Wide paragraph", + markdown: "First\n\nSecond", contentMaxWidth: 1600, }), - ).toBe(44); - expect( - estimateAssistantMessageHeightFromCache({ - markdown: "Wide paragraph", - contentMaxWidth: DEFAULT_CONTENT_MAX_WIDTH, - }), ).toBeNull(); }); - - it("falls back to image metadata when markdown blocks are not measured", () => { - setAssistantImageMetadata( - { - source: "https://example.com/landscape.png", - }, - { width: 1200, height: 800 }, - ); - - expect( - estimateAssistantMessageHeightFromCache({ - markdown: "Here is the screenshot\n\n![Screenshot](https://example.com/landscape.png)", - contentMaxWidth: DEFAULT_CONTENT_MAX_WIDTH, - }), - ).toBeGreaterThan(220); - }); }); diff --git a/packages/app/src/utils/assistant-message-height-estimate.ts b/packages/app/src/utils/assistant-message-height-estimate.ts index 02e153e0bb..a7d34b115b 100644 --- a/packages/app/src/utils/assistant-message-height-estimate.ts +++ b/packages/app/src/utils/assistant-message-height-estimate.ts @@ -1,20 +1,11 @@ -import { - type AssistantMessageHeightEstimateInput, - estimateAssistantMessageHeightFromCache as estimateAssistantImageMessageHeightFromCache, -} from "@/utils/assistant-image-metadata"; -import { splitMarkdownBlocks } from "@/utils/split-markdown-blocks"; +const ASSISTANT_MESSAGE_HEIGHT_CACHE_LIMIT = 1000; -const ASSISTANT_MARKDOWN_BLOCK_HEIGHT_CACHE_LIMIT = 1000; -const ASSISTANT_MARKDOWN_BLOCK_INSET = 16; -const ASSISTANT_MESSAGE_VERTICAL_PADDING = 24; -const ASSISTANT_MARKDOWN_BLOCK_GAP = 12; - -interface MarkdownBlockHeightInput { - block: string; +interface MessageHeightInput { + markdown: string; width: number; } -const assistantMarkdownBlockHeightCache = new Map(); +const assistantMessageHeightCache = new Map(); function touchCacheEntry(cache: Map, key: K, value: V, limit: number): void { cache.delete(key); @@ -28,98 +19,67 @@ function touchCacheEntry(cache: Map, key: K, value: V, limit: number } } -function hashMarkdownBlock(block: string): string { +function hashMessage(markdown: string): string { let hash = 2166136261; - for (let index = 0; index < block.length; index += 1) { - hash ^= block.charCodeAt(index); + for (let index = 0; index < markdown.length; index += 1) { + hash ^= markdown.charCodeAt(index); hash = Math.imul(hash, 16777619); } - return `${block.length}:${(hash >>> 0).toString(36)}`; + return `${markdown.length}:${(hash >>> 0).toString(36)}`; } -function normalizeMarkdownBlockWidth(width: number): number | null { +function normalizeMessageWidth(width: number): number | null { if (!Number.isFinite(width) || width <= 0) { return null; } return Math.round(width); } -function createMarkdownBlockHeightKey(input: MarkdownBlockHeightInput): string | null { - const normalizedWidth = normalizeMarkdownBlockWidth(input.width); +function createMessageHeightKey(input: MessageHeightInput): string | null { + const normalizedWidth = normalizeMessageWidth(input.width); if (normalizedWidth === null) { return null; } - if (input.block.length === 0) { + if (input.markdown.length === 0) { return null; } - return `${normalizedWidth}:${hashMarkdownBlock(input.block)}`; + return `${normalizedWidth}:${hashMessage(input.markdown)}`; } -export function setAssistantMarkdownBlockHeight(input: { - block: string; +export function setAssistantMessageHeight(input: { + markdown: string; width: number; height: number; }): number | null { if (!Number.isFinite(input.height) || input.height <= 0) { return null; } - const key = createMarkdownBlockHeightKey({ - block: input.block, + const key = createMessageHeightKey({ + markdown: input.markdown, width: input.width, }); if (!key) { return null; } const height = Math.ceil(input.height); - touchCacheEntry( - assistantMarkdownBlockHeightCache, - key, - height, - ASSISTANT_MARKDOWN_BLOCK_HEIGHT_CACHE_LIMIT, - ); + touchCacheEntry(assistantMessageHeightCache, key, height, ASSISTANT_MESSAGE_HEIGHT_CACHE_LIMIT); return height; } -function estimateAssistantMarkdownBlockHeightFromCache( - markdown: string, - contentMaxWidth: number, -): number | null { - const blocks = splitMarkdownBlocks(markdown); - if (blocks.length === 0) { - return null; - } - - let blockHeight = 0; - for (const block of blocks) { - const key = createMarkdownBlockHeightKey({ - block, - width: contentMaxWidth - ASSISTANT_MARKDOWN_BLOCK_INSET, - }); - const cachedHeight = key ? assistantMarkdownBlockHeightCache.get(key) : undefined; - if (cachedHeight === undefined) { - return null; - } - blockHeight += cachedHeight; - } - - return ( - ASSISTANT_MESSAGE_VERTICAL_PADDING + - blockHeight + - ASSISTANT_MARKDOWN_BLOCK_GAP * Math.max(0, blocks.length - 1) - ); -} - export function estimateAssistantMessageHeightFromCache({ markdown, contentMaxWidth, - imageContext, -}: AssistantMessageHeightEstimateInput): number | null { +}: { + markdown: string; + contentMaxWidth: number; +}): number | null { return ( - estimateAssistantMarkdownBlockHeightFromCache(markdown, contentMaxWidth) ?? - estimateAssistantImageMessageHeightFromCache({ markdown, contentMaxWidth, imageContext }) + assistantMessageHeightCache.get( + createMessageHeightKey({ markdown: markdown, width: contentMaxWidth }) ?? "", + ) ?? null ); } export function clearAssistantMessageHeightEstimateCache(): void { - assistantMarkdownBlockHeightCache.clear(); + assistantMessageHeightCache.clear(); } diff --git a/packages/app/src/utils/markdown-ast.test.ts b/packages/app/src/utils/markdown-ast.test.ts deleted file mode 100644 index e7b439d79f..0000000000 --- a/packages/app/src/utils/markdown-ast.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { markdownNodeContainsType } from "./markdown-ast"; - -describe("markdownNodeContainsType", () => { - it("matches the node itself", () => { - expect(markdownNodeContainsType({ type: "image", children: [] }, "image")).toBe(true); - }); - - it("matches descendants", () => { - const paragraph = { - type: "paragraph", - children: [ - { type: "text", children: [] }, - { - type: "link", - children: [{ type: "image", children: [] }], - }, - ], - }; - - expect(markdownNodeContainsType(paragraph, "image")).toBe(true); - }); - - it("returns false when the type is absent", () => { - const paragraph = { - type: "paragraph", - children: [ - { type: "text", children: [] }, - { type: "strong", children: [{ type: "text", children: [] }] }, - ], - }; - - expect(markdownNodeContainsType(paragraph, "image")).toBe(false); - }); -}); diff --git a/packages/app/src/utils/markdown-ast.ts b/packages/app/src/utils/markdown-ast.ts deleted file mode 100644 index bcd2526e41..0000000000 --- a/packages/app/src/utils/markdown-ast.ts +++ /dev/null @@ -1,12 +0,0 @@ -export interface MarkdownAstNodeWithChildren { - type: string; - children: MarkdownAstNodeWithChildren[]; -} - -export function markdownNodeContainsType(node: MarkdownAstNodeWithChildren, type: string): boolean { - if (node.type === type) { - return true; - } - - return node.children.some((child) => markdownNodeContainsType(child, type)); -} diff --git a/packages/app/src/utils/markdown-list.test.ts b/packages/app/src/utils/markdown-list.test.ts deleted file mode 100644 index a30a69a9eb..0000000000 --- a/packages/app/src/utils/markdown-list.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { getMarkdownListMarker, getMarkdownListSpacing } from "./markdown-list"; - -describe("getMarkdownListMarker", () => { - it("returns a bullet marker for unordered list items", () => { - expect(getMarkdownListMarker({ index: 0 }, [{ type: "bullet_list" }])).toEqual({ - isOrdered: false, - marker: "•", - }); - }); - - it("returns numbered markers for ordered list items", () => { - expect(getMarkdownListMarker({ index: 1, markup: "." }, [{ type: "ordered_list" }])).toEqual({ - isOrdered: true, - marker: "2.", - }); - }); - - it("respects ordered list start attribute", () => { - expect( - getMarkdownListMarker({ index: 2, markup: ")" }, [ - { type: "ordered_list", attributes: { start: "5" } }, - ]), - ).toEqual({ - isOrdered: true, - marker: "7)", - }); - }); - - it("prefers the nearest list ancestor in nested lists", () => { - expect( - getMarkdownListMarker({ index: 0, markup: "." }, [ - { type: "ordered_list" }, - { type: "bullet_list" }, - ]), - ).toEqual({ - isOrdered: true, - marker: "1.", - }); - }); -}); - -describe("getMarkdownListSpacing", () => { - it("keeps top-level list spacing as a section boundary", () => { - const paragraph = { type: "paragraph" }; - const list = { type: "bullet_list" }; - const body = { type: "body", children: [list, paragraph] }; - - expect(getMarkdownListSpacing(list, [body])).toEqual({ - marginTop: 4, - marginBottom: 16, - }); - }); - - it("does not add bottom spacing after a list at the end of a markdown block", () => { - const list = { type: "bullet_list" }; - const body = { type: "body", children: [list] }; - - expect(getMarkdownListSpacing(list, [body])).toEqual({ - marginTop: 4, - marginBottom: 0, - }); - }); - - it("uses a smaller gap between adjacent top-level lists", () => { - const list = { type: "bullet_list" }; - const body = { type: "body", children: [list, { type: "ordered_list" }] }; - - expect(getMarkdownListSpacing(list, [body])).toEqual({ - marginTop: 4, - marginBottom: 8, - }); - }); - - it("does not add section spacing after a nested list", () => { - expect( - getMarkdownListSpacing({ type: "bullet_list" }, [ - { type: "list_item" }, - { type: "bullet_list" }, - { type: "body" }, - ]), - ).toEqual({ - marginTop: 4, - marginBottom: 0, - }); - }); -}); diff --git a/packages/app/src/utils/markdown-list.ts b/packages/app/src/utils/markdown-list.ts deleted file mode 100644 index 3dd5150242..0000000000 --- a/packages/app/src/utils/markdown-list.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { SPACING } from "@/styles/theme"; - -interface MarkdownNode { - type?: string; - index?: number; - markup?: string; - attributes?: { - start?: number | string; - }; - children?: MarkdownNode[]; -} - -const LIST_BULLET = "•"; -const DEFAULT_ORDERED_LIST_MARKUP = "."; -const MARKDOWN_LIST_MARGIN_TOP = SPACING[1]; -const MARKDOWN_LIST_MARGIN_BOTTOM_TO_PROSE = SPACING[4]; -const MARKDOWN_LIST_MARGIN_BOTTOM_TO_LIST = SPACING[2]; -const MARKDOWN_NESTED_LIST_MARGIN_BOTTOM = 0; -const MARKDOWN_TERMINAL_LIST_MARGIN_BOTTOM = 0; - -function toParentNodes(parent: unknown): MarkdownNode[] { - if (Array.isArray(parent)) { - return parent; - } - - if (parent && typeof parent === "object") { - return [parent as MarkdownNode]; - } - - return []; -} - -function getNearestListParent(parent: unknown): MarkdownNode | undefined { - return toParentNodes(parent).find( - (ancestor) => ancestor?.type === "ordered_list" || ancestor?.type === "bullet_list", - ); -} - -function getOrderedListItemIndex(node: MarkdownNode, listParent: MarkdownNode): number { - if (typeof node.index === "number" && Number.isFinite(node.index) && node.index >= 0) { - return node.index; - } - - if (Array.isArray(listParent.children)) { - const fallbackIndex = listParent.children.indexOf(node); - if (fallbackIndex >= 0) { - return fallbackIndex; - } - } - - return 0; -} - -function parseOrderedListStart(node: MarkdownNode): number { - const rawStart = node.attributes?.start; - if (typeof rawStart === "number" && Number.isFinite(rawStart)) { - return rawStart; - } - - if (typeof rawStart === "string") { - const parsed = Number.parseInt(rawStart, 10); - if (Number.isFinite(parsed)) { - return parsed; - } - } - - return 1; -} - -export function getMarkdownNextSiblingType( - node: MarkdownNode, - parent: unknown, -): string | undefined { - const ancestors = toParentNodes(parent); - for (let i = ancestors.length - 1; i >= 0; i--) { - const ancestor = ancestors[i]; - if (!Array.isArray(ancestor?.children)) continue; - const idx = ancestor.children.indexOf(node); - if (idx >= 0) { - return ancestor.children[idx + 1]?.type; - } - } - return undefined; -} - -function isListType(type: string | undefined): boolean { - return type === "bullet_list" || type === "ordered_list"; -} - -function hasListItemAncestor(parent: unknown): boolean { - return toParentNodes(parent).some((ancestor) => ancestor?.type === "list_item"); -} - -export function getMarkdownListSpacing( - node: MarkdownNode, - parent: unknown, -): { marginTop: number; marginBottom: number } { - if (hasListItemAncestor(parent)) { - return { - marginTop: MARKDOWN_LIST_MARGIN_TOP, - marginBottom: MARKDOWN_NESTED_LIST_MARGIN_BOTTOM, - }; - } - - const nextType = getMarkdownNextSiblingType(node, parent); - if (!nextType) { - return { - marginTop: MARKDOWN_LIST_MARGIN_TOP, - marginBottom: MARKDOWN_TERMINAL_LIST_MARGIN_BOTTOM, - }; - } - - return { - marginTop: MARKDOWN_LIST_MARGIN_TOP, - marginBottom: isListType(nextType) - ? MARKDOWN_LIST_MARGIN_BOTTOM_TO_LIST - : MARKDOWN_LIST_MARGIN_BOTTOM_TO_PROSE, - }; -} - -export function getMarkdownListMarker( - node: MarkdownNode, - parent: unknown, -): { - isOrdered: boolean; - marker: string; -} { - const listParent = getNearestListParent(parent); - if (!listParent || listParent.type !== "ordered_list") { - return { - isOrdered: false, - marker: LIST_BULLET, - }; - } - - const orderedIndex = getOrderedListItemIndex(node, listParent); - const orderedStart = parseOrderedListStart(listParent); - const orderedMarkup = - typeof node.markup === "string" && node.markup.length > 0 - ? node.markup - : DEFAULT_ORDERED_LIST_MARKUP; - - return { - isOrdered: true, - marker: `${orderedStart + orderedIndex}${orderedMarkup}`, - }; -} diff --git a/packages/app/src/utils/split-markdown-blocks.ts b/packages/app/src/utils/split-markdown-blocks.ts deleted file mode 100644 index 8282fc3ca9..0000000000 --- a/packages/app/src/utils/split-markdown-blocks.ts +++ /dev/null @@ -1,92 +0,0 @@ -import MarkdownIt from "markdown-it"; - -// Only block maps are needed here; inline parsing belongs to each rendered block. -const markdownBlockParser = new MarkdownIt(); -markdownBlockParser.core.ruler.disable("inline"); - -// The renderer decides what counts as a definition, so ask the same parser: a block -// that produces no tokens but registers references is nothing but definitions. -function isLinkReferenceDefinitionBlock(block: string): boolean { - const env: { references?: Record } = {}; - const tokens = markdownBlockParser.parse(block, env); - return tokens.length === 0 && Object.keys(env.references ?? {}).length > 0; -} - -/** - * Definitions render nothing and resolve nothing on their own, so a block made only of - * them would paint an empty row and break every reference that pointed at it. Fold it - * into the block it belongs to: the one above, or the one below when it leads. - */ -function foldLinkReferenceDefinitions(blocks: string[]): string[] { - const folded: string[] = []; - let leading: string[] = []; - for (const block of blocks) { - if (isLinkReferenceDefinitionBlock(block)) { - if (folded.length > 0) folded[folded.length - 1] += `\n\n${block}`; - else leading.push(block); - continue; - } - folded.push([...leading, block].join("\n\n")); - leading = []; - } - if (leading.length > 0) folded.push(leading.join("\n\n")); - return folded; -} - -export function splitMarkdownBlocks(text: string): string[] { - if (text.length === 0) { - return []; - } - - const blocks: string[] = []; - let currentLines: string[] = []; - let sawBlockSeparator = false; - const lines = text.split("\n"); - const structuralBlankLines = getStructuralBlankLines(text, lines); - - for (const [index, line] of lines.entries()) { - const isBlankLine = line.trim().length === 0; - - if (isBlankLine && structuralBlankLines.has(index)) { - currentLines.push(line); - continue; - } - - if (isBlankLine) { - if (currentLines.length > 0) { - sawBlockSeparator = true; - } - continue; - } - - if (sawBlockSeparator) { - blocks.push(currentLines.join("\n")); - currentLines = []; - sawBlockSeparator = false; - } - - currentLines.push(line); - } - - if (currentLines.length > 0) { - blocks.push(currentLines.join("\n")); - } - - return foldLinkReferenceDefinitions(blocks.filter((block) => block.length > 0)); -} - -function getStructuralBlankLines(text: string, lines: string[]): Set { - const blankLines = new Set(); - for (const token of markdownBlockParser.parse(text, {})) { - if (token.level !== 0 || !token.map) { - continue; - } - const [start, end] = token.map; - for (let index = start; index < end - 1; index += 1) { - if (lines[index]?.trim().length === 0) { - blankLines.add(index); - } - } - } - return blankLines; -} diff --git a/packages/app/src/utils/streaming-markdown/index.ts b/packages/app/src/utils/streaming-markdown/index.ts deleted file mode 100644 index b70b464d76..0000000000 --- a/packages/app/src/utils/streaming-markdown/index.ts +++ /dev/null @@ -1,153 +0,0 @@ -import type MarkdownIt from "markdown-it"; -import type StateInline from "markdown-it/lib/rules_inline/state_inline.mjs"; - -const STREAMING_TAIL = Symbol("streaming markdown tail"); - -/** Adds provisional inline formatting without changing the source or literal code blocks. */ -export function enableStreamingMarkdown(parser: MarkdownIt): void { - parser.core.ruler.at("inline", (state) => { - const tail = state.tokens.findLast((token) => token.nesting !== -1); - for (const token of state.tokens) { - if (token.type === "inline") { - const env = { ...state.env, [STREAMING_TAIL]: token === tail }; - token.children = []; - state.md.inline.parse(token.content, state.md, env, token.children); - } - } - }); - parser.inline.ruler.before("backticks", "streaming_code", completeCode); - parser.inline.ruler.before("strikethrough", "streaming_marker", hideOpeningMarker); - parser.inline.ruler.after("image", "streaming_link", completeLink); - parser.inline.ruler2.after("balance_pairs", "streaming_emphasis", completeEmphasis); - parser.core.ruler.after("linkify", "streaming_link_text", (state) => { - for (const block of state.tokens) { - for (const token of block.children ?? []) { - if (token.type === "streaming_link_text") token.type = "text"; - } - } - }); -} - -function isStreamingTail(state: StateInline): boolean { - return state.env[STREAMING_TAIL] === true && state.posMax === state.src.length; -} - -function hideOpeningMarker(state: StateInline, silent: boolean): boolean { - if (silent || !isStreamingTail(state)) return false; - if (!/^(?:\*{1,3}|_{1,3}|~{1,2})$/.test(state.src.slice(state.pos))) return false; - if (state.scanDelims(state.pos, state.src[state.pos] !== "_").can_close) return false; - state.pos = state.posMax; - return true; -} - -function completeCode(state: StateInline, silent: boolean): boolean { - if (silent || !isStreamingTail(state) || state.src[state.pos] !== "`") return false; - const runs = /`+/g; - runs.lastIndex = state.pos; - const opening = runs.exec(state.src)!; - let closing: RegExpExecArray | null; - while ((closing = runs.exec(state.src))) { - if (closing[0].length === opening[0].length) return false; - } - let content = state.src.slice(state.pos + opening[0].length); - // A shorter trailing run may be the first characters of the closing marker. - const partialClosing = content.match(/`+$/); - if (partialClosing && partialClosing[0].length < opening[0].length) { - content = content.slice(0, -partialClosing[0].length); - } - if (content) { - const token = state.push("code_inline", "code", 0); - token.markup = opening[0]; - token.content = content.replace(/\n/g, " ").replace(/^ (.+) $/, "$1"); - } - state.pos = state.posMax; - return true; -} - -function completeLink(state: StateInline, silent: boolean): boolean { - if (silent || !isStreamingTail(state)) return false; - const image = state.src[state.pos] === "!"; - const start = state.pos + (image ? 1 : 0); - if (state.src[start] !== "[") return false; - const labelEnd = state.md.helpers.parseLinkLabel(state, start, false); - if (labelEnd !== -1 && labelEnd + 1 < state.posMax) { - if (state.src[labelEnd + 1] !== "(" || hasClosedDestination(state.src, labelEnd + 1)) { - return false; - } - } - const end = state.posMax; - if (!image) { - if (state.pending) state.pushPending(); - const firstLabelToken = state.tokens.length; - state.pos = start + 1; - state.posMax = labelEnd === -1 ? end : labelEnd; - state.level++; - state.md.inline.tokenize(state); - state.level--; - // Keep URL-shaped labels out of the automatic linkifier until the real - // destination arrives. Restore ordinary text before tokens reach the view. - for (let index = firstLabelToken; index < state.tokens.length; index++) { - if (state.tokens[index].type === "text") state.tokens[index].type = "streaming_link_text"; - } - } - state.pos = state.posMax = end; - return true; -} - -function hasClosedDestination(source: string, start: number): boolean { - let depth = 0; - let quote = ""; - for (let index = start; index < source.length; index++) { - const char = source[index]; - if (char === "\\") { - index++; - } else if (quote) { - if (char === quote) quote = ""; - } else if (char === "<") { - quote = ">"; - } else if ((char === '"' || char === "'") && /\s/.test(source[index - 1])) { - quote = char; - } else if (char === "(") { - depth++; - } else if (char === ")" && --depth === 0) { - return true; - } - } - return false; -} - -function completeEmphasis(state: StateInline): boolean { - if (!isStreamingTail(state)) return false; - const open = []; - let closedThrough = -1; - // Reuse markdown-it's escape, word-boundary, nesting and delimiter matching - // decisions. Only unmatched openers outside already-closed spans reach EOF. - for (const [index, delimiter] of state.delimiters.entries()) { - if (delimiter.end !== -1) { - closedThrough = Math.max(closedThrough, delimiter.end); - } else if (index > closedThrough && delimiter.open) { - open.push(delimiter); - } - } - for (const delimiter of open.toReversed()) { - if (delimiter.marker === 126 && /(? + + + + + + diff --git a/packages/expo-two-way-audio/android/src/androidTest/java/expo/modules/twowayaudio/AudioEngineTest.kt b/packages/expo-two-way-audio/android/src/androidTest/java/expo/modules/twowayaudio/AudioEngineTest.kt new file mode 100644 index 0000000000..ad840deb20 --- /dev/null +++ b/packages/expo-two-way-audio/android/src/androidTest/java/expo/modules/twowayaudio/AudioEngineTest.kt @@ -0,0 +1,223 @@ +package expo.modules.twowayaudio + +import AudioEngine +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.media.AudioAttributes +import android.media.AudioFocusRequest +import android.media.AudioManager +import android.os.Handler +import android.os.Looper +import android.os.SystemClock +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.filters.SdkSuppress +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.atomic.AtomicInteger + +class AudioEngineTestActivity : Activity() + +/** Real AudioManager and AudioRecord: focus loss is delivered by Android, never injected. */ +@RunWith(AndroidJUnit4::class) +class AudioEngineTest { + private val instrumentation = InstrumentationRegistry.getInstrumentation() + private lateinit var activity: Activity + private lateinit var engine: AudioEngine + private lateinit var audio: AudioManager + private val packets = AtomicInteger() + private val events = CopyOnWriteArrayList() + private val requests = mutableListOf() + + @Before fun setUp() { + val context = instrumentation.context + shell("pm grant ${context.packageName} android.permission.RECORD_AUDIO") + activity = instrumentation.startActivitySync( + Intent(context, AudioEngineTestActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + ) + instrumentation.waitForIdleSync() + audio = activity.getSystemService(Context.AUDIO_SERVICE) as AudioManager + main { + engine = AudioEngine(activity) + engine.onMicDataCallback = { packets.incrementAndGet() } + engine.onAudioInterruptionCallback = { events.add(it) } + } + } + + @After fun tearDown() { + shell("cmd sensor_privacy disable 0 microphone") + main { + requests.forEach { audio.abandonAudioFocusRequest(it) } + engine.tearDown() + activity.finish() + } + } + + @Test fun dictationContinuesWhenMusicTakesFocus() { + assertDictationSurvives(AudioManager.AUDIOFOCUS_GAIN) + } + + @Test fun dictationContinuesWhenFocusIsTemporarilyLost() { + assertDictationSurvives(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT) + } + + @Test fun dictationContinuesWhenAnotherAppRequestsDucking() { + assertDictationSurvives(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK) + } + + @Test fun conversationStillStopsOnFocusLoss() { + main { assertTrue(engine.toggleRecording(true)) } + await("conversation microphone data") { packets.get() > 2 } + takeFocus(AudioManager.AUDIOFOCUS_GAIN) + await("terminal conversation interruption") { events.contains("blocked") } + assertFalse(engine.isRecording) + } + + @Test fun dictationStartsWhileMusicOwnsFocusAndCanBeStartedAgain() { + takeFocus(AudioManager.AUDIOFOCUS_GAIN) + startDictation() + main { engine.toggleRecording(false); engine.releaseAudioSession() } + val stoppedAt = packets.get() + startDictation() + await("new recording data") { packets.get() > stoppedAt + 2 } + assertFalse(events.contains("blocked")) + } + + @Test fun cancellingWhileBackgroundedDoesNotRestartOnResume() { + startDictation() + main { + engine.pauseRecordingAndPlayer() + engine.toggleRecording(false) + engine.releaseAudioSession() + engine.resumeRecordingAndPlayer() + } + assertFalse(engine.isRecording) + } + + @Test fun dictationKeepsItsCaptureModeAfterBackgroundResume() { + startDictation() + main { engine.pauseRecordingAndPlayer(); engine.resumeRecordingAndPlayer() } + val resumedAt = packets.get() + takeFocus(AudioManager.AUDIOFOCUS_GAIN) + await("resumed dictation data after focus loss") { packets.get() > resumedAt + 3 } + assertTrue(engine.isRecording) + assertFalse(events.contains("blocked")) + } + + @Test @SdkSuppress(minSdkVersion = 31) + fun microphonePrivacyInterruptionPreservesCaptureAndRecovers() { + startDictation() + shell("cmd sensor_privacy enable 0 microphone") + await("input paused") { events.contains("capture_paused") } + assertTrue(engine.isRecording) + assertFalse(events.contains("blocked")) + // Once Android reports silencing, do not enqueue artificial silence for transcription. + SystemClock.sleep(100) + val pausedAt = packets.get() + SystemClock.sleep(150) + assertEquals(pausedAt, packets.get()) + shell("cmd sensor_privacy disable 0 microphone") + await("input resumed") { events.contains("capture_resumed") } + await("microphone data after recovery") { packets.get() > pausedAt + 3 } + assertTrue(engine.isRecording) + assertEquals(listOf("capture_paused", "capture_resumed"), events.toList()) + } + + @Test @SdkSuppress(minSdkVersion = 31) + fun startingWithMicrophonePrivacyEnabledWaitsAndRecovers() { + shell("cmd sensor_privacy enable 0 microphone") + main { assertTrue(engine.startDictation()) } + await("initial input paused") { events.contains("capture_paused") } + assertTrue(engine.isRecording) + assertFalse(events.contains("blocked")) + shell("cmd sensor_privacy disable 0 microphone") + await("initial input resumed") { events.contains("capture_resumed") } + await("microphone data") { packets.get() > 3 } + } + + @Test fun resumingDictationDoesNotTakeFocusBackFromMusic() { + startDictation() + main { engine.pauseRecordingAndPlayer() } + val changes = CopyOnWriteArrayList() + val request = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) + .setOnAudioFocusChangeListener({ changes.add(it) }, Handler(Looper.getMainLooper())) + .build() + requests.add(request) + main { assertEquals(AudioManager.AUDIOFOCUS_REQUEST_GRANTED, audio.requestAudioFocus(request)) } + val before = packets.get() + main { engine.resumeRecordingAndPlayer() } + await("resumed microphone data") { packets.get() > before + 3 } + assertTrue(engine.isRecording) + assertTrue("Music lost focus on dictation resume: $changes", changes.isEmpty()) + } + + @Test @SdkSuppress(minSdkVersion = 31) + fun stoppingDictationReleasesTheCommunicationRoute() { + startDictation() + await("selected communication route") { hasCommunicationRoute() } + main { engine.toggleRecording(false); engine.releaseAudioSession() } + await("normal audio mode") { audio.mode == AudioManager.MODE_NORMAL } + await("cleared communication route") { !hasCommunicationRoute() } + assertFalse(engine.isRecording) + } + + private fun assertDictationSurvives(gain: Int) { + startDictation() + val before = packets.get() + val request = takeFocus(gain) + await("microphone data while focus is lost") { packets.get() > before + 3 } + assertTrue(engine.isRecording) + assertFalse(events.contains("blocked")) + val during = packets.get() + main { audio.abandonAudioFocusRequest(request) } + await("microphone data after focus release") { packets.get() > during + 3 } + assertTrue(engine.isRecording) + assertFalse(events.contains("blocked")) + } + + private fun startDictation() { + val before = packets.get() + main { assertTrue(engine.startDictation()) } + await("initial microphone data") { packets.get() > before + 3 } + } + + private fun takeFocus(gain: Int): AudioFocusRequest { + val request = AudioFocusRequest.Builder(gain) + .setAudioAttributes(AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC).build()) + .setOnAudioFocusChangeListener({}, Handler(Looper.getMainLooper())) + .build() + requests.add(request) + main { assertEquals(AudioManager.AUDIOFOCUS_REQUEST_GRANTED, audio.requestAudioFocus(request)) } + return request + } + + private fun main(action: () -> Unit) = instrumentation.runOnMainSync(action) + + private fun await(description: String, condition: () -> Boolean) { + val deadline = SystemClock.elapsedRealtime() + 5000 + while (!condition() && SystemClock.elapsedRealtime() < deadline) SystemClock.sleep(20) + assertTrue("Timed out waiting for $description; events=$events packets=${packets.get()}", condition()) + } + + // communicationDevice reports the effective system device even after our selection is cleared. + // Inspect only current route clients, never the historical routing event log. + private fun hasCommunicationRoute(): Boolean { + val dump = shell("dumpsys audio") + assertTrue("Missing Android communication route diagnostics", dump.contains("Communication route clients:")) + return dump.lineSequence().any { + it.trimStart().startsWith("[CommunicationRouteClient: mUid: ${android.os.Process.myUid()} ") + } + } + + private fun shell(command: String): String = + instrumentation.uiAutomation.executeShellCommand(command).use { + android.os.ParcelFileDescriptor.AutoCloseInputStream(it).readBytes().toString(Charsets.UTF_8) + } +} diff --git a/packages/expo-two-way-audio/android/src/main/java/expo/modules/twowayaudio/AudioEngine.kt b/packages/expo-two-way-audio/android/src/main/java/expo/modules/twowayaudio/AudioEngine.kt index a6fe25303e..48aff1f1df 100644 --- a/packages/expo-two-way-audio/android/src/main/java/expo/modules/twowayaudio/AudioEngine.kt +++ b/packages/expo-two-way-audio/android/src/main/java/expo/modules/twowayaudio/AudioEngine.kt @@ -6,12 +6,14 @@ import android.media.AudioFocusRequest import android.media.AudioFormat import android.media.AudioManager import android.media.AudioRecord +import android.media.AudioRecordingConfiguration import android.media.AudioTrack import android.media.MediaRecorder import android.media.audiofx.AcousticEchoCanceler import android.media.audiofx.NoiseSuppressor import android.os.Build -import android.os.PowerManager +import android.os.Handler +import android.os.Looper import android.util.Log import androidx.annotation.RequiresApi import java.util.LinkedList @@ -34,7 +36,26 @@ class AudioEngine (context: Context) { private var noiseSuppressor: NoiseSuppressor? = null private val executorServiceMicrophone = Executors.newFixedThreadPool(1) private val executorServicePlayback = Executors.newFixedThreadPool(1) - private var speakerDevice: AudioDeviceInfo? = null + private enum class CaptureMode { CONVERSATION, DICTATION } + private var captureMode = CaptureMode.CONVERSATION + private val mainHandler = Handler(Looper.getMainLooper()) + @Volatile private var captureSilenced = false + private val recordingCallback = object : AudioManager.AudioRecordingCallback() { + override fun onRecordingConfigChanged(configs: MutableList) { + if (!isRecording || !::audioRecord.isInitialized) return + val config = configs.firstOrNull { it.clientAudioSessionId == audioRecord.audioSessionId } + ?: return + setCaptureSilenced(config.isClientSilenced) + } + } + private val deviceCallback = object : android.media.AudioDeviceCallback() { + override fun onAudioDevicesAdded(devices: Array) { + if (communicationRouteActive) updateAudioRouting() + } + override fun onAudioDevicesRemoved(devices: Array) { + if (communicationRouteActive) updateAudioRouting() + } + } private var communicationRouteActive = false private var bridgeWindowStartedAtMs = System.currentTimeMillis() private var micEvents = 0 @@ -44,7 +65,7 @@ class AudioEngine (context: Context) { private var playbackWrites = 0 private var playbackWriteBytes = 0L - var isRecording = false + @Volatile var isRecording = false private var isRecordingBeforePause = false var isPlaying = false @@ -83,28 +104,9 @@ class AudioEngine (context: Context) { @SuppressLint("NewApi") private fun initializeAudio(context:Context) { audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager - activateCommunicationRoute() - if (!requestAudioFocus()) { - handleAudioFocusBlocked() - } - - // Listen for changes in audio routing - audioManager.registerAudioDeviceCallback(object:android.media.AudioDeviceCallback(){ - override fun onAudioDevicesAdded(addedDevices: Array?) { - Log.d("AudioEngine", "onAudioDevicesAdded") - super.onAudioDevicesAdded(addedDevices) - if (communicationRouteActive) { - updateAudioRouting() - } - } - override fun onAudioDevicesRemoved(removedDevices: Array?) { - Log.d("AudioEngine", "onAudioDevicesRemoved") - super.onAudioDevicesRemoved(removedDevices) - if (communicationRouteActive) { - updateAudioRouting() - } - } - }, null) + // Initialization must not interrupt music before a capture/playback operation owns a session. + audioManager.registerAudioDeviceCallback(deviceCallback, mainHandler) + audioManager.registerAudioRecordingCallback(recordingCallback, mainHandler) val bufferSize = AudioTrack.getMinBufferSize( SAMPLE_RATE, @@ -125,49 +127,42 @@ class AudioEngine (context: Context) { bufferSize, AudioTrack.MODE_STREAM, audioManager.generateAudioSessionId() - ).apply { - play() - } + ) } + @SuppressLint("NewApi") private fun updateAudioRouting() { - val devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS) - var isExternalDeviceConnected = false - var selectedDevice: AudioDeviceInfo? = null - - for (device in devices) { - if (device.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) { - speakerDevice = device - } - if (device.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES || - device.type == AudioDeviceInfo.TYPE_WIRED_HEADSET || - device.type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP || - device.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO) { - isExternalDeviceConnected = true - selectedDevice = device - break - } else if (device.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) { - selectedDevice = device - } - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - // Use the modern API for Android S and above - try { - selectedDevice?.let { - audioManager.setCommunicationDevice(it) - } - }catch (e:Exception){ - Log.e("AudioEngine", "Error setting communication device. Using speaker") - speakerDevice?.let { - audioManager.setCommunicationDevice(it) - } + val devices = audioManager.availableCommunicationDevices + // A2DP is a music output, not a microphone route. Android chooses the matching + // input for a valid communication sink (including SCO and BLE headsets). + val selected = devices.firstOrNull { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO || + it.type == AudioDeviceInfo.TYPE_BLE_HEADSET } + ?: devices.firstOrNull { it.type == AudioDeviceInfo.TYPE_WIRED_HEADSET || + it.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES || + it.type == AudioDeviceInfo.TYPE_USB_HEADSET } + ?: devices.firstOrNull { it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER } + val routed = try { + selected != null && audioManager.setCommunicationDevice(selected) + } catch (_: IllegalArgumentException) { + // The device disconnected between enumeration and selection. + false } - + if (!routed) audioManager.clearCommunicationDevice() } else { - // Fall back to deprecated method for older Android versions @Suppress("DEPRECATION") - audioManager.isSpeakerphoneOn = !isExternalDeviceConnected + val devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS) + // Before Android 12, A2DP signals a connected headset; start SCO to open its mic. + val bluetooth = devices.any { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO || + it.type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP } + @Suppress("DEPRECATION") + if (bluetooth) audioManager.startBluetoothSco() else audioManager.stopBluetoothSco() + @Suppress("DEPRECATION") + audioManager.isSpeakerphoneOn = !bluetooth && !devices.any { + it.type == AudioDeviceInfo.TYPE_WIRED_HEADSET || + it.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES || + it.type == AudioDeviceInfo.TYPE_USB_HEADSET + } } } @@ -186,6 +181,8 @@ class AudioEngine (context: Context) { } else { @Suppress("DEPRECATION") audioManager.isSpeakerphoneOn = false + @Suppress("DEPRECATION") + audioManager.stopBluetoothSco() } audioManager.mode = AudioManager.MODE_NORMAL } @@ -245,8 +242,8 @@ class AudioEngine (context: Context) { audioFocusRequest = null } - val focusRequest = - AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) + lateinit var focusRequest: AudioFocusRequest + focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) .setAudioAttributes( AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) @@ -255,6 +252,7 @@ class AudioEngine (context: Context) { ) .setAcceptsDelayedFocusGain(true) .setOnAudioFocusChangeListener { focusChange -> + if (audioFocusRequest !== focusRequest) return@setOnAudioFocusChangeListener when (focusChange) { AudioManager.AUDIOFOCUS_GAIN -> { Log.d("AudioEngine", "Audio focus gained") @@ -286,6 +284,14 @@ class AudioEngine (context: Context) { } private fun handleAudioFocusBlocked() { + // Focus governs playback. Dictation keeps its AudioRecord alive; Android's + // recording configuration reports real input silencing and restores it after calls. + if (captureMode == CaptureMode.DICTATION) { + stopPlayback() + audioFocusRequest?.let { audioManager.abandonAudioFocusRequest(it) } + audioFocusRequest = null + return + } if (isRecording) { stopRecording() } @@ -306,11 +312,11 @@ class AudioEngine (context: Context) { @RequiresApi(Build.VERSION_CODES.Q) @SuppressLint("MissingPermission") - private fun startRecording(): Boolean { + private fun startRecording(requestFocus: Boolean = true): Boolean { activateCommunicationRoute() - if (!requestAudioFocus()) { + if (requestFocus && !requestAudioFocus()) { handleAudioFocusBlocked() - return false + if (captureMode == CaptureMode.CONVERSATION) return false } val bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT) @@ -344,17 +350,27 @@ class AudioEngine (context: Context) { audioRecord.startRecording() isRecording = true - startMicSampleTap() + setCaptureSilenced(audioRecord.activeRecordingConfiguration?.isClientSilenced ?: false) + startMicSampleTap(audioRecord) return true } - private fun startMicSampleTap(){ + private fun setCaptureSilenced(silenced: Boolean) { + if (captureSilenced == silenced) return + captureSilenced = silenced + if (silenced) onInputVolumeCallback?.invoke(0.0F) + onAudioInterruptionCallback?.invoke(if (silenced) "capture_paused" else "capture_resumed") + } + + private fun startMicSampleTap(recorder: AudioRecord){ executorServiceMicrophone.execute { val buffer = ByteArray(1024) try { - while (isRecording) { - val read = audioRecord.read(buffer, 0, buffer.size) - if (read > 0) { + while (isRecording && audioRecord === recorder) { + val read = recorder.read(buffer, 0, buffer.size) + if (!isRecording || audioRecord !== recorder) break + if (read < 0) throw IllegalStateException("AudioRecord read failed: $read") + if (read > 0 && !captureSilenced) { val data = buffer.copyOf(read) micEvents += 1 micBytes += data.size.toLong() @@ -367,9 +383,13 @@ class AudioEngine (context: Context) { Log.d("AudioEngine", "Mic sample tap stopped.") }catch (e: Exception){ Log.e("AudioEngine", "Error reading mic sample data", e) - isRecording = false - tearDown() - throw e + mainHandler.post { + if (isRecording && audioRecord === recorder) { + stopRecording() + releaseAudioSession() + onAudioInterruptionCallback?.invoke("blocked") + } + } } } } @@ -379,16 +399,31 @@ class AudioEngine (context: Context) { isRecording = false if (audioRecord.recordingState == AudioRecord.RECORDSTATE_RECORDING) { audioRecord.stop() - audioRecord.release() } + audioRecord.release() + echoCanceler?.release() + echoCanceler = null + noiseSuppressor?.release() + noiseSuppressor = null + setCaptureSilenced(false) onInputVolumeCallback?.invoke(0.0F) } + @RequiresApi(Build.VERSION_CODES.Q) + fun startDictation(): Boolean { + isRecordingBeforePause = false + if (isRecording) return true + captureMode = CaptureMode.DICTATION + return startRecording() + } + @RequiresApi(Build.VERSION_CODES.Q) fun toggleRecording(value: Boolean): Boolean { + isRecordingBeforePause = false if (value == isRecording) return isRecording if (value) { + captureMode = CaptureMode.CONVERSATION return startRecording() } else { stopRecording() @@ -401,6 +436,7 @@ class AudioEngine (context: Context) { @SuppressLint("NewApi") fun playPCMData(data: ByteArray) { acquireAudioSessionIfNeeded() + audioTrack.play() audioSampleQueue.add(data) playbackEvents += 1 playbackQueuedBytes += data.size.toLong() @@ -464,7 +500,7 @@ class AudioEngine (context: Context) { @RequiresApi(Build.VERSION_CODES.Q) fun pauseRecordingAndPlayer() { isRecordingBeforePause = isRecording - isRecording = toggleRecording(false) + stopRecording() audioTrack.pause() } @@ -478,16 +514,18 @@ class AudioEngine (context: Context) { if (!wasRecordingBeforePause && !isPlaying) { return } - activateCommunicationRoute() - if (!requestAudioFocus()) { - handleAudioFocusBlocked() - return + isRecordingBeforePause = false + if (wasRecordingBeforePause) { + // Returning from music controls must not pause music the user explicitly resumed. + if (!startRecording(requestFocus = captureMode == CaptureMode.CONVERSATION)) return + } else { + activateCommunicationRoute() + if (!requestAudioFocus()) { + handleAudioFocusBlocked() + return + } } - isRecording = toggleRecording(wasRecordingBeforePause) - if (wasRecordingBeforePause && !isRecording) { - return - } - audioTrack.play() + if (captureMode == CaptureMode.CONVERSATION) audioTrack.play() } fun stopPlayback() { @@ -520,7 +558,11 @@ class AudioEngine (context: Context) { audioManager.abandonAudioFocusRequest(request) } audioFocusRequest = null + audioManager.unregisterAudioDeviceCallback(deviceCallback) + audioManager.unregisterAudioRecordingCallback(recordingCallback) + audioTrack.release() executorServiceMicrophone.shutdownNow() + executorServicePlayback.shutdownNow() } diff --git a/packages/expo-two-way-audio/android/src/main/java/expo/modules/twowayaudio/ExpoTwoWayAudioModule.kt b/packages/expo-two-way-audio/android/src/main/java/expo/modules/twowayaudio/ExpoTwoWayAudioModule.kt index f9b107c99f..68f1489de7 100644 --- a/packages/expo-two-way-audio/android/src/main/java/expo/modules/twowayaudio/ExpoTwoWayAudioModule.kt +++ b/packages/expo-two-way-audio/android/src/main/java/expo/modules/twowayaudio/ExpoTwoWayAudioModule.kt @@ -42,6 +42,12 @@ class ExpoTwoWayAudioModule : Module() { audioEngine?.isRecording ?: false } + Function("startDictation") { + val isRecording = audioEngine?.startDictation() ?: false + sendEvent(ON_RECORDING_CHANGE_EVENT, mapOf("data" to isRecording)) + isRecording + } + Function("toggleRecording") { value: Boolean -> audioEngine?.let { engine -> val isRecording = engine.toggleRecording(value) diff --git a/packages/expo-two-way-audio/ios/ExpoTwoWayAudioModule.swift b/packages/expo-two-way-audio/ios/ExpoTwoWayAudioModule.swift index baec747995..046f856747 100644 --- a/packages/expo-two-way-audio/ios/ExpoTwoWayAudioModule.swift +++ b/packages/expo-two-way-audio/ios/ExpoTwoWayAudioModule.swift @@ -59,6 +59,12 @@ public class ExpoTwoWayAudioModule: Module { return audioEngine.isRecording } + Function("startDictation") { () -> Bool in + let isRecording = self.audioEngine?.toggleRecording(true) ?? false + self.sendEvent(ON_RECORDING_CHANGE_EVENT_NAME, ["data": isRecording]) + return isRecording + } + Function("toggleRecording") { (val: Bool) -> Bool in guard let audioEngine = self.audioEngine else { print("AudioEngine not initialized") diff --git a/packages/expo-two-way-audio/package.json b/packages/expo-two-way-audio/package.json index 1e6bd41230..9e49135e39 100644 --- a/packages/expo-two-way-audio/package.json +++ b/packages/expo-two-way-audio/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/expo-two-way-audio", - "version": "0.11.2", + "version": "0.12.0-beta.1", "description": "Native module for two way audio streaming", "keywords": [ "ExpoTwoWayAudio", diff --git a/packages/expo-two-way-audio/src/core.ts b/packages/expo-two-way-audio/src/core.ts index 1a54e0e9ce..48215f2bae 100644 --- a/packages/expo-two-way-audio/src/core.ts +++ b/packages/expo-two-way-audio/src/core.ts @@ -17,6 +17,16 @@ export function toggleRecording(val: boolean): boolean { return ExpoTwoWayAudioModule.toggleRecording(val); } +/** Capture speech without treating another app's playback focus as a microphone failure. */ +export function startDictation(): boolean { + // COMPAT(dictationCapture): added after v0.11.2, remove after 2027-01-16 once + // the native binary floor includes startDictation. Older OTA clients retain legacy capture. + if (typeof ExpoTwoWayAudioModule.startDictation !== "function") { + return ExpoTwoWayAudioModule.toggleRecording(true); + } + return ExpoTwoWayAudioModule.startDictation(); +} + export function isRecording(): boolean { return ExpoTwoWayAudioModule.isRecording(); } diff --git a/packages/highlight/package.json b/packages/highlight/package.json index 02e9e28daa..2e3a55146c 100644 --- a/packages/highlight/package.json +++ b/packages/highlight/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/highlight", - "version": "0.11.2", + "version": "0.12.0-beta.1", "files": [ "dist", "!dist/**/*.map", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 2797c32211..a7354bc086 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/plugin", - "version": "0.11.2", + "version": "0.12.0-beta.1", "description": "Paseo plugin SDK package", "files": [ "dist", @@ -70,8 +70,8 @@ "use-sync-external-store": "^1.6.0" }, "devDependencies": { - "@getpaseo/client": "0.11.2", - "@getpaseo/protocol": "0.11.2", + "@getpaseo/client": "0.12.0-beta.1", + "@getpaseo/protocol": "0.12.0-beta.1", "@types/node": "^20.9.0", "@types/react": "~19.2.0", "react": "19.1.0", @@ -81,8 +81,8 @@ "zod": "^4.4.3" }, "peerDependencies": { - "@getpaseo/client": "0.11.2", - "@getpaseo/protocol": "0.11.2", + "@getpaseo/client": "0.12.0-beta.1", + "@getpaseo/protocol": "0.12.0-beta.1", "react": "~19.1.0", "react-native": ">=0.81.5", "zod": "^4.4.3" diff --git a/packages/protocol/codegen/README.md b/packages/protocol/codegen/README.md index 7b2b0bcee5..69d7ac79b6 100644 --- a/packages/protocol/codegen/README.md +++ b/packages/protocol/codegen/README.md @@ -1,7 +1,7 @@ # Protocol Validator Codegen -This directory is build-time only. `ws-outbound.compile.ts` is the zod-aot discovery entry for the inbound WebSocket validator. +This directory is build-time only. The generator writes `generated/ws-outbound.compile.ts`, the zod-aot discovery entry for the inbound WebSocket validators: one for the envelope and one per session message type. -The generated runtime file is written to `../src/generated/validation/ws-outbound.aot.ts` and is not committed. The protocol package owns every generation trigger through its npm lifecycle scripts. +The discovery entry and the generated runtime files under `../src/generated/validation/` are not committed. The protocol package owns every generation trigger through its npm lifecycle scripts. `zod-aot` is exact-pinned, and the protocol generator applies the small compiler patches it requires before generation. Treat changes to those patches like compiler changes: regenerate, inspect the output, and run the protocol validation regression tests before shipping. diff --git a/packages/protocol/codegen/ws-outbound.compile.ts b/packages/protocol/codegen/ws-outbound.compile.ts deleted file mode 100644 index 589986cf6a..0000000000 --- a/packages/protocol/codegen/ws-outbound.compile.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { compile } from "zod-aot"; -import { WSOutboundMessageSchema as SourceWSOutboundMessageSchema } from "../src/messages.js"; - -export const WSOutboundMessageSchema = compile(SourceWSOutboundMessageSchema); diff --git a/packages/protocol/package.json b/packages/protocol/package.json index 8ce97a1025..964ff5a51e 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/protocol", - "version": "0.11.2", + "version": "0.12.0-beta.1", "description": "Paseo shared protocol schemas and wire types", "files": [ "dist", diff --git a/packages/protocol/scripts/generate-validation-aot.mjs b/packages/protocol/scripts/generate-validation-aot.mjs index c3fd0b72ed..34c393c19e 100644 --- a/packages/protocol/scripts/generate-validation-aot.mjs +++ b/packages/protocol/scripts/generate-validation-aot.mjs @@ -4,8 +4,12 @@ import { dirname, relative, resolve, sep } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); -const source = resolve(packageRoot, "codegen/ws-outbound.compile.ts"); -const runtimeSchemaMetadata = resolve(packageRoot, "src/validation/ws-outbound-schema-metadata.ts"); +const envelopeSource = resolve(packageRoot, "src/validation/ws-outbound-envelope.ts"); +const source = resolve(packageRoot, "codegen/generated/ws-outbound.compile.ts"); +const runtimeSchemaMetadata = resolve( + packageRoot, + "src/generated/validation/ws-outbound-schema-metadata.ts", +); const output = resolve(packageRoot, "src/generated/validation/ws-outbound.aot.ts"); const require = createRequire(import.meta.url); @@ -72,16 +76,69 @@ await Promise.all([ ensureZodAotDiscriminatedUnionOutputPatch(), ]); -const [{ discoverSchemas }, { compileSchemas }, { generateCompiledFileContent }] = - await Promise.all([ - import(pathToFileURL(resolve(zodAotRoot, "dist/discovery.js")).href), - import(pathToFileURL(resolve(zodAotRoot, "dist/core/pipeline.js")).href), - import(pathToFileURL(resolve(zodAotRoot, "dist/cli/emitter.js")).href), - ]); +const [ + { loadSourceFile }, + { discoverSchemas }, + { compileSchemas }, + { generateCompiledFileContent }, +] = await Promise.all([ + import(pathToFileURL(resolve(zodAotRoot, "dist/loader.js")).href), + import(pathToFileURL(resolve(zodAotRoot, "dist/discovery.js")).href), + import(pathToFileURL(resolve(zodAotRoot, "dist/core/pipeline.js")).href), + import(pathToFileURL(resolve(zodAotRoot, "dist/cli/emitter.js")).href), +]); + +// One compiled validator per session message plus a tag-only envelope. zod-aot emits one function +// per compiled schema, so this keeps every generated function small enough for V8 to optimize cheaply. +const { sessionOutboundMessageOptions } = await loadSourceFile(envelopeSource, { cacheBust: true }); +const sessionTags = sessionOutboundMessageOptions.map((option) => option.shape.type.value); +function sessionExport(index) { + return `SessionOutboundMessage${index}`; +} +const envelopeImport = relative(dirname(source), envelopeSource) + .replace(/\.ts$/, ".js") + .split(sep) + .join("/"); +const metadataImport = relative(dirname(runtimeSchemaMetadata), envelopeSource) + .replace(/\.ts$/, ".js") + .split(sep) + .join("/"); + +await mkdir(dirname(source), { recursive: true }); +await writeFile( + source, + [ + "// AUTO-GENERATED by generate-validation-aot.mjs — DO NOT EDIT", + 'import { compile } from "zod-aot";', + `import { WSOutboundEnvelopeSchema, sessionOutboundMessageOptions } from "${envelopeImport}";`, + "export const WSOutboundEnvelope = compile(WSOutboundEnvelopeSchema);", + ...sessionTags.map( + (_, index) => + `export const ${sessionExport(index)} = compile(sessionOutboundMessageOptions[${index}]);`, + ), + "", + ].join("\n"), +); +await mkdir(dirname(runtimeSchemaMetadata), { recursive: true }); +await writeFile( + runtimeSchemaMetadata, + [ + "// AUTO-GENERATED by generate-validation-aot.mjs — DO NOT EDIT", + `import { WSOutboundEnvelopeSchema, sessionOutboundMessageOptions } from "${metadataImport}";`, + "export const WSOutboundEnvelope = { schema: WSOutboundEnvelopeSchema };", + ...sessionTags.map( + (_, index) => + `export const ${sessionExport(index)} = { schema: sessionOutboundMessageOptions[${index}] };`, + ), + "", + ].join("\n"), +); const schemas = await discoverSchemas(source, { cacheBust: true }); -if (schemas.length === 0) { - throw new Error(`No zod-aot compile() exports found in ${relative(packageRoot, source)}`); +if (schemas.length !== sessionTags.length + 1) { + throw new Error( + `Expected ${sessionTags.length + 1} zod-aot compile() exports in ${relative(packageRoot, source)}, found ${schemas.length}`, + ); } const compiled = compileSchemas(schemas, { mode: "inline" }); @@ -95,12 +152,16 @@ const content = generateCompiledFileContent(compiled, runtimeImportPath, { "// AUTO-GENERATED by zod-aot — DO NOT EDIT", "// @ts-nocheck\n// AUTO-GENERATED by zod-aot — DO NOT EDIT", ); +const sessionMessageValidators = [ + "export const SessionOutboundMessageValidators = {", + ...sessionTags.map((tag, index) => ` ${JSON.stringify(tag)}: ${sessionExport(index)},`), + "};", + "", +].join("\n"); await mkdir(dirname(output), { recursive: true }); -await writeFile(output, content); +await writeFile(output, `${content}\n${sessionMessageValidators}`); console.info( - `generated ${relative(packageRoot, output)} from ${relative(packageRoot, source)} (${schemas - .map((schema) => schema.exportName) - .join(", ")})`, + `generated ${relative(packageRoot, output)}: envelope + ${sessionTags.length} session message validators`, ); diff --git a/packages/protocol/src/generated/validation/README.md b/packages/protocol/src/generated/validation/README.md index 7d650faf4b..10ebc31e14 100644 --- a/packages/protocol/src/generated/validation/README.md +++ b/packages/protocol/src/generated/validation/README.md @@ -1,6 +1,6 @@ # Generated protocol validators -`ws-outbound.aot.ts` is generated by zod-aot from `packages/protocol/codegen/ws-outbound.compile.ts`. +`ws-outbound.aot.ts` is generated by zod-aot from `packages/protocol/codegen/generated/ws-outbound.compile.ts`, which the generator writes along with `ws-outbound-schema-metadata.ts`. Run: diff --git a/packages/protocol/src/validation/ws-outbound-envelope.ts b/packages/protocol/src/validation/ws-outbound-envelope.ts new file mode 100644 index 0000000000..0ced88e8a9 --- /dev/null +++ b/packages/protocol/src/validation/ws-outbound-envelope.ts @@ -0,0 +1,25 @@ +import { z } from "zod"; +import { + SessionOutboundMessageSchema, + WSHelloRejectedMessageSchema, + WSPongMessageSchema, +} from "../messages.js"; + +export const sessionOutboundMessageOptions = SessionOutboundMessageSchema.options; + +const [firstSessionTag, ...otherSessionTags] = sessionOutboundMessageOptions.map((option) => + z.object({ type: option.shape.type }), +); + +// WSOutboundMessageSchema with every session message reduced to its tag. It reports the same +// envelope and discriminator issues, while each session message gets its own generated validator: +// one function for the whole protocol is large enough that V8's Maglev tier spends hundreds of MB +// and seconds compiling it. +export const WSOutboundEnvelopeSchema = z.discriminatedUnion("type", [ + WSPongMessageSchema, + z.object({ + type: z.literal("session"), + message: z.discriminatedUnion("type", [firstSessionTag, ...otherSessionTags]), + }), + WSHelloRejectedMessageSchema, +]); diff --git a/packages/protocol/src/validation/ws-outbound-schema-metadata.ts b/packages/protocol/src/validation/ws-outbound-schema-metadata.ts deleted file mode 100644 index 23c9b5e563..0000000000 --- a/packages/protocol/src/validation/ws-outbound-schema-metadata.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { WSOutboundMessageSchema as SourceWSOutboundMessageSchema } from "../messages.js"; - -export const WSOutboundMessageSchema = { schema: SourceWSOutboundMessageSchema }; diff --git a/packages/protocol/src/validation/ws-outbound.ts b/packages/protocol/src/validation/ws-outbound.ts index bcb568cd8e..6cc7ad1268 100644 --- a/packages/protocol/src/validation/ws-outbound.ts +++ b/packages/protocol/src/validation/ws-outbound.ts @@ -1,19 +1,54 @@ +import { ZodRealError } from "zod"; import type { z } from "zod"; -import { WSOutboundMessageSchema } from "../generated/validation/ws-outbound.aot.js"; -import type { WSOutboundMessage } from "../messages.js"; +import { + SessionOutboundMessageValidators, + WSOutboundEnvelope, +} from "../generated/validation/ws-outbound.aot.js"; +import type { SessionOutboundMessage, WSOutboundMessage } from "../messages.js"; -type WSOutboundValidationResult = - | { success: true; data: WSOutboundMessage } - | { success: false; error: z.ZodError }; +type ValidationResult = { success: true; data: T } | { success: false; error: z.ZodError }; -interface WSOutboundGeneratedValidator { - safeParse(input: unknown): WSOutboundValidationResult; +interface GeneratedValidator { + safeParse(input: unknown): ValidationResult; } -// zod-aot emits runtime JavaScript from WSOutboundMessageSchema but not its TypeScript surface. +// zod-aot emits runtime JavaScript from the envelope and message schemas but not its TypeScript surface. // Protocol regression tests cover the patched compiler cases behind this boundary. -const wsOutboundValidator = WSOutboundMessageSchema as WSOutboundGeneratedValidator; +const envelopeValidator = WSOutboundEnvelope as GeneratedValidator; +const sessionMessageValidators = SessionOutboundMessageValidators as Record< + SessionOutboundMessage["type"], + GeneratedValidator +>; -export function validateWSOutboundMessage(input: unknown): WSOutboundValidationResult { - return wsOutboundValidator.safeParse(input); +// The message validator reports paths relative to the message, nested issues included. +function underMessage(issue: z.core.$ZodIssue): z.core.$ZodIssue { + const path = ["message", ...issue.path]; + switch (issue.code) { + case "invalid_union": + return issue.inclusive === false + ? { ...issue, path } + : { ...issue, path, errors: issue.errors.map((branch) => branch.map(underMessage)) }; + case "invalid_key": + case "invalid_element": + return { ...issue, path, issues: issue.issues.map(underMessage) }; + default: + return { ...issue, path }; + } +} + +export function validateWSOutboundMessage(input: unknown): ValidationResult { + const envelope = envelopeValidator.safeParse(input); + if (!envelope.success || envelope.data.type !== "session") { + return envelope; + } + const message = envelope.data.message; + const result = sessionMessageValidators[message.type].safeParse(message); + if (!result.success) { + return { success: false, error: new ZodRealError(result.error.issues.map(underMessage)) }; + } + // Message validators return a new object when they apply defaults. + if (result.data === message) { + return envelope; + } + return { success: true, data: { ...envelope.data, message: result.data } }; } diff --git a/packages/protocol/tests/validation/ws-outbound.test.ts b/packages/protocol/tests/validation/ws-outbound.test.ts index e2e3e3170a..ed147d89ad 100644 --- a/packages/protocol/tests/validation/ws-outbound.test.ts +++ b/packages/protocol/tests/validation/ws-outbound.test.ts @@ -3,8 +3,10 @@ import { createRequire } from "node:module"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { createJiti } from "jiti"; +import type { z } from "zod"; import { describe, expect, it } from "vitest"; -import { WSOutboundMessageSchema as GeneratedWSOutboundMessageSchema } from "../../src/generated/validation/ws-outbound.aot.js"; +import { WSOutboundMessageSchema } from "../../src/messages.js"; +import { validateWSOutboundMessage } from "../../src/validation/ws-outbound.js"; interface GeneratedSchema { safeParse(input: unknown): { success: boolean; data?: unknown }; @@ -132,10 +134,8 @@ const SourceSchema = z.object({ }); it("accepts a minimal valid envelope and rejects a corrupted envelope", () => { - expect(GeneratedWSOutboundMessageSchema.safeParse({ type: "pong" }).success).toBe(true); - expect(GeneratedWSOutboundMessageSchema.safeParse({ type: "not_a_message" }).success).toBe( - false, - ); + expect(validateWSOutboundMessage({ type: "pong" }).success).toBe(true); + expect(validateWSOutboundMessage({ type: "not_a_message" }).success).toBe(false); }); it("accepts project config responses with and without setup commit status", () => { @@ -158,11 +158,10 @@ const SourceSchema = z.object({ }, }); - expect(GeneratedWSOutboundMessageSchema.safeParse(envelope(payload)).success).toBe(true); + expect(validateWSOutboundMessage(envelope(payload)).success).toBe(true); expect( - GeneratedWSOutboundMessageSchema.safeParse( - envelope({ ...payload, hasUncommittedWorktreeSetupChanges: true }), - ).success, + validateWSOutboundMessage(envelope({ ...payload, hasUncommittedWorktreeSetupChanges: true })) + .success, ).toBe(true); }); @@ -196,7 +195,7 @@ const SourceSchema = z.object({ }, }; - expect(GeneratedWSOutboundMessageSchema.safeParse(envelope)).toEqual({ + expect(validateWSOutboundMessage(envelope)).toEqual({ success: true, data: envelope, }); @@ -255,7 +254,7 @@ const SourceSchema = z.object({ ])("preserves workspaceId in a $name", ({ message }) => { const envelope = { type: "session", message }; - expect(GeneratedWSOutboundMessageSchema.safeParse(envelope)).toEqual({ + expect(validateWSOutboundMessage(envelope)).toEqual({ success: true, data: envelope, }); @@ -263,11 +262,11 @@ const SourceSchema = z.object({ it("emits runtime imports with .js extensions", async () => { const generated = await readFile(generatedWSOutboundPath, "utf8"); - expect(generated).toContain('from "../../validation/ws-outbound-schema-metadata.js"'); + expect(generated).toContain('from "./ws-outbound-schema-metadata.js"'); }); it("accepts a forge.search.response envelope", () => { - const result = GeneratedWSOutboundMessageSchema.safeParse({ + const result = validateWSOutboundMessage({ type: "session", message: { type: "forge.search.response", @@ -293,7 +292,7 @@ const SourceSchema = z.object({ }); it("accepts a legacy github_search_response envelope", () => { - const result = GeneratedWSOutboundMessageSchema.safeParse({ + const result = validateWSOutboundMessage({ type: "session", message: { type: "github_search_response", @@ -319,4 +318,86 @@ const SourceSchema = z.object({ }); expect(result.success).toBe(true); }); + + it("keeps each generated validator function small", async () => { + // A single function for the whole protocol made V8's Maglev tier spend ~800 MB and seconds compiling it. + const generated = await readFile(generatedWSOutboundPath, "utf8"); + const starts = [...generated.matchAll(/function safeParse_\w+\(/g)].map((match) => match.index); + const sizes = starts.map((start, index) => (starts[index + 1] ?? generated.length) - start); + + expect(Math.max(...sizes)).toBeLessThan(1_000_000); + expect(sizes.length).toBeGreaterThan(1); + }); + + it("keeps nested union issue paths rooted at the envelope", () => { + // Generated validators root branch issues at the input; runtime Zod reports them relative to the union. + const envelope = { + type: "session", + message: { + type: "read_project_config_response", + payload: { repoRoot: "/repo", ok: true, config: {}, revision: { mtimeMs: 1, size: 1 } }, + }, + }; + const paths = (issues: z.core.$ZodIssue[]): PropertyKey[][] => + issues.flatMap((issue) => [ + issue.path, + ...(issue.code === "invalid_union" ? paths(issue.errors.flat()) : []), + ]); + + const result = validateWSOutboundMessage(envelope); + + expect(result.success).toBe(false); + if (result.success) return; + expect(paths(result.error.issues)).toEqual([ + ["message", "payload"], + ["message", "payload", "requestId"], + ["message", "payload", "requestId"], + ["message", "payload", "ok"], + ["message", "payload", "error"], + ]); + }); + + it("applies defaults from the matching session message schema", () => { + const envelope = { + type: "session", + message: { + type: "agent_update", + payload: { + kind: "upsert", + agent: { + id: "agent-1", + provider: "claude", + cwd: "/repo", + model: "model", + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + lastUserMessageAt: "2026-01-01T00:00:00.000Z", + status: "initializing", + capabilities: { + supportsStreaming: false, + supportsSessionPersistence: false, + supportsDynamicModes: false, + supportsMcpServers: false, + supportsReasoningStream: false, + supportsToolInvocations: false, + }, + currentModeId: "default", + availableModes: [], + pendingPermissions: [], + persistence: { provider: "claude", sessionId: "session-1" }, + title: "Agent", + }, + }, + }, + }; + + const result = validateWSOutboundMessage(envelope); + const expected = WSOutboundMessageSchema.safeParse(envelope); + + expect(expected.success).toBe(true); + expect(result).toEqual(expected); + expect(result.success && result.data).toMatchObject({ + message: { payload: { agent: { capabilities: { supportsRewindConversation: false } } } }, + }); + }); }); diff --git a/packages/relay/package.json b/packages/relay/package.json index d53a1a870e..c182f2c403 100644 --- a/packages/relay/package.json +++ b/packages/relay/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/relay", - "version": "0.11.2", + "version": "0.12.0-beta.1", "description": "Paseo relay for bridging daemon and client connections", "files": [ "dist", diff --git a/packages/server/package.json b/packages/server/package.json index 6e5e2448a2..1116605bf4 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/server", - "version": "0.11.2", + "version": "0.12.0-beta.1", "description": "Paseo backend server", "files": [ "dist/server", @@ -107,11 +107,11 @@ "@agentclientprotocol/sdk": "^0.17.1", "@anthropic-ai/claude-agent-sdk": "0.3.246", "@anthropic-ai/sdk": "^0.104.2", - "@getpaseo/client": "0.11.2", - "@getpaseo/highlight": "0.11.2", - "@getpaseo/plugin": "0.11.2", - "@getpaseo/protocol": "0.11.2", - "@getpaseo/relay": "0.11.2", + "@getpaseo/client": "0.12.0-beta.1", + "@getpaseo/highlight": "0.12.0-beta.1", + "@getpaseo/plugin": "0.12.0-beta.1", + "@getpaseo/protocol": "0.12.0-beta.1", + "@getpaseo/relay": "0.12.0-beta.1", "@isaacs/ttlcache": "^2.1.4", "@modelcontextprotocol/sdk": "1.29.0", "@opencode-ai/sdk": "1.14.46", diff --git a/packages/server/src/server/agent/agent-loading.ts b/packages/server/src/server/agent/agent-loading.ts index 248e8d2b1b..761f1008bd 100644 --- a/packages/server/src/server/agent/agent-loading.ts +++ b/packages/server/src/server/agent/agent-loading.ts @@ -12,6 +12,13 @@ import { toAgentPersistenceHandle, } from "../persistence-hooks.js"; +export class AgentNotFoundError extends Error { + constructor(agentId: string) { + super(`Agent not found: ${agentId}`); + this.name = "AgentNotFoundError"; + } +} + interface PendingAgentInitialization { promise: Promise; options: { broadcastTimeline: boolean }; @@ -94,7 +101,7 @@ export async function ensureAgentLoaded( const initPromise = (async () => { const record = await deps.agentStorage.get(agentId); if (!record) { - throw new Error(`Agent not found: ${agentId}`); + throw new AgentNotFoundError(agentId); } const validProviders = deps.validProviders ?? deps.agentManager.getRegisteredProviderIds(); diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index 763659e9f4..44e38a344f 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -10929,6 +10929,41 @@ test("authoritative timeline records a daemon-handled submitted prompt before it } }); +test("tryRunOutOfBand gives providers the command another agent sent, not its envelope", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-agent-out-of-band-")); + const seenPrompts: AgentPromptInput[] = []; + + class SlashCommandSession extends TestAgentSession { + override tryHandleOutOfBand(prompt: AgentPromptInput) { + seenPrompts.push(prompt); + return prompt === "/handled" ? { run: async () => {} } : null; + } + } + const client = new (class extends TestAgentClient { + override async createSession(config: AgentSessionConfig) { + return new SlashCommandSession(config); + } + })(); + const manager = new AgentManager({ clients: { codex: client }, logger }); + const agent = await manager.createAgent({ provider: "codex", cwd: workdir }, undefined, { + workspaceId: undefined, + }); + + try { + const fromAgent = prepareAgentMessage("/handled", { kind: "agent-message", agentId: "sender" }); + expect(manager.tryRunOutOfBand(agent.id, fromAgent.prompt)).toBe(true); + expect(seenPrompts).toEqual(["/handled"]); + + // A human who pastes an envelope gets it quoted, and it stays a plain prompt. + const pasted = prepareAgentMessage(String(fromAgent.prompt)); + expect(manager.tryRunOutOfBand(agent.id, pasted.prompt)).toBe(false); + expect(seenPrompts.at(-1)).toBe(pasted.prompt); + } finally { + await manager.closeAgent(agent.id); + rmSync(workdir, { recursive: true, force: true }); + } +}); + test("replaceAgentRun succeeds when foreground turn terminal event is never delivered", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-stale-fg-")); const storagePath = join(workdir, "agents"); diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index e09d62f6a9..ff91ef85ef 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -79,7 +79,7 @@ import { type PendingForegroundRun, } from "./agent-run-state.js"; import { invokeRewindCapability, type RewindMode } from "./rewind/rewind.js"; -import { projectAgentMessage } from "./agent-messages/index.js"; +import { parseAgentMessage, projectAgentMessage } from "./agent-messages/index.js"; import { isStaleProviderSessionError } from "./stale-provider-session-error.js"; import { stripInternalPaseoMcpServer, withRuntimePaseoMcpServer } from "./runtime-mcp-config.js"; import { resolveCreateAgentTitles } from "./create-agent-title.js"; @@ -2459,7 +2459,12 @@ export class AgentManager { */ tryRunOutOfBand(agentId: string, prompt: AgentPromptInput, options?: AgentRunOptions): boolean { const agent = this.requireSessionAgent(agentId); - const handler = agent.session.tryHandleOutOfBand?.(prompt); + // Providers parse slash commands from the raw text, so a command another agent sent + // must reach them without its sender envelope. + const agentMessage = typeof prompt === "string" ? parseAgentMessage(prompt) : null; + const handler = agent.session.tryHandleOutOfBand?.( + agentMessage?.source ? agentMessage.text : prompt, + ); if (!handler) { return false; } diff --git a/packages/server/src/server/agent/mcp-server.test.ts b/packages/server/src/server/agent/mcp-server.test.ts index de0099cecf..33467b22f6 100644 --- a/packages/server/src/server/agent/mcp-server.test.ts +++ b/packages/server/src/server/agent/mcp-server.test.ts @@ -5,7 +5,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; import { describe, expect, it, vi } from "vitest"; import { realpathSync } from "node:fs"; -import { access, mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; +import { access, mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { join, resolve as resolvePath } from "node:path"; import { tmpdir } from "node:os"; import { z } from "zod"; @@ -15,6 +15,8 @@ import { createAgentMcpServer as createAgentMcpServerWithDependencies } from "./ import { AgentManager, type ManagedAgent } from "./agent-manager.js"; import { AgentStorage, type StoredAgentRecord } from "./agent-storage.js"; import { createTestAgentClients } from "../test-utils/fake-agent-client.js"; +import { OmpAgentClient } from "./providers/omp/agent.js"; +import { FakeOmp } from "./providers/omp/test-utils/fake-omp.js"; import type { AgentClient, AgentMode, @@ -841,6 +843,49 @@ function createAgentMcpServer(options: Parameters { + it("documents only advertised create_workspace input names", async () => { + const server = await createAgentMcpServer({ + agentManager: new BoundaryAgentManagerFake() as AgentManager, + agentStorage: new BoundaryAgentStorageFake() as AgentStorage, + providerSnapshotManager: + new BoundaryProviderSnapshotManagerFake() as unknown as ProviderSnapshotManager, + logger: createTestLogger(), + }); + const client = await connectInMemoryMcpClient(server); + + try { + const skill = await readFile( + new URL("../../../../../skills/paseo/SKILL.md", import.meta.url), + "utf8", + ); + const paragraph = skill.split("\n").find((line) => line.startsWith("**`create_workspace`**")); + expect(paragraph).toBeTypeOf("string"); + // The final sentence describes output, rather than tool inputs. + const inputGuidance = paragraph!.split("Returns ")[0]!; + const documentedInputs = [...inputGuidance.matchAll(/`([a-zA-Z][a-zA-Z0-9]*)`/g)].map( + (match) => match[1]!, + ); + expect(documentedInputs.length).toBeGreaterThan(0); + const { tools } = await client.listTools(); + const tool = tools.find((entry) => entry.name === "create_workspace")!; + const properties = tool.inputSchema.properties!; + const advertisedNamesAndValues = new Set([ + ...Object.keys(properties), + ...Object.values(properties).flatMap((property) => + typeof property === "object" && property !== null && Array.isArray(property.enum) + ? property.enum + : [], + ), + ]); + expect(documentedInputs.filter((name) => !advertisedNamesAndValues.has(name))).toEqual([]); + } finally { + await client.close(); + await server.close(); + } + }); +}); + describe("browser MCP tools", () => { const logger = createTestLogger(); @@ -3856,8 +3901,7 @@ class HeldTurnAgentSession implements AgentSession { } tryHandleOutOfBand(prompt: AgentPromptInput) { - const text = typeof prompt === "string" ? (parseAgentMessage(prompt)?.text ?? prompt) : null; - return text === "/side-command" ? { run: async () => {} } : null; + return prompt === "/side-command" ? { run: async () => {} } : null; } async startTurn(prompt: AgentPromptInput): Promise<{ turnId: string }> { @@ -4557,6 +4601,180 @@ describe("send_agent_prompt MCP tool", () => { }); }); +describe("send_agent_prompt MCP tool to an OMP child", () => { + const logger = createTestLogger(); + const existingCwd = process.cwd(); + + async function setupOmpChild() { + const workdir = await mkdtemp(join(tmpdir(), "mcp-omp-child-")); + const storage = new AgentStorage(join(workdir, "agents"), logger); + const parentClient = new HeldTurnAgentClient("claude", false); + const omp = new FakeOmp(); + const agentManager = new AgentManager({ + clients: { + claude: parentClient, + omp: new OmpAgentClient({ + logger, + runtime: omp, + noTurnScheduler: { waitForSettle: async () => {} }, + runtimeSettings: { command: { mode: "replace", argv: [process.execPath] } }, + }), + }, + registry: storage, + logger, + }); + const parent = await agentManager.createAgent( + { provider: "claude", cwd: existingCwd }, + undefined, + { workspaceId: "wks_parent" }, + ); + const child = await agentManager.createAgent( + { provider: "omp", cwd: existingCwd, title: "OMP child" }, + undefined, + { workspaceId: "wks_parent" }, + ); + const server = await createAgentMcpServer({ + agentManager, + agentStorage: storage, + callerAgentId: parent.id, + providerSnapshotManager: createOpenCodeManager().manager, + logger, + }); + return { + agentManager, + storage, + workdir, + parentClient, + ompSession: omp.latestSession(), + childId: child.id, + tool: registeredTool(server, "send_agent_prompt"), + }; + } + + async function expectOneFinishNotification( + agentManager: AgentManager, + parentClient: HeldTurnAgentClient, + childId: string, + ) { + await vi.waitFor(() => expect(agentManager.getAgent(childId)?.lifecycle).toBe("idle")); + await vi.waitFor(() => { + expect(parentClient.sessions[0]!.prompts).toHaveLength(1); + expect(parentClient.sessions[0]!.prompts[0]).toContain( + `Agent ${childId} (OMP child) finished.`, + ); + }); + } + + it.each([ + { command: "follow-up", streamingBehavior: "followUp" }, + { command: "steer", streamingBehavior: "steer" }, + ] as const)( + "delivers /$command into a turn OMP started on its own, without aborting it", + async ({ command, streamingBehavior }) => { + const { agentManager, storage, workdir, parentClient, ompSession, childId, tool } = + await setupOmpChild(); + try { + ompSession.beginTurn(); + ompSession.streamAssistantText("background job finished, working on it"); + await vi.waitFor(() => { + expect(agentManager.getAgent(childId)?.activeTurnId).toMatch(/^autonomous-/); + }); + + const response = await invokeToolWithParsedInput(tool, { + agentId: childId, + prompt: `/${command} then summarize the logs`, + }); + + expect(response.structuredContent).toMatchObject({ + success: true, + disposition: "out_of_band", + status: "running", + }); + await vi.waitFor(() => { + expect(ompSession.prompts).toEqual([ + { message: "then summarize the logs", imageCount: 0, streamingBehavior }, + ]); + }); + expect(ompSession.abortRequested).toBe(false); + expect(agentManager.getAgent(childId)?.lifecycle).toBe("running"); + + ompSession.finishTurn(); + await expectOneFinishNotification(agentManager, parentClient, childId); + } finally { + await removeAgentStateDir(agentManager, storage, workdir); + } + }, + ); + + it.each(["before ack", "after ack"])( + "reports an idle follow-up rejected %s and notifies its parent", + async (order) => { + const { agentManager, storage, workdir, parentClient, ompSession, childId, tool } = + await setupOmpChild(); + try { + ompSession.promptAck = { requestId: "follow-up-rejected" }; + const rejection = { + type: "prompt_result", + id: "follow-up-rejected", + agentInvoked: false, + status: "error", + sessionSettled: true, + error: { message: "Provider rejected the follow-up", retryable: false }, + } as const; + const promptStarted = ompSession.nextPrompt(); + const response = invokeToolWithParsedInput(tool, { + agentId: childId, + prompt: "/follow-up check the build", + }); + await promptStarted; + if (order === "before ack") ompSession.emit(rejection); + await response; + if (order === "after ack") ompSession.emit(rejection); + await vi.waitFor(() => expect(agentManager.getAgent(childId)?.lifecycle).toBe("error")); + expect(agentManager.getAgent(childId)?.lastError).toContain( + "Provider rejected the follow-up", + ); + await vi.waitFor(() => expect(parentClient.sessions[0]!.prompts).toHaveLength(1)); + expect(parentClient.sessions[0]!.prompts[0]).toContain( + `Agent ${childId} (OMP child) errored.`, + ); + } finally { + await removeAgentStateDir(agentManager, storage, workdir); + } + }, + ); + + it("lets an idle child start a run with the follow-up", async () => { + const { agentManager, storage, workdir, parentClient, ompSession, childId, tool } = + await setupOmpChild(); + try { + const response = await invokeToolWithParsedInput(tool, { + agentId: childId, + prompt: "/follow-up check the build", + }); + + expect(response.structuredContent).toMatchObject({ + success: true, + disposition: "out_of_band", + status: "running", + }); + await vi.waitFor(() => { + expect(ompSession.prompts).toEqual([ + { message: "check the build", imageCount: 0, streamingBehavior: "followUp" }, + ]); + }); + + ompSession.beginTurn(); + ompSession.streamAssistantText("Build is green"); + await vi.waitFor(() => expect(agentManager.getAgent(childId)?.lifecycle).toBe("running")); + ompSession.finishTurn(); + await expectOneFinishNotification(agentManager, parentClient, childId); + } finally { + await removeAgentStateDir(agentManager, storage, workdir); + } + }); +}); + describe("update_agent MCP tool", () => { const logger = createTestLogger(); diff --git a/packages/server/src/server/agent/providers/acp-agent.ts b/packages/server/src/server/agent/providers/acp-agent.ts index 3b61fcb863..823f941de1 100644 --- a/packages/server/src/server/agent/providers/acp-agent.ts +++ b/packages/server/src/server/agent/providers/acp-agent.ts @@ -88,6 +88,7 @@ import { type AgentStreamEvent, type AgentTimelineItem, type AgentUsage, + type AgentUsageSession, type FetchCatalogOptions, type ProviderRefreshContext, type ImportableProviderSession, @@ -112,6 +113,7 @@ import { import { importSessionFromPersistence } from "../provider-session-import.js"; import { checkProviderLaunchAvailable, + createProviderEnv, createProviderEnvSpec, resolveProviderLaunch, type ProviderRuntimeSettings, @@ -456,6 +458,8 @@ export type ACPCatalogModelResolver = ( interface ACPAgentClientOptions { provider: string; + // Provider id usage sources match a session against; ACP sessions without one report no usage. + usageProvider?: string; logger: Logger; runtimeSettings?: ProviderRuntimeSettings; defaultCommand: [string, ...string[]]; @@ -488,6 +492,7 @@ interface ACPAgentClientOptions { interface ACPAgentSessionOptions { provider: string; + usageProvider?: string; logger: Logger; runtimeSettings?: ProviderRuntimeSettings; defaultCommand: [string, ...string[]]; @@ -910,6 +915,7 @@ function isACPCreateConfigUnattended(input: AgentCreateConfigUnattendedInput): b export class ACPAgentClient implements AgentClient { readonly provider: string; + private readonly usageProvider?: string; readonly capabilities: AgentCapabilityFlags; readonly resolveCreateConfig = resolveACPCreateConfig; readonly isCreateConfigUnattended = isACPCreateConfigUnattended; @@ -951,6 +957,7 @@ export class ACPAgentClient implements AgentClient { constructor(options: ACPAgentClientOptions) { this.provider = options.provider; + this.usageProvider = options.usageProvider; this.terminateProcess = options.terminateProcess ?? terminateWithTreeKill; this.capabilities = options.capabilities ?? DEFAULT_ACP_CAPABILITIES; this.logger = options.logger.child({ @@ -989,6 +996,7 @@ export class ACPAgentClient implements AgentClient { { ...config, provider: this.provider }, { provider: this.provider, + usageProvider: this.usageProvider, logger: this.logger, runtimeSettings: this.runtimeSettings, defaultCommand: this.defaultCommand, @@ -1046,6 +1054,7 @@ export class ACPAgentClient implements AgentClient { const providerOptions = ACPProviderOptionsSchema.parse(mergedConfig.providerOptions ?? {}); const session = new ACPAgentSession(mergedConfig, { provider: this.provider, + usageProvider: this.usageProvider, logger: this.logger, runtimeSettings: this.runtimeSettings, defaultCommand: this.defaultCommand, @@ -1715,6 +1724,9 @@ export class ACPAgentSession implements AgentSession, ACPClient { ) => Promise; private readonly agentId?: string; private readonly launchEnv?: Record; + private readonly usageProvider?: string; + private readonly usageSessionKey = randomUUID(); + private readonly harnessEnvironment: Record; private readonly subscribers = new Set<(event: AgentStreamEvent) => void>(); private readonly pendingPermissions = new Map(); private pendingUserMessage: PendingUserMessage | null = null; @@ -1778,6 +1790,11 @@ export class ACPAgentSession implements AgentSession, ACPClient { this.availableModes = options.defaultModes; this.agentId = options.agentId; this.launchEnv = options.launchEnv; + this.usageProvider = options.usageProvider; + this.harnessEnvironment = createProviderEnv({ + runtimeSettings: options.runtimeSettings, + overlays: [options.launchEnv], + }); this.initialHandle = options.handle; this.resumePurpose = options.resumePurpose ?? "interactive"; this.config = { ...config, provider: options.provider }; @@ -1794,6 +1811,16 @@ export class ACPAgentSession implements AgentSession, ACPClient { return this.sessionId; } + usageSession(): AgentUsageSession | null { + if (this.closed || !this.usageProvider) return null; + return { + provider: this.usageProvider, + model: this.currentModel ?? undefined, + env: this.harnessEnvironment, + sessionKey: this.usageSessionKey, + }; + } + async initializeNewSession(): Promise { try { const spawned = await this.spawnProcess(); diff --git a/packages/server/src/server/agent/providers/claude/agent.background-subagent.test.ts b/packages/server/src/server/agent/providers/claude/agent.background-subagent.test.ts index 630bdbf0a6..d0a5bf644a 100644 --- a/packages/server/src/server/agent/providers/claude/agent.background-subagent.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.background-subagent.test.ts @@ -4,7 +4,7 @@ import path from "node:path"; import { afterEach, describe, expect, test, vi } from "vitest"; import { createTestLogger } from "../../../../test-utils/test-logger.js"; -import type { AgentStreamEvent } from "../../agent-sdk-types.js"; +import type { AgentSession, AgentStreamEvent } from "../../agent-sdk-types.js"; import type { AgentTimelineRow } from "../../agent-manager.js"; import { projectTimelineRows } from "../../timeline-projection.js"; import { ClaudeAgentClient } from "./agent.js"; @@ -105,6 +105,39 @@ async function collectUntilTerminal( return events; } +async function collectTimelineAcrossTurns( + session: AgentSession, + prompt: string, + turnCount: number, +): Promise { + const rows: AgentTimelineRow[] = []; + let currentTurnId: string | undefined; + let completedTurns = 0; + let unsubscribe = () => {}; + const turnsCompleted = new Promise((resolve) => { + unsubscribe = session.subscribe((event) => { + if (event.type === "turn_started") currentTurnId = event.turnId; + if (event.type === "timeline") { + rows.push({ + seq: rows.length + 1, + timestamp: new Date(Date.UTC(2026, 9, 4, 0, 0, rows.length)).toISOString(), + item: event.item, + ...(currentTurnId ? { turnId: currentTurnId } : {}), + }); + } + if (event.type === "turn_completed" && ++completedTurns === turnCount) resolve(); + }); + }); + try { + await session.startTurn(prompt); + await turnsCompleted; + return rows; + } finally { + unsubscribe(); + await session.close(); + } +} + describe("background Claude subagents", () => { afterEach(() => { queryFactory.mockReset(); @@ -431,6 +464,124 @@ describe("background Claude subagents", () => { type: "sub_agent", subAgentType: "general-purpose", description: "Count files here", + log: "[Bash] ls -1", + }, + }); + }); + + test("keeps one logged Task card when the child streams on after the parent's turn ends", async () => { + // A child can stream across the main session's turn boundary. Claude opens the next + // main-session turn with system/init; bare child frames do not open a turn. + queryFactory.mockImplementation(() => + buildQueryMock([ + { type: "system", subtype: "init", session_id: "bg-session", permissionMode: "default" }, + { + type: "assistant", + message: { + model: "claude-haiku-4-5", + content: [ + { + type: "tool_use", + id: "toolu_background", + name: "Agent", + input: { + subagent_type: "general-purpose", + description: "Count files here", + prompt: "Run ls and reply with the number of entries.", + run_in_background: true, + }, + }, + ], + }, + }, + { + type: "system", + subtype: "task_started", + task_id: "bg-task", + tool_use_id: "toolu_background", + task_type: "local_agent", + subagent_type: "general-purpose", + description: "Count files here", + prompt: "Run ls and reply with the number of entries.", + }, + { + type: "user", + message: { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "toolu_background", + content: "Async agent launched successfully.", + }, + ], + }, + }, + { + type: "assistant", + parent_tool_use_id: "toolu_background", + message: { + model: "claude-haiku-4-5", + content: [ + { + type: "tool_use", + id: "toolu_ls", + name: "Bash", + input: { command: "ls -1", description: "List files" }, + }, + ], + }, + }, + { type: "result", subtype: "success", usage: {}, total_cost_usd: 0 }, + { type: "system", subtype: "init", session_id: "bg-session", permissionMode: "default" }, + { + type: "user", + parent_tool_use_id: "toolu_background", + message: { + role: "user", + content: [{ type: "tool_result", tool_use_id: "toolu_ls", content: "a\nb\nc" }], + }, + }, + { + type: "assistant", + parent_tool_use_id: "toolu_background", + message: { model: "claude-haiku-4-5", content: [{ type: "text", text: "3" }] }, + }, + { + type: "system", + subtype: "task_updated", + task_id: "bg-task", + patch: { status: "completed" }, + }, + { + type: "system", + subtype: "task_notification", + task_id: "bg-task", + tool_use_id: "toolu_background", + status: "completed", + }, + { type: "result", subtype: "success", usage: {}, total_cost_usd: 0 }, + ]), + ); + const session = await new ClaudeAgentClient({ + logger: createTestLogger(), + queryFactory, + resolveBinary: async () => "/test/claude/bin", + }).createSession({ provider: "claude", cwd: process.cwd() }); + const rows = await collectTimelineAcrossTurns(session, "delegate work", 2); + + const cards = projectTimelineRows({ rows, mode: "projected" }) + .map((entry) => entry.item) + .filter((item) => item.type === "tool_call" && item.callId === "toolu_background"); + + expect(cards).toHaveLength(1); + expect(cards[0]).toMatchObject({ + status: "completed", + detail: { + type: "sub_agent", + subAgentType: "general-purpose", + description: "Count files here", + log: "[Bash] ls -1", }, }); }); diff --git a/packages/server/src/server/agent/providers/claude/agent.image-rendering.test.ts b/packages/server/src/server/agent/providers/claude/agent.image-rendering.test.ts index 66891e01bf..5a06fea7fd 100644 --- a/packages/server/src/server/agent/providers/claude/agent.image-rendering.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.image-rendering.test.ts @@ -140,11 +140,11 @@ function imageMessages(items: AgentTimelineItem[]): string[] { return items .filter((item) => item.type === "assistant_message") .map((item) => (item as { text: string }).text) - .filter((text) => text.startsWith("![")); + .filter((text) => text.trimStart().startsWith("![")); } function markdownImageSource(markdown: string): string { - const match = markdown.match(/^!\[[^\]]*]\((.*)\)$/); + const match = markdown.trim().match(/^!\[[^\]]*]\((.*)\)$/); if (!match) { throw new Error(`Expected markdown image, got: ${markdown}`); } diff --git a/packages/server/src/server/agent/providers/claude/agent.live-settings.test.ts b/packages/server/src/server/agent/providers/claude/agent.live-settings.test.ts new file mode 100644 index 0000000000..cb272ebecb --- /dev/null +++ b/packages/server/src/server/agent/providers/claude/agent.live-settings.test.ts @@ -0,0 +1,157 @@ +import { expect, test } from "vitest"; +import type { Query, SDKMessage } from "@anthropic-ai/claude-agent-sdk"; +import { createTestLogger } from "../../../../test-utils/test-logger.js"; +import type { AgentStreamEvent } from "../../agent-sdk-types.js"; +import { ClaudeAgentClient } from "./agent.js"; +import type { ClaudeQueryInput } from "./query.js"; + +function deferred() { + let resolve!: (value: T) => void; + const promise = new Promise((done) => { + resolve = done; + }); + return { promise, resolve }; +} + +// The injected SDK adapter consumes real session input and holds a command open +// until the test releases it. Closing the query interrupts that command. +class ForegroundQuery { + readonly work = deferred(); + readonly closed = deferred(); + private stopped = false; + private readonly output: AsyncGenerator; + + constructor(input: ClaudeQueryInput) { + this.output = this.events(input); + } + + private async *events(input: ClaudeQueryInput): AsyncGenerator { + if (typeof input.prompt === "string") throw new Error("Expected streaming input"); + for await (const _prompt of input.prompt) { + yield { + type: "system", + subtype: "init", + session_id: "settings-session", + model: "claude-haiku-5-5", + permissionMode: "auto", + } as SDKMessage; + yield { + type: "assistant", + message: { + content: [ + { type: "tool_use", id: "tool_sleep", name: "Bash", input: { command: "sleep 20" } }, + ], + }, + } as SDKMessage; + await this.work.promise; + if (this.stopped) return; + yield { type: "assistant", message: { content: "DONE" } } as SDKMessage; + yield { + type: "result", + subtype: "success", + usage: { input_tokens: 1, cache_read_input_tokens: 0, output_tokens: 1 }, + total_cost_usd: 0, + } as SDKMessage; + } + } + + next() { + return this.output.next(); + } + return() { + this.close(); + return this.output.return(); + } + close() { + this.stopped = true; + this.closed.resolve(); + this.work.resolve(); + } + async interrupt() { + this.close(); + } + async setPermissionMode() {} + async setModel() {} + async supportedCommands() { + return []; + } + [Symbol.asyncIterator]() { + return this; + } +} + +test.each(["mode", "model", "commands"] as const)( + "a pending thinking change preserves the current turn during a %s request", + async (control) => { + const queries: ForegroundQuery[] = []; + const efforts: ClaudeQueryInput["options"]["effort"][] = []; + const client = new ClaudeAgentClient({ + logger: createTestLogger(), + resolveBinary: async () => "/test/claude", + queryFactory: (input) => { + const query = new ForegroundQuery(input); + queries.push(query); + efforts.push(input.options.effort); + return query as unknown as Query; + }, + }); + const session = await client.createSession({ + provider: "claude", + cwd: process.cwd(), + model: "claude-haiku-5-5", + modeId: "auto", + thinkingOptionId: "low", + }); + const running = deferred(); + let terminal = deferred(); + const events: AgentStreamEvent[] = []; + session.subscribe((event) => { + events.push(event); + if ( + event.type === "timeline" && + event.item.type === "tool_call" && + event.item.status === "running" + ) + running.resolve(); + if ( + event.type === "turn_completed" || + event.type === "turn_failed" || + event.type === "turn_canceled" + ) + terminal.resolve(event.type); + }); + try { + await session.startTurn("Run the foreground command"); + await running.promise; + expect(await session.setThinkingOption?.("medium")).toMatchObject({ + message: "Thinking level applies next turn", + }); + if (control === "mode") await session.setMode("acceptEdits"); + else if (control === "model") await session.setModel("claude-sonnet-5-6"); + else await session.listCommands(); + queries[0].work.resolve(); + expect( + await Promise.race([ + terminal.promise, + queries[0].closed.promise.then(() => "query closed before turn finished"), + ]), + ).toBe("turn_completed"); + expect( + events.some( + (event) => + event.type === "timeline" && + event.item.type === "assistant_message" && + event.item.text === "DONE", + ), + ).toBe(true); + + terminal = deferred(); + await session.startTurn("Run another command with the new thinking level"); + queries[1].work.resolve(); + expect(await terminal.promise).toBe("turn_completed"); + expect(efforts).toEqual(["low", "medium"]); + } finally { + await session.close(); + } + }, +); diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index 8c5cc1a41c..7517a87029 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -2228,18 +2228,37 @@ class ClaudeAgentSession implements AgentSession { getToolInput: (toolUseId) => this.toolUseCache.get(toolUseId)?.input ?? null, readWorkflowResult: readClaudeWorkflowResultFile, }); + /** + * Subagent tool calls Claude has already answered while the child still runs, with the input + * they were launched with and the turn that answered them. A background child is answered at + * launch and streams its frames afterwards. + */ + private readonly settledSubagentToolCalls = new Map< + string, + { + item: Extract; + input: AgentMetadata | null; + turnId: string | null; + } + >(); private readonly sidechainTracker = new ClaudeSidechainTracker({ - getToolInput: (toolUseId) => this.toolUseCache.get(toolUseId)?.input ?? null, + getToolInput: (toolUseId) => + this.toolUseCache.get(toolUseId)?.input ?? + this.settledSubagentToolCallInCurrentTurn(toolUseId)?.input ?? + null, // Releases that predate the task protocol announce nothing, so the tracker keeps deriving // identity and status from frames for them. Detecting the capability beats comparing version // strings: it reacts to what this session actually does. isDescriptorOwnedElsewhere: () => this.taskProtocolSource.isActive, - // The parent's tool call owns its card once its result arrives. A background child streams - // its frames after Claude has already answered the call, and re-emitting the card from them - // would replace the settled card with an unlabeled, running one. + // A background child streams its frames after Claude has already answered the call, so its + // card is rebuilt from the settled call: a running one would replace the settled, labeled card. + // A card belongs to the turn that answered the call, so frames from a later turn leave it be. needsSyntheticParentToolCard: (toolUseId) => - this.toolUseCache.has(toolUseId) && + (this.toolUseCache.has(toolUseId) || + this.settledSubagentToolCallInCurrentTurn(toolUseId) !== undefined) && this.taskProtocolSource.needsSyntheticParentToolCard(toolUseId), + getSettledParentToolCall: (toolUseId) => + this.settledSubagentToolCallInCurrentTurn(toolUseId)?.item, }); private persistedHistory: PersistedTimelineEntry[] = []; private persistedProviderSubagentEvents: Extract< @@ -2591,13 +2610,13 @@ class ClaudeAgentSession implements AgentSession { const normalized = isPermissionMode(modeId) ? modeId : "default"; assertClaudeModeCanRun(normalized, this.harnessEnvironment); const previousMode = this.currentMode; - const launchesQuery = !this.query || this.queryRestartNeeded; + const previousQuery = this.query; const activeQuery = await this.ensureQuery(normalized); try { await activeQuery.setPermissionMode(normalized); } catch (error) { // The query was launched in the rejected mode; relaunch in the current mode next time. - if (launchesQuery) this.queryRestartNeeded = true; + if (activeQuery !== previousQuery) this.queryRestartNeeded = true; throw error; } if (normalized === "plan") { @@ -2876,6 +2895,7 @@ class ClaudeAgentSession implements AgentSession { this.cancelCurrentTurn = null; this.turnState = "idle"; this.sidechainTracker.clear(); + this.settledSubagentToolCalls.clear(); this.taskProtocolSource.reset(); this.input?.end(); this.query?.close?.(); @@ -3294,7 +3314,9 @@ class ClaudeAgentSession implements AgentSession { } private async ensureQuery(launchMode: PermissionMode = this.currentMode): Promise { - if (this.query && !this.queryRestartNeeded) { + // Controls use the running turn's query. Pending launch settings take effect + // after that turn finishes, when its active query handle has been cleared. + if (this.query && (!this.queryRestartNeeded || this.activeForegroundQuery === this.query)) { return this.query; } @@ -4361,6 +4383,7 @@ class ClaudeAgentSession implements AgentSession { for (const event of foldSubagentObservations(subagentObservations)) { events.push({ type: "provider_subagent", provider: "claude", event }); } + this.releaseFinishedSubagentToolCalls(subagentObservations); for (const observation of subagentObservations) { if (observation.kind !== "declared") continue; if (!this.taskProtocolSource.needsSyntheticParentToolCard(observation.id)) continue; @@ -5548,28 +5571,22 @@ class ClaudeAgentSession implements AgentSession { const { images, text } = splitClaudeToolResultImages(block.content); const output = this.buildToolOutput(text, block, entry); - if (block.is_error) { - this.pushToolCall( - mapClaudeFailedToolCall({ + const settledToolCall = block.is_error + ? mapClaudeFailedToolCall({ name: toolName, callId, input: entry?.input ?? null, output: output ?? null, error: { ...block, content: text }, - }), - items, - ); - } else { - this.pushToolCall( - mapClaudeCompletedToolCall({ + }) + : mapClaudeCompletedToolCall({ name: toolName, callId, input: entry?.input ?? null, output: output ?? null, - }), - items, - ); - } + }); + this.pushToolCall(settledToolCall, items); + this.rememberSettledSubagentToolCall(settledToolCall, entry); for (const image of images) { const imageItem = renderProviderImageOutputAsAssistantMarkdown(image, { @@ -5585,6 +5602,34 @@ class ClaudeAgentSession implements AgentSession { } } + private rememberSettledSubagentToolCall( + item: Extract | null, + entry: ToolUseCacheEntry | undefined, + ): void { + const subagentId = item ? this.taskProtocolSource.resolveSubagentId(item.callId) : undefined; + if (!item || !subagentId) return; + this.settledSubagentToolCalls.set(subagentId, { + item, + input: entry?.input ?? null, + turnId: this.activeForegroundTurnId ?? this.autonomousTurn?.id ?? null, + }); + } + + private settledSubagentToolCallInCurrentTurn(subagentId: string) { + const settled = this.settledSubagentToolCalls.get(subagentId); + const currentTurnId = this.activeForegroundTurnId ?? this.autonomousTurn?.id ?? null; + return settled && settled.turnId === currentTurnId ? settled : undefined; + } + + /** A finished child streams no more frames, so its settled call has nothing left to update. */ + private releaseFinishedSubagentToolCalls(observations: SubagentObservation[]): void { + for (const observation of observations) { + if (observation.kind === "status" && observation.status !== "running") { + this.settledSubagentToolCalls.delete(observation.id); + } + } + } + private buildToolOutput( content: unknown, block: ClaudeContentChunk, diff --git a/packages/server/src/server/agent/providers/claude/sidechain-tracker.ts b/packages/server/src/server/agent/providers/claude/sidechain-tracker.ts index 195e21f857..f054523126 100644 --- a/packages/server/src/server/agent/providers/claude/sidechain-tracker.ts +++ b/packages/server/src/server/agent/providers/claude/sidechain-tracker.ts @@ -18,6 +18,8 @@ interface ClaudeContentChunk { [key: string]: unknown; } +type ClaudeToolCallItem = Extract; + interface SubAgentActionEntry { index: number; toolName: string; @@ -64,6 +66,7 @@ export class ClaudeSidechainTracker { private readonly getToolInput: (toolUseId: string) => AgentMetadata | null | undefined; private readonly isDescriptorOwnedElsewhere: () => boolean; private readonly needsSyntheticParentToolCard: (toolUseId: string) => boolean; + private readonly getSettledParentToolCall: (toolUseId: string) => ClaudeToolCallItem | undefined; constructor(input: { getToolInput: (toolUseId: string) => AgentMetadata | null | undefined; @@ -74,10 +77,17 @@ export class ClaudeSidechainTracker { */ isDescriptorOwnedElsewhere?: () => boolean; needsSyntheticParentToolCard?: (toolUseId: string) => boolean; + /** + * The parent's tool call once Claude has answered it. A background child streams its frames + * after that answer, so its card keeps the settled status and output and gains the child's + * action log. + */ + getSettledParentToolCall?: (toolUseId: string) => ClaudeToolCallItem | undefined; }) { this.getToolInput = input.getToolInput; this.isDescriptorOwnedElsewhere = input.isDescriptorOwnedElsewhere ?? (() => false); this.needsSyntheticParentToolCard = input.needsSyntheticParentToolCard ?? (() => true); + this.getSettledParentToolCall = input.getSettledParentToolCall ?? (() => undefined); } handleMessage(message: SDKMessage, parentToolUseId: string): AgentStreamEvent[] { @@ -119,17 +129,19 @@ export class ClaudeSidechainTracker { return []; } - const toolCall = mapClaudeRunningToolCall({ - name: "Task", - callId: parentToolUseId, - input: null, - output: null, - }); + const toolCall = + this.getSettledParentToolCall(parentToolUseId) ?? + mapClaudeRunningToolCall({ + name: "Task", + callId: parentToolUseId, + input: null, + output: null, + }); if (!toolCall) { return []; } - const detail: Extract["detail"] = { + const detail: ClaudeToolCallItem["detail"] = { type: "sub_agent", ...(state.subAgentType ? { subAgentType: state.subAgentType } : {}), ...(state.description ? { description: state.description } : {}), diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts index c8269722a7..8238a80131 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts @@ -502,7 +502,7 @@ function asInternals(session: CodexTestSession): CodexSessionTestAccess { } function markdownImageSource(markdown: string): string { - const match = markdown.match(/^!\[[^\]]*]\((.*)\)$/); + const match = markdown.trim().match(/^!\[[^\]]*]\((.*)\)$/); if (!match) { throw new Error(`Expected markdown image, got: ${markdown}`); } @@ -6000,7 +6000,7 @@ describe("Codex app-server provider", () => { turnId: "test-turn", item: { type: "assistant_message", - text: "![Image](file:///tmp/paseo%20image.png)", + text: "\n\n![Image](file:///tmp/paseo%20image.png)\n\n", }, }, ]); @@ -6032,7 +6032,7 @@ describe("Codex app-server provider", () => { turnId: "test-turn", item: { type: "assistant_message", - text: `![Image](${expectedPath})`, + text: `\n\n![Image](${expectedPath})\n\n`, }, }, ]); diff --git a/packages/server/src/server/agent/providers/cursor-acp-agent.test.ts b/packages/server/src/server/agent/providers/cursor-acp-agent.test.ts index bd43d913e8..97b4c1cc86 100644 --- a/packages/server/src/server/agent/providers/cursor-acp-agent.test.ts +++ b/packages/server/src/server/agent/providers/cursor-acp-agent.test.ts @@ -1,10 +1,14 @@ import type { SessionConfigOption } from "@agentclientprotocol/sdk"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; import { describe, expect, test, vi } from "vitest"; import { ACPAgentSession } from "./acp-agent.js"; import type { SpawnedACPProcess, SessionStateResponse } from "./acp-agent.js"; import type { AgentSessionConfig } from "../agent-sdk-types.js"; import { CURSOR_FAST_FEATURE_OPTION, CursorACPAgentClient } from "./cursor-acp-agent.js"; +import { GenericACPAgentClient } from "./generic-acp-agent.js"; import { createTestLogger } from "../../../test-utils/test-logger.js"; function captureWarnings(): { @@ -355,3 +359,70 @@ describe("CursorACPAgentClient session start", () => { await expect(session.initializeNewSession()).rejects.toThrow("Invalid params"); }); }); + +describe("CursorACPAgentClient usage session", () => { + test("matches usage sources as cursor, with the agent's provider and launch env", async () => { + await withFakeACPAgent(async (command, cwd) => { + const client = new CursorACPAgentClient({ + logger: createTestLogger(), + command, + env: { XDG_CONFIG_HOME: "/profile/config" }, + }); + const session = await client.createSession( + { provider: "acp", cwd }, + { agentId: "agent-1", env: { HOME: "/agent/home" } }, + ); + const usage = session.usageSession?.(); + expect(usage).toMatchObject({ + provider: "cursor", + env: { XDG_CONFIG_HOME: "/profile/config", HOME: "/agent/home" }, + }); + expect(session.usageSession?.()?.sessionKey).toBe(usage?.sessionKey); + await session.close(); + expect(session.usageSession?.()).toBeNull(); + }); + }); + + test("leaves other ACP agents without a usage session", async () => { + await withFakeACPAgent(async (command, cwd) => { + const client = new GenericACPAgentClient({ logger: createTestLogger(), command }); + const session = await client.createSession({ provider: "acp", cwd }); + try { + expect(session.usageSession?.()).toBeNull(); + } finally { + await session.close(); + } + }); + }); +}); + +async function withFakeACPAgent( + run: (command: [string, ...string[]], cwd: string) => Promise, +): Promise { + const testDir = await mkdtemp(path.join(tmpdir(), "paseo-cursor-usage-")); + try { + const scriptPath = path.join(testDir, "fake-acp-agent.cjs"); + await writeFile(scriptPath, fakeACPAgentScript, "utf8"); + await run([process.execPath, scriptPath], testDir); + } finally { + await rm(testDir, { recursive: true, force: true }); + } +} + +const fakeACPAgentScript = ` +const readline = require("node:readline"); +const rl = readline.createInterface({ input: process.stdin }); +function write(message) { + process.stdout.write(JSON.stringify({ jsonrpc: "2.0", ...message }) + "\\n"); +} +rl.on("line", (line) => { + const message = JSON.parse(line); + if (message.method === "initialize") { + write({ id: message.id, result: { protocolVersion: message.params?.protocolVersion ?? 1, agentCapabilities: {} } }); + } else if (message.method === "session/new") { + write({ id: message.id, result: { sessionId: "session-1" } }); + } else if (message.id !== undefined) { + write({ id: message.id, result: {} }); + } +}); +`; diff --git a/packages/server/src/server/agent/providers/cursor-acp-agent.ts b/packages/server/src/server/agent/providers/cursor-acp-agent.ts index 0b5b9ac4ec..4c5cd8a9c3 100644 --- a/packages/server/src/server/agent/providers/cursor-acp-agent.ts +++ b/packages/server/src/server/agent/providers/cursor-acp-agent.ts @@ -92,6 +92,7 @@ export class CursorACPAgentClient extends GenericACPAgentClient { env: options.env, providerId: options.providerId, label: options.label, + usageProvider: "cursor", // cursor-agent publishes slash commands asynchronously via available_commands_update. waitForInitialCommands: true, initialCommandsWaitTimeoutMs: CURSOR_INITIAL_COMMANDS_WAIT_TIMEOUT_MS, diff --git a/packages/server/src/server/agent/providers/generic-acp-agent.ts b/packages/server/src/server/agent/providers/generic-acp-agent.ts index 5a14d99137..37db4d02dd 100644 --- a/packages/server/src/server/agent/providers/generic-acp-agent.ts +++ b/packages/server/src/server/agent/providers/generic-acp-agent.ts @@ -22,6 +22,7 @@ interface GenericACPAgentClientOptions { env?: Record; providerId?: string; label?: string; + usageProvider?: string; waitForInitialCommands?: boolean; initialCommandsWaitTimeoutMs?: number; diagnosticPhaseTimeoutMs?: number; @@ -41,6 +42,7 @@ export class GenericACPAgentClient extends ACPAgentClient { constructor(options: GenericACPAgentClientOptions) { super({ provider: "acp", + usageProvider: options.usageProvider, logger: options.logger, runtimeSettings: { env: options.env, diff --git a/packages/server/src/server/agent/providers/omp/agent.test.ts b/packages/server/src/server/agent/providers/omp/agent.test.ts index a1dc9130a8..c9911a64b8 100644 --- a/packages/server/src/server/agent/providers/omp/agent.test.ts +++ b/packages/server/src/server/agent/providers/omp/agent.test.ts @@ -39,6 +39,19 @@ const ABORTED_TERMINAL_RESPONSE: OmpAgentMessage = { errorMessage: "Interrupted by user", }; +test("an idle follow-up is submitted as a prompt so OMP can start a turn", async () => { + const runtime = new FakeOmp(); + const client = new OmpAgentClient({ logger: createTestLogger(), runtime }); + const session = await client.createSession({ provider: "omp", cwd: process.cwd() }); + onTestFinished(() => session.close()); + const handler = session.tryHandleOutOfBand?.("/follow-up check the build"); + expect(handler).not.toBeNull(); + await handler!.run({ emit: () => {} }); + expect(runtime.latestSession().prompts).toEqual([ + { message: "check the build", imageCount: 0, streamingBehavior: "followUp" }, + ]); +}); + test("OMP ready timeout defaults to 20 seconds and RPC timeout overrides both", () => { expect(resolveOmpProviderOptions({}).runtimeOptions).toMatchObject({ readyTimeoutMs: 20_000, diff --git a/packages/server/src/server/agent/providers/omp/agent.ts b/packages/server/src/server/agent/providers/omp/agent.ts index 5413364cdb..ab82df9475 100644 --- a/packages/server/src/server/agent/providers/omp/agent.ts +++ b/packages/server/src/server/agent/providers/omp/agent.ts @@ -705,6 +705,7 @@ export class OmpAgentSession implements AgentSession { private activeAssistantMessageId: string | null = null; private activeTurnTerminalAssistantMessage: OmpAgentMessage | null = null; private activeTurnStarted = false; + private outOfBandTurnStartAnnounced = false; private activeTurnHasUserMessage = false; private activeNoTurnPromptText: string | null = null; private readonly pendingNoTurnOutputs: Array<{ turnId: string; message: string }> = []; @@ -849,6 +850,20 @@ export class OmpAgentSession implements AgentSession { } async startTurn(prompt: AgentPromptInput, options?: AgentRunOptions): Promise { + return this.startPrompt({ prompt, options }); + } + + private async startPrompt({ + prompt, + options, + streamingBehavior, + announceTurn = false, + }: { + prompt: AgentPromptInput; + options?: AgentRunOptions; + streamingBehavior?: "steer" | "followUp"; + announceTurn?: boolean; + }): Promise { if (this.activeTurnId) { throw new Error("An OMP turn is already active"); } @@ -861,17 +876,26 @@ export class OmpAgentSession implements AgentSession { this.activeAssistantMessageId = null; this.activeTurnTerminalAssistantMessage = null; this.activeTurnStarted = false; + this.outOfBandTurnStartAnnounced = false; this.activeTurnHasUserMessage = false; this.activePromptRequestId = null; this.clearNoTurnBuffers(); this.activeNoTurnPromptText = payload.text; this.usagePoller.startTurn(); + if (announceTurn) { + this.outOfBandTurnStartAnnounced = true; + this.emit({ type: "turn_started", provider: this.provider, turnId }); + } void (async () => { try { if (this.runtimeDead) await this.replaceRuntime(this.currentModeId ?? "full"); if (this.closed) throw new Error("OMP session is closed"); - const ack = await this.runtimeSession.prompt(payload.text, payload.images); + const ack = await this.runtimeSession.prompt({ + message: payload.text, + images: payload.images, + streamingBehavior, + }); this.activePromptRequestId = ack.requestId ?? null; const correlatedResult = ack.requestId ? this.pendingPromptResults.get(ack.requestId) @@ -1191,6 +1215,7 @@ export class OmpAgentSession implements AgentSession { this.activeAssistantMessageId = null; this.activeTurnTerminalAssistantMessage = null; this.activeTurnStarted = false; + this.outOfBandTurnStartAnnounced = false; this.activeTurnHasUserMessage = false; this.pendingClientMessages.length = 0; this.clearNoTurnBuffers(); @@ -1235,12 +1260,13 @@ export class OmpAgentSession implements AgentSession { if (!message) { return null; } + const streamingBehavior = commandName === "steer" ? "steer" : "followUp"; return { run: async () => { - if (commandName === "steer") { - this.runtimeSession.steer(message); + if (!this.activeTurnId && !this.activeTurnStarted) { + await this.startPrompt({ prompt: message, streamingBehavior, announceTurn: true }); } else { - this.runtimeSession.followUp(message); + await this.runtimeSession.prompt({ message, streamingBehavior }); } }, }; @@ -1804,11 +1830,11 @@ export class OmpAgentSession implements AgentSession { case "turn_start": this.activeTurnStarted = true; this.clearNoTurnBuffers(); - this.emit({ - type: "turn_started", - provider: this.provider, - turnId, - }); + if (this.outOfBandTurnStartAnnounced) { + this.outOfBandTurnStartAnnounced = false; + } else { + this.emit({ type: "turn_started", provider: this.provider, turnId }); + } return; case "message_start": this.handleMessageStart(event); diff --git a/packages/server/src/server/agent/providers/omp/cli-runtime.test.ts b/packages/server/src/server/agent/providers/omp/cli-runtime.test.ts index 2deae45850..4c8a81fe46 100644 --- a/packages/server/src/server/agent/providers/omp/cli-runtime.test.ts +++ b/packages/server/src/server/agent/providers/omp/cli-runtime.test.ts @@ -143,6 +143,26 @@ describe("OMP CLI runtime", () => { await expect(result).rejects.toThrow("rejected"); await session.close(); }); + test("sends the prompt streamingBehavior only when one is given", async () => { + const child = createOmpChild(); + const session = await createRuntime(child, []).startSession({ cwd: "/workspace/project" }); + + for (const streamingBehavior of ["followUp", undefined] as const) { + const pending = capturePendingCommand(child, "prompt"); + const result = session.prompt({ message: "after this, update the docs", streamingBehavior }); + const command = await pending; + expect(withoutRequestId(command)).toEqual({ + type: "prompt", + message: "after this, update the docs", + ...(streamingBehavior ? { streamingBehavior } : {}), + }); + child.stdout.write( + `${JSON.stringify({ id: command.id, type: "response", command: "prompt", success: true })}\n`, + ); + await result; + } + await session.close(); + }); test("uses the configured RPC timeout and attributes the pending phase", async () => { vi.useFakeTimers(); const child = createOmpChild(); @@ -438,7 +458,7 @@ describe("OMP CLI runtime", () => { replyToCommands(child, () => undefined); const session = await createRuntime(child).startSession({ cwd: "/workspace/project" }); - await expect(session.prompt("hello")).resolves.toEqual({ requestId: "req_1" }); + await expect(session.prompt({ message: "hello" })).resolves.toEqual({ requestId: "req_1" }); }); test("reports OMP 18.3's late prompt rejection as a failed prompt result", async () => { @@ -455,7 +475,7 @@ describe("OMP CLI runtime", () => { const events: unknown[] = []; session.onEvent((event) => events.push(event)); - await expect(session.prompt("hello")).resolves.toEqual({ requestId: "req_1" }); + await expect(session.prompt({ message: "hello" })).resolves.toEqual({ requestId: "req_1" }); expect(events).toContainEqual({ type: "prompt_result", diff --git a/packages/server/src/server/agent/providers/omp/cli-runtime.ts b/packages/server/src/server/agent/providers/omp/cli-runtime.ts index 75bd79f5f0..5ae9d8f9c4 100644 --- a/packages/server/src/server/agent/providers/omp/cli-runtime.ts +++ b/packages/server/src/server/agent/providers/omp/cli-runtime.ts @@ -15,6 +15,7 @@ import { type OmpRuntime, type OmpRuntimeLaunch, type OmpRuntimeSession, + type OmpPromptInput, type OmpStartSessionInput, } from "./runtime.js"; import { @@ -142,14 +143,12 @@ class OmpCliRuntimeSession implements OmpRuntimeSession { }; } - async prompt( - message: string, - images?: Array<{ type: "image"; data: string; mimeType: string }>, - ): Promise { + async prompt({ message, images, streamingBehavior }: OmpPromptInput): Promise { const { id: requestId, promise } = this.process.startRequest({ type: "prompt", message, ...(images?.length ? { images } : {}), + ...(streamingBehavior ? { streamingBehavior: streamingBehavior } : {}), }); const ack = OmpPromptAckSchema.parse(await promise) ?? {}; return { requestId, ...ack }; @@ -282,13 +281,6 @@ class OmpCliRuntimeSession implements OmpRuntimeSession { await this.request({ type: "steer", message, ...(images?.length ? { images } : {}) }); } - followUp( - message: string, - images?: Array<{ type: "image"; data: string; mimeType: string }>, - ): void { - this.process.send({ type: "follow_up", message, ...(images?.length ? { images } : {}) }); - } - async handoff(customInstructions?: string): Promise { await this.request({ type: "handoff", diff --git a/packages/server/src/server/agent/providers/omp/rpc-types.ts b/packages/server/src/server/agent/providers/omp/rpc-types.ts index b0514c7863..733dedf571 100644 --- a/packages/server/src/server/agent/providers/omp/rpc-types.ts +++ b/packages/server/src/server/agent/providers/omp/rpc-types.ts @@ -535,6 +535,7 @@ export const OmpRpcCommandSchema = z.discriminatedUnion("type", [ type: z.literal("prompt"), message: z.string(), images: z.array(OmpImageContentSchema).optional(), + streamingBehavior: z.enum(["steer", "followUp"]).optional(), }), z.object({ ...OmpCommandBase, diff --git a/packages/server/src/server/agent/providers/omp/runtime.ts b/packages/server/src/server/agent/providers/omp/runtime.ts index 691e907139..523595ed07 100644 --- a/packages/server/src/server/agent/providers/omp/runtime.ts +++ b/packages/server/src/server/agent/providers/omp/runtime.ts @@ -42,13 +42,18 @@ export interface OmpStartSessionInput { extraArgs?: string[]; } +export interface OmpPromptInput { + message: string; + images?: Array<{ type: "image"; data: string; mimeType: string }>; + streamingBehavior?: "steer" | "followUp"; +} + export interface OmpRuntimeSession { readonly environment: Record; onEvent(callback: (event: OmpRuntimeEvent) => void): () => void; - prompt( - message: string, - images?: Array<{ type: "image"; data: string; mimeType: string }>, - ): Promise; + // With streamingBehavior, a busy OMP queues the message instead of rejecting it, and an + // idle OMP starts a run with it. + prompt(input: OmpPromptInput): Promise; compact(customInstructions?: string): Promise; setAutoCompaction(enabled: boolean): Promise; abort(): Promise; @@ -71,10 +76,6 @@ export interface OmpRuntimeSession { message: string, images?: Array<{ type: "image"; data: string; mimeType: string }>, ): Promise; - followUp( - message: string, - images?: Array<{ type: "image"; data: string; mimeType: string }>, - ): void; handoff(customInstructions?: string): Promise; respondToExtensionUiRequest( id: string, diff --git a/packages/server/src/server/agent/providers/omp/test-utils/fake-omp.ts b/packages/server/src/server/agent/providers/omp/test-utils/fake-omp.ts index 58688c245d..02033f8dea 100644 --- a/packages/server/src/server/agent/providers/omp/test-utils/fake-omp.ts +++ b/packages/server/src/server/agent/providers/omp/test-utils/fake-omp.ts @@ -2,6 +2,7 @@ import type { OmpRuntime, OmpRuntimeLaunch, OmpRuntimeSession, + OmpPromptInput, OmpStartSessionInput, } from "../runtime.js"; import type { @@ -133,7 +134,11 @@ export class FakeOmpSession implements OmpRuntimeSession { readonly environment: Record; fastModeResult = { enabled: false, active: false }; readonly setFastModeRequests: boolean[] = []; - readonly prompts: Array<{ message: string; imageCount: number }> = []; + readonly prompts: Array<{ + message: string; + imageCount: number; + streamingBehavior?: "steer" | "followUp"; + }> = []; readonly compactRequests: Array<{ customInstructions?: string }> = []; readonly setAutoCompactionRequests: boolean[] = []; readonly subagentSubscriptionRequests: FakeOmpSubagentSubscriptionLevel[] = []; @@ -143,7 +148,6 @@ export class FakeOmpSession implements OmpRuntimeSession { readonly handoffRequests: Array<{ customInstructions?: string }> = []; readonly steerRequests: Array<{ message: string; imageCount: number }> = []; steerError: Error | null = null; - readonly followUpRequests: Array<{ message: string; imageCount: number }> = []; readonly hostToolSetRequests: OmpRpcHostToolDefinition[][] = []; readonly hostToolResults: OmpRpcHostToolResult[] = []; readonly hostToolUpdates: OmpRpcHostToolUpdate[] = []; @@ -216,14 +220,19 @@ export class FakeOmpSession implements OmpRuntimeSession { }; } - async prompt( - message: string, - images?: Array<{ type: "image"; data: string; mimeType: string }>, - ): Promise { - if (this.state.isStreaming || this.state.isCompacting) { + async prompt({ message, images, streamingBehavior }: OmpPromptInput): Promise { + const busy = this.state.isStreaming || this.state.isCompacting; + if (busy && !streamingBehavior) { throw new Error("Agent is already processing"); } - this.prompts.push({ message, imageCount: images?.length ?? 0 }); + this.prompts.push({ + message, + imageCount: images?.length ?? 0, + ...(streamingBehavior ? { streamingBehavior: streamingBehavior } : {}), + }); + if (busy) { + return {}; + } this.promptWaiters.shift()?.(); const heldPrompt = this.nextHeldPrompt; if (heldPrompt) { @@ -412,13 +421,6 @@ export class FakeOmpSession implements OmpRuntimeSession { if (this.steerError) throw this.steerError; } - followUp( - message: string, - images?: Array<{ type: "image"; data: string; mimeType: string }>, - ): void { - this.followUpRequests.push({ message, imageCount: images?.length ?? 0 }); - } - sendHostToolResult(result: OmpRpcHostToolResult): void { this.hostToolResults.push(result); this.hostToolResultWaiters.shift()?.(result); diff --git a/packages/server/src/server/agent/providers/opencode/bridge-input-errors.test.ts b/packages/server/src/server/agent/providers/opencode/bridge-input-errors.test.ts new file mode 100644 index 0000000000..f9a776d168 --- /dev/null +++ b/packages/server/src/server/agent/providers/opencode/bridge-input-errors.test.ts @@ -0,0 +1,202 @@ +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { expect, test } from "vitest"; +import { createTestLogger } from "../../../../test-utils/test-logger.js"; +import { createPaseoToolCatalog } from "../../tools/paseo-tools.js"; +import { AgentManager } from "../../agent-manager.js"; +import { AgentStorage } from "../../agent-storage.js"; +import { ProviderSnapshotManager } from "../../provider-snapshot-manager.js"; +import { OpenCodeBridge } from "./bridge.js"; +import { ScheduleService } from "../../../schedule/service.js"; +import { createWorkspaceScriptsService } from "../../../session/workspace-scripts/workspace-scripts-service.js"; +import { + FileBackedProjectRegistry, + FileBackedWorkspaceRegistry, +} from "../../../workspace-registry.js"; +import { createServiceProxySubsystem } from "../../../service-proxy.js"; +import { WorkspaceScriptRuntimeStore } from "../../../workspace-script-runtime-store.js"; +import { createTerminalManager } from "../../../../terminal/terminal-manager.js"; + +test("bridge preserves tool failure messages and classifies invalid requests as client errors", async () => { + const paseoHome = await mkdtemp(path.join(tmpdir(), "paseo-7045-")); + const logger = createTestLogger(); + const providerSnapshotManager = new ProviderSnapshotManager({ logger }); + const agentManager = new AgentManager({ logger }); + const agentStorage = new AgentStorage(paseoHome, logger); + const unused = async (): Promise => { + throw new Error("Unexpected workspace or agent creation"); + }; + const scheduleService = new ScheduleService({ + paseoHome, + logger, + agentManager, + agentStorage, + createAgent: unused, + createDirectoryWorkspace: unused, + createPaseoWorktreeWorkspace: unused, + archiveWorkspace: unused, + runner: async () => ({ agentId: null, output: "done" }), + }); + const workspaceScripts = createWorkspaceScriptsService({ + serviceProxy: createServiceProxySubsystem({ logger }), + scriptRuntimeStore: new WorkspaceScriptRuntimeStore(), + terminalManager: createTerminalManager(), + workspaceRegistry: new FileBackedWorkspaceRegistry( + path.join(paseoHome, "workspaces.json"), + logger, + ), + projectRegistry: new FileBackedProjectRegistry(path.join(paseoHome, "projects.json"), logger), + workspaceGitService: { + peekSnapshot: () => { + throw new Error("Unexpected git lookup"); + }, + }, + getDaemonTcpPort: null, + getDaemonTcpHost: null, + serviceProxyPublicBaseUrl: null, + resolveScriptHealth: null, + logger, + emit: () => {}, + publishStatusUpdate: () => {}, + spawnWorkspaceScript: unused, + assertAutomationAllowed: unused, + }); + const catalog = createPaseoToolCatalog({ + workspaceScripts, + agentManager, + agentStorage, + scheduleService, + providerSnapshotManager, + logger, + }); + const bridge = new OpenCodeBridge({ paseoHome, logger }); + try { + await bridge.start(); + bridge.setManifestCatalog(catalog); + bridge.bindSession({ sessionId: "diagnosis", env: {}, tools: catalog }); + const config = JSON.parse(bridge.decorateServerEnv({}).OPENCODE_CONFIG_CONTENT); + const { baseUrl, token } = config.plugin[0][1]; + const completed = await scheduleService.create({ + prompt: "Run once", + name: "Daily check", + cadence: { type: "cron", expression: "0 0 * * *" }, + target: { type: "new-agent", config: { provider: "codex", cwd: paseoHome } }, + maxRuns: 1, + runOnCreate: true, + }); + await scheduleService.tick(); + expect((await scheduleService.inspect(completed.id)).status).toBe("completed"); + const manifest = await fetch(`${baseUrl}/_internal/opencode/tools`, { + headers: { Authorization: `Bearer ${token}` }, + }); + const { tools } = await manifest.json(); + expect( + tools.find((tool: { name: string }) => tool.name === "create_agent").inputSchema.properties, + ).not.toHaveProperty("background"); + expect( + tools.find((tool: { name: string }) => tool.name === "update_schedule").inputSchema + .properties, + ).not.toHaveProperty("every"); + await writeFile( + path.join(paseoHome, "schedules", "broken.json"), + JSON.stringify({ id: "broken", prompt: 17 }), + ); + const observed = []; + for (const [tool, body, expected] of [ + [ + "create_agent", + JSON.stringify({ title: "Probe", provider: "opencode", initialPrompt: "Reply OK" }), + 400, + ], + ["create_agent", "{", 400], + ["nonexistent_tool", "{}", 404], + ["inspect_schedule", JSON.stringify({ id: "missing" }), 400], + ...["pause_schedule", "resume_schedule", "run_schedule_once"].map( + (action) => [action, JSON.stringify({ id: completed.id }), 400] as const, + ), + [ + "create_schedule", + JSON.stringify({ prompt: "Probe", provider: "codex", cron: "not-a-cron" }), + 400, + ], + [ + "create_schedule", + JSON.stringify({ + prompt: "Probe", + provider: "codex", + cron: "0 0 * * *", + timezone: "Mars/Phobos", + }), + 400, + ], + ["update_schedule", JSON.stringify({ id: completed.id, name: "" }), 200], + ["update_schedule", JSON.stringify({ id: completed.id, cron: "0 0 30 2 *" }), 400], + ["update_schedule", JSON.stringify({ id: completed.id, expiresIn: "not-a-duration" }), 400], + ["send_agent_prompt", JSON.stringify({ agentId: "missing", prompt: "Hello" }), 400], + ["inspect_schedule", JSON.stringify({ id: "broken" }), 500], + ["list_workspace_scripts", JSON.stringify({ workspaceId: "wks_typo" }), 500], + [ + "start_workspace_script", + JSON.stringify({ workspaceId: "wks_typo", scriptName: "web" }), + 500, + ], + ] as const) { + const response = await fetch( + `${baseUrl}/_internal/opencode/sessions/diagnosis/tools/${tool}`, + { + method: "POST", + headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, + body, + }, + ); + observed.push({ + tool, + body, + expected, + status: response.status, + payload: await response.json(), + }); + } + + for (const result of observed) expect.soft(result.status, result.body).toBe(result.expected); + expect((await scheduleService.inspect(completed.id)).name).toBeNull(); + expect( + tools.find((tool: { name: string }) => tool.name === "update_schedule").inputSchema.properties + .name, + ).not.toHaveProperty("minLength"); + for (const result of observed.slice(14)) { + expect.soft(result.payload).toEqual({ error: "Workspace not found: wks_typo" }); + } + expect.soft(observed[12].payload).toEqual({ error: "Agent not found: missing" }); + expect.soft(observed[13].payload).toEqual({ error: expect.stringContaining('"prompt"') }); + expect.soft(observed[13].payload).not.toHaveProperty("issues"); + expect(observed[3].payload).toEqual({ error: "Schedule not found: missing" }); + for (const result of observed.slice(4, 7)) + expect(result.payload).toEqual({ error: `Schedule ${completed.id} is already completed` }); + for (const [index, field] of [ + [7, "cron"], + [8, "timezone"], + [10, "cron"], + [11, "expiresIn"], + ] as const) { + expect(observed[index].payload).toMatchObject({ + error: "Invalid tool input", + issues: expect.arrayContaining([expect.objectContaining({ path: [field] })]), + }); + } + expect(observed[0].payload).toMatchObject({ + error: "Invalid tool input", + issues: expect.arrayContaining([ + expect.objectContaining({ + path: ["provider"], + message: expect.stringContaining("provider/model"), + }), + ]), + }); + } finally { + await bridge.close(); + await providerSnapshotManager.shutdown(); + await rm(paseoHome, { recursive: true, force: true }); + } +}); diff --git a/packages/server/src/server/agent/providers/opencode/bridge-plugin.mjs b/packages/server/src/server/agent/providers/opencode/bridge-plugin.mjs index 82e4166492..6b3a38da9c 100644 --- a/packages/server/src/server/agent/providers/opencode/bridge-plugin.mjs +++ b/packages/server/src/server/agent/providers/opencode/bridge-plugin.mjs @@ -13,8 +13,11 @@ export default async function paseoPlugin(input, options) { }); const payload = await response.json().catch(() => ({})); if (!response.ok) { - const error = new Error(payload.error ?? `Paseo OpenCode bridge failed: ${response.status}`); + const detail = formatErrorDetail(payload.issues ?? payload.details); + const message = payload.error ?? `Paseo OpenCode bridge failed: ${response.status}`; + const error = new Error(detail ? `${message}: ${detail}` : message); error.status = response.status; + if (detail) error.details = detail; throw error; } return payload; @@ -79,6 +82,17 @@ export default async function paseoPlugin(input, options) { }; } +function formatErrorDetail(details) { + if (!Array.isArray(details)) return typeof details === "string" ? details : ""; + return details + .map((issue) => { + const path = + Array.isArray(issue?.path) && issue.path.length > 0 ? issue.path.join(".") : "input"; + return `${path}: ${issue?.message ?? "Invalid value"}`; + }) + .join("; "); +} + function logPluginError(stage, context, error) { console.error(`[paseo-opencode-plugin] ${stage} failed`, { ...context, diff --git a/packages/server/src/server/agent/providers/opencode/bridge.test.ts b/packages/server/src/server/agent/providers/opencode/bridge.test.ts index 076cbe94bd..4b42410845 100644 --- a/packages/server/src/server/agent/providers/opencode/bridge.test.ts +++ b/packages/server/src/server/agent/providers/opencode/bridge.test.ts @@ -9,6 +9,7 @@ import { afterEach, describe, expect, test, vi } from "vitest"; import { createTestLogger } from "../../../../test-utils/test-logger.js"; import type { PaseoToolCatalog } from "../../tools/types.js"; +import { PaseoToolInputError } from "../../tools/types.js"; import { createPaseoToolCatalog } from "../../tools/paseo-tools.js"; import { AgentManager } from "../../agent-manager.js"; import { AgentStorage } from "../../agent-storage.js"; @@ -36,8 +37,12 @@ function createCatalog(): PaseoToolCatalog { description: "Returns the supplied value.", inputSchema: { value: z.string() }, async handler(input: unknown) { - const parsed = z.object({ value: z.string() }).parse(input); - return { content: [{ type: "text", text: parsed.value }] }; + const parsed = z + .object({ value: z.string().min(2, "value must be at least 2 characters") }) + .strict() + .safeParse(input); + if (!parsed.success) throw new PaseoToolInputError(parsed.error.message, parsed.error.issues); + return { content: [{ type: "text", text: parsed.data.value }] }; }, }; const tools = new Map([[tool.name, tool]]); @@ -193,6 +198,13 @@ describe("OpenCodeBridge", () => { expect(asked).toEqual([ { permission: "paseo_echo_context", patterns: ["*"], always: ["*"], metadata: {} }, ]); + asked.length = 0; + await expect( + hooks.tool.paseo_echo_context.execute( + { value: "x" }, + { sessionID: "ses_one", ask: async () => undefined }, + ), + ).rejects.toThrow("value: value must be at least 2 characters"); executedInputs.length = 0; await expect( @@ -357,12 +369,17 @@ describe("OpenCodeBridge", () => { body: JSON.stringify(input), }, ); - expect(response.status).toBe(500); + expect(response.status).toBe(400); const payload = await response.json(); - expect(payload.error).toContain("provider must be provider/model"); + expect(payload).toMatchObject({ error: "Invalid tool input", issues: expect.any(Array) }); await expect( tools.get("paseo_create_agent")!.execute(input, { sessionID: "caller" }), - ).rejects.toMatchObject({ message: payload.error, status: 500 }); + ).rejects.toMatchObject({ + status: 400, + message: expect.stringContaining( + "Invalid tool input: provider: provider must be provider/model, for example codex/gpt-5.4", + ), + }); } finally { await dispose(); } @@ -550,6 +567,104 @@ describe("OpenCodeBridge", () => { } }); + test("returns actionable 4xx responses for invalid bridge requests", async () => { + const paseoHome = await mkdtemp(path.join(tmpdir(), "paseo-opencode-bridge-errors-")); + temporaryDirectories.push(paseoHome); + const bridge = new OpenCodeBridge({ paseoHome, logger: createTestLogger() }); + await bridge.start(); + const catalog = createCatalog(); + bridge.bindSession({ sessionId: "ses_errors", env: {}, tools: catalog }); + const expectedErrorCatalog: PaseoToolCatalog = { + tools: new Map([ + [ + "expected_error", + { + name: "expected_error", + description: "Throws an expected tool error.", + inputSchema: {}, + async handler() { + throw new PaseoToolInputError("Agent not found: missing-agent"); + }, + }, + ], + [ + "internal_error", + { + name: "internal_error", + description: "Throws an internal error.", + inputSchema: {}, + async handler() { + throw new Error("Provider unavailable"); + }, + }, + ], + ]), + getTool(name) { + return this.tools.get(name); + }, + async executeTool(name, input, context) { + const tool = this.tools.get(name); + if (!tool) throw new Error(`Unknown tool: ${name}`); + return tool.handler(input, context ?? {}); + }, + }; + bridge.bindSession({ sessionId: "ses_expected_error", env: {}, tools: expectedErrorCatalog }); + + try { + const plugin = readPluginOptions(bridge.decorateServerEnv({})); + const headers = { + Authorization: `Bearer ${plugin.token}`, + "Content-Type": "application/json", + }; + const invalidInput = await fetch( + `${plugin.baseUrl}/_internal/opencode/sessions/ses_errors/tools/echo_context`, + { method: "POST", headers, body: JSON.stringify({ value: "ok", extra: true }) }, + ); + expect(invalidInput.status).toBe(400); + expect(await invalidInput.json()).toEqual({ + error: "Invalid tool input", + issues: [ + { + code: "unrecognized_keys", + keys: ["extra"], + path: [], + message: 'Unrecognized key: "extra"', + }, + ], + }); + + const unknownTool = await fetch( + `${plugin.baseUrl}/_internal/opencode/sessions/ses_errors/tools/missing`, + { method: "POST", headers, body: "{}" }, + ); + expect(unknownTool.status).toBe(404); + expect(await unknownTool.json()).toEqual({ error: "Paseo tool not found: missing" }); + + const malformedBody = await fetch( + `${plugin.baseUrl}/_internal/opencode/sessions/ses_errors/tools/echo_context`, + { method: "POST", headers, body: "{" }, + ); + expect(malformedBody.status).toBe(400); + expect(await malformedBody.json()).toEqual({ error: "Malformed JSON request body" }); + + const expectedError = await fetch( + `${plugin.baseUrl}/_internal/opencode/sessions/ses_expected_error/tools/expected_error`, + { method: "POST", headers, body: "{}" }, + ); + expect(expectedError.status).toBe(400); + expect(await expectedError.json()).toEqual({ error: "Agent not found: missing-agent" }); + + const internalError = await fetch( + `${plugin.baseUrl}/_internal/opencode/sessions/ses_expected_error/tools/internal_error`, + { method: "POST", headers, body: "{}" }, + ); + expect(internalError.status).toBe(500); + expect(await internalError.json()).toEqual({ error: "Provider unavailable" }); + } finally { + await bridge.close(); + } + }); + test("preserves user OpenCode config while installing one content-addressed plugin", async () => { const paseoHome = await mkdtemp(path.join(tmpdir(), "paseo-opencode-bridge-config-")); temporaryDirectories.push(paseoHome); diff --git a/packages/server/src/server/agent/providers/opencode/bridge.ts b/packages/server/src/server/agent/providers/opencode/bridge.ts index 52a2cd3612..4e9902bd7a 100644 --- a/packages/server/src/server/agent/providers/opencode/bridge.ts +++ b/packages/server/src/server/agent/providers/opencode/bridge.ts @@ -11,11 +11,22 @@ import { addModelVisibleStructuredContent, serializePaseoToolInputParameters, } from "../../tools/paseo-tool-serialization.js"; -import type { PaseoToolCatalog } from "../../tools/types.js"; +import { PaseoToolInputError, type PaseoToolCatalog } from "../../tools/types.js"; const INTERNAL_PREFIX = "/_internal/opencode"; const MAX_REQUEST_BYTES = 1024 * 1024; +class OpenCodeBridgeClientError extends Error { + constructor( + readonly status: number, + message: string, + readonly details?: unknown, + ) { + super(message); + this.name = "OpenCodeBridgeClientError"; + } +} + interface OpenCodeBridgeOptions { paseoHome: string; logger: Logger; @@ -187,10 +198,26 @@ export class OpenCodeBridge { sendJson(response, 404, { error: "Not found" }); } catch (error) { this.logger.warn({ err: error }, "OpenCode bridge request failed"); + const failure = error instanceof Error ? error : new Error(String(error)); if (!response.headersSent) { - sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) }); + if (error instanceof OpenCodeBridgeClientError) { + sendJson(response, error.status, { + error: error.message, + ...(error.details === undefined ? {} : { details: error.details }), + }); + } else if (error instanceof PaseoToolInputError) { + sendJson( + response, + error.status, + error.issues + ? { error: "Invalid tool input", issues: error.issues } + : { error: error.message }, + ); + } else { + sendJson(response, 500, { error: failure.message }); + } } else { - response.destroy(error instanceof Error ? error : new Error(String(error))); + response.destroy(failure); } } } @@ -224,6 +251,9 @@ export class OpenCodeBridge { sendJson(input.response, 403, { error: "Paseo tools are disabled for this session" }); return; } + if (!binding.tools.getTool(input.toolName)) { + throw new OpenCodeBridgeClientError(404, `Paseo tool not found: ${input.toolName}`); + } const body = await readJsonBody(input.request); const controller = new AbortController(); input.request.once("aborted", () => @@ -349,11 +379,17 @@ async function readJsonBody(request: IncomingMessage): Promise { for await (const chunk of request) { const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); size += buffer.byteLength; - if (size > MAX_REQUEST_BYTES) throw new Error("OpenCode bridge request body is too large"); + if (size > MAX_REQUEST_BYTES) { + throw new OpenCodeBridgeClientError(413, "OpenCode bridge request body is too large"); + } chunks.push(buffer); } if (chunks.length === 0) return {}; - return JSON.parse(Buffer.concat(chunks).toString("utf8")) as unknown; + try { + return JSON.parse(Buffer.concat(chunks).toString("utf8")) as unknown; + } catch { + throw new OpenCodeBridgeClientError(400, "Malformed JSON request body"); + } } function sendJson(response: ServerResponse, status: number, body: unknown): void { diff --git a/packages/server/src/server/agent/providers/opencode/v2/bridge-plugin.mjs b/packages/server/src/server/agent/providers/opencode/v2/bridge-plugin.mjs index 6b845d19ed..1664c0b59f 100644 --- a/packages/server/src/server/agent/providers/opencode/v2/bridge-plugin.mjs +++ b/packages/server/src/server/agent/providers/opencode/v2/bridge-plugin.mjs @@ -15,12 +15,14 @@ export default { if (allowMissing && response.status === 404) return null; if (!response.ok) { const payload = await response.json().catch(() => null); + const detail = formatErrorDetail(payload?.issues ?? payload?.details); const message = typeof payload?.error === "string" && payload.error ? payload.error : `Paseo tool bridge returned HTTP ${response.status}`; - const error = new Error(message); + const error = new Error(detail ? `${message}: ${detail}` : message); error.status = response.status; + if (detail) error.details = detail; throw error; } return response.json(); @@ -78,6 +80,17 @@ export default { }, }; +function formatErrorDetail(details) { + if (!Array.isArray(details)) return typeof details === "string" ? details : ""; + return details + .map((issue) => { + const path = + Array.isArray(issue?.path) && issue.path.length > 0 ? issue.path.join(".") : "input"; + return `${path}: ${issue?.message ?? "Invalid value"}`; + }) + .join("; "); +} + // OpenCode tool content is text or a file; MCP images arrive as base64 data. function toOpenCodeContent(part) { if (part.type === "image") { diff --git a/packages/server/src/server/agent/providers/opencode/v2/timeline.test.ts b/packages/server/src/server/agent/providers/opencode/v2/timeline.test.ts index 82236de2b8..d86b9a7dcb 100644 --- a/packages/server/src/server/agent/providers/opencode/v2/timeline.test.ts +++ b/packages/server/src/server/agent/providers/opencode/v2/timeline.test.ts @@ -92,7 +92,7 @@ test("a tool's image output is shown as an image, not as base64 in the tool outp }, { type: "assistant_message", - text: expect.stringMatching(/^!\[Image\]\(file:\/\/.*paseo-attachments.*\.png\)$/), + text: expect.stringMatching(/^\n\n!\[Image\]\(file:\/\/.*paseo-attachments.*\.png\)\n\n$/), }, ]); expect(timeline.messages([screenshot])).toEqual([]); diff --git a/packages/server/src/server/agent/providers/provider-image-output.test.ts b/packages/server/src/server/agent/providers/provider-image-output.test.ts index 0c32af04fe..b4f4784a66 100644 --- a/packages/server/src/server/agent/providers/provider-image-output.test.ts +++ b/packages/server/src/server/agent/providers/provider-image-output.test.ts @@ -1,5 +1,7 @@ import { existsSync, rmSync } from "node:fs"; import path from "node:path"; +import MarkdownIt from "markdown-it"; +import { projectTimelineRows } from "../timeline-projection.js"; import { describe, expect, test } from "vitest"; import { @@ -44,7 +46,7 @@ describe("isProviderImageMarkdown", () => { ); expect(markdown).toBe( - `![Image](file:///C:/Users/me/AppData/Local/Temp/paseo-attachments/${HASH}.png)`, + `\n\n![Image](file:///C:/Users/me/AppData/Local/Temp/paseo-attachments/${HASH}.png)\n\n`, ); expect(isProviderImageMarkdown(markdown)).toBe(true); }); @@ -53,20 +55,20 @@ describe("isProviderImageMarkdown", () => { const markdown = renderImageMarkdown("/home/user/Projects/Project With Spaces/screenshot.png"); expect(markdown).toBe( - "![Image](file:///home/user/Projects/Project%20With%20Spaces/screenshot.png)", + "\n\n![Image](file:///home/user/Projects/Project%20With%20Spaces/screenshot.png)\n\n", ); }); test("encodes URI-significant characters in POSIX file paths", () => { const markdown = renderImageMarkdown("/tmp/screenshot#1?draft.png"); - expect(markdown).toBe("![Image](file:///tmp/screenshot%231%3Fdraft.png)"); + expect(markdown).toBe("\n\n![Image](file:///tmp/screenshot%231%3Fdraft.png)\n\n"); }); test("preserves double-leading slashes in POSIX file paths", () => { const markdown = renderImageMarkdown("//tmp/screenshot#1.png"); - expect(markdown).toBe("![Image](file:////tmp/screenshot%231.png)"); + expect(markdown).toBe("\n\n![Image](file:////tmp/screenshot%231.png)\n\n"); }); test.each([ @@ -77,7 +79,7 @@ describe("isProviderImageMarkdown", () => { "file://server/share/shot%3Fdraft.png", ], ])("encodes %s image paths as file URIs", (_label, imagePath, expectedSource) => { - expect(renderImageMarkdown(imagePath)).toBe(`![Image](${expectedSource})`); + expect(renderImageMarkdown(imagePath)).toBe(`\n\n![Image](${expectedSource})\n\n`); }); test("rejects user-authored markdown that is not a materialized attachment", () => { @@ -113,3 +115,52 @@ describe("materializeProviderImage", () => { } }); }); + +describe("provider image paragraph boundaries", () => { + test.each(["", "\n"])("keeps image outputs separate from prose ending in %j", (ending) => { + const image = renderProviderImageOutputAsAssistantMarkdown({ + url: "https://example.com/chart.png", + }); + if (!image) throw new Error("Expected image output"); + const projected = projectTimelineRows({ + mode: "projected", + rows: [ + { + seq: 1, + timestamp: "2026-10-16T15:08:23Z", + item: { type: "assistant_message", text: `Before${ending}`, messageId: "commentary" }, + }, + { seq: 2, timestamp: "2026-10-16T15:08:24Z", item: image }, + { + seq: 3, + timestamp: "2026-10-16T15:08:25Z", + item: { type: "assistant_message", text: "After" }, + }, + ], + }); + expect(projected).toHaveLength(1); + const item = projected[0]!.item; + if (item.type !== "assistant_message") throw new Error("Expected assistant message"); + const paragraphs = new MarkdownIt() + .parse(item.text, {}) + .filter((token) => token.type === "inline"); + expect(paragraphs.map((token) => token.content)).toEqual([ + "Before", + "![Image](https://example.com/chart.png)", + "After", + ]); + }); +}); + +test("materialized image output retains paragraph boundaries and attachment recognition", () => { + const item = renderProviderImageOutputAsAssistantMarkdown( + { data: "YWJjMTIz", mimeType: "image/png" }, + { materialize: () => ({ path: `/tmp/paseo-attachments/${HASH}.png` }) }, + ); + expect(item).toEqual({ + type: "assistant_message", + text: `\n\n![Image](file:///tmp/paseo-attachments/${HASH}.png)\n\n`, + }); + if (!item || item.type !== "assistant_message") throw new Error("Expected image output"); + expect(isProviderImageMarkdown(item.text)).toBe(true); +}); diff --git a/packages/server/src/server/agent/providers/provider-image-output.ts b/packages/server/src/server/agent/providers/provider-image-output.ts index 387158b32f..ba8843918b 100644 --- a/packages/server/src/server/agent/providers/provider-image-output.ts +++ b/packages/server/src/server/agent/providers/provider-image-output.ts @@ -108,7 +108,7 @@ const PROVIDER_IMAGE_MARKDOWN = new RegExp( ); export function isProviderImageMarkdown(text: string): boolean { - return PROVIDER_IMAGE_MARKDOWN.test(text); + return PROVIDER_IMAGE_MARKDOWN.test(text.trimStart()); } interface RenderProviderImageOutputOptions { @@ -178,7 +178,8 @@ export function renderProviderImageOutputAsAssistantMarkdown( const altText = escapeMarkdownImageAlt(nonEmptyString(image.altText) ?? "Image"); return { type: "assistant_message", - text: `![${altText}](${escapeMarkdownImageSource(source)})`, + // Image outputs remain standalone paragraphs when adjacent assistant chunks merge. + text: `\n\n![${altText}](${escapeMarkdownImageSource(source)})\n\n`, }; } @@ -208,6 +209,6 @@ export function renderProviderImageOutputAsAssistantMarkdown( const altText = escapeMarkdownImageAlt(nonEmptyString(image.altText) ?? "Image"); return { type: "assistant_message", - text: `![${altText}](${escapeMarkdownImageSource(materialized.path)})`, + text: `\n\n![${altText}](${escapeMarkdownImageSource(materialized.path)})\n\n`, }; } diff --git a/packages/server/src/server/agent/tools/paseo-tools.ts b/packages/server/src/server/agent/tools/paseo-tools.ts index dae9c16f4f..68e9d99a40 100644 --- a/packages/server/src/server/agent/tools/paseo-tools.ts +++ b/packages/server/src/server/agent/tools/paseo-tools.ts @@ -25,7 +25,7 @@ import { import { curateAgentActivity } from "../activity-curator.js"; import { selectItemsByProjectedLimit } from "../timeline-projection.js"; import type { AgentStorage } from "../agent-storage.js"; -import { ensureAgentLoaded } from "../agent-loading.js"; +import { AgentNotFoundError, ensureAgentLoaded } from "../agent-loading.js"; import { isStoredAgentProviderAvailable } from "../../persistence-hooks.js"; import { archiveByScope, @@ -40,7 +40,12 @@ import { everyMsToFiveFieldCron } from "@getpaseo/protocol/schedule/cadence"; import { expandUserPath, isSameOrDescendantPath, resolvePathFromBase } from "../../path-utils.js"; import type { TerminalManager } from "../../../terminal/terminal-manager.js"; import type { CreatePaseoWorktreeWorkflowFn } from "../../worktree-session.js"; -import type { ScheduleService } from "../../schedule/service.js"; +import { computeNextRunAt, validateScheduleCadence } from "../../schedule/cron.js"; +import { + ScheduleNotFoundError, + ScheduleStateConflictError, + type ScheduleService, +} from "../../schedule/service.js"; import { ScheduleRunSchema, ScheduleSummarySchema, @@ -91,12 +96,13 @@ import { } from "../../worktree/commands.js"; import { registerBrowserTools } from "../../browser-tools/tools.js"; import type { BrowserToolsBroker } from "../../browser-tools/broker.js"; -import type { - PaseoToolCatalog, - PaseoToolConfig, - PaseoToolDefinition, - PaseoToolExecutionContext, - PaseoToolResult, +import { + PaseoToolInputError, + type PaseoToolCatalog, + type PaseoToolConfig, + type PaseoToolDefinition, + type PaseoToolExecutionContext, + type PaseoToolResult, } from "./types.js"; import type { ProviderPaseoToolsPolicy } from "@getpaseo/protocol/provider-config"; import { isPaseoToolEnabled } from "../paseo-tool-policy.js"; @@ -229,7 +235,7 @@ function assertOptionsAbsent( message: string, ): void { if (options.some(([, value]) => value !== undefined)) { - throw new Error(message); + throw new PaseoToolInputError(message); } } @@ -265,7 +271,7 @@ function resolveWorkspaceWorktreeTarget(input: WorkspaceWorktreeOptions): Worksp }; case "checkout-branch": if (!input.branch) { - throw new Error("branch is required for checkout-branch mode"); + throw new PaseoToolInputError("branch is required for checkout-branch mode"); } assertOptionsAbsent( [ @@ -279,7 +285,7 @@ function resolveWorkspaceWorktreeTarget(input: WorkspaceWorktreeOptions): Worksp return { action: "checkout", refName: input.branch }; case "checkout-pr": if (input.prNumber === undefined) { - throw new Error("prNumber is required for checkout-pr mode"); + throw new PaseoToolInputError("prNumber is required for checkout-pr mode"); } assertOptionsAbsent( [ @@ -355,7 +361,7 @@ function resolveScheduleProviderAndModel(params: { const provider = providerInput.slice(0, slashIndex).trim(); const model = providerInput.slice(slashIndex + 1).trim(); if (!provider || !model) { - throw new Error("provider must be or /"); + throw new PaseoToolInputError("provider must be or /"); } return { @@ -372,7 +378,7 @@ function resolveScheduleUpdateProviderAndModel(params: { const modelInput = typeof params.model === "string" ? params.model.trim() : params.model; if (params.model !== undefined && modelInput === "") { - throw new Error("model cannot be empty"); + throw new PaseoToolInputError("model cannot be empty"); } if (!providerInput) { @@ -390,13 +396,13 @@ function resolveScheduleUpdateProviderAndModel(params: { const provider = providerInput.slice(0, slashIndex).trim(); const modelFromProvider = providerInput.slice(slashIndex + 1).trim(); if (!provider || !modelFromProvider) { - throw new Error("provider must be or /"); + throw new PaseoToolInputError("provider must be or /"); } if (params.model === null) { - throw new Error("provider specifies a model but model is null"); + throw new PaseoToolInputError("provider specifies a model but model is null"); } if (typeof modelInput === "string" && modelInput !== modelFromProvider) { - throw new Error("Conflicting model values provided"); + throw new PaseoToolInputError("Conflicting model values provided"); } return { @@ -438,26 +444,82 @@ function normalizeScheduleTimeZoneArg(value: string | undefined): string | undef return normalizeScheduleCadenceArg(value); } +function addScheduleCadenceIssue( + input: { cron?: string; timezone?: string }, + context: z.RefinementCtx, +): void { + const expression = input.cron?.trim(); + if (!expression) { + return; + } + const cadence: ScheduleCadence = { + type: "cron", + expression, + ...(input.timezone !== undefined ? { timezone: input.timezone.trim() } : {}), + }; + try { + validateScheduleCadence(cadence); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + context.addIssue({ + code: "custom", + path: [message.startsWith("Invalid cron time zone:") ? "timezone" : "cron"], + message, + }); + return; + } + try { + computeNextRunAt(cadence, new Date()); + } catch (error) { + context.addIssue({ + code: "custom", + path: ["cron"], + message: error instanceof Error ? error.message : String(error), + }); + } +} + +function parseToolArguments(schema: z.ZodType, input: unknown): T { + const result = schema.safeParse(input); + if (!result.success) { + throw new PaseoToolInputError(result.error.message, result.error.issues); + } + return result.data; +} + +function addScheduleDurationIssue(value: string, context: z.RefinementCtx): void { + try { + parseDurationString(value); + } catch (error) { + context.addIssue({ + code: "custom", + message: error instanceof Error ? error.message : String(error), + }); + } +} + +const scheduleDurationInputSchema = z.string().superRefine(addScheduleDurationIssue); + function resolveScheduleUpdateCadence(input: ScheduleUpdateToolInput): ScheduleCadence | undefined { const every = normalizeScheduleCadenceArg(input.every); const cron = normalizeScheduleCadenceArg(input.cron); const timeZone = normalizeScheduleTimeZoneArg(input.timezone); if (every !== undefined && cron !== undefined) { - throw new Error("Specify at most one of every or cron"); + throw new PaseoToolInputError("Specify at most one of every or cron"); } if (timeZone !== undefined && cron === undefined) { - throw new Error("timezone can only be used with cron"); + throw new PaseoToolInputError("timezone can only be used with cron"); } if (every !== undefined) { // COMPAT(scheduleEveryInput): accept the old hidden field and canonicalize it before write. // Added in v0.2.0; remove after 2027-01-17. - const everyMs = parseDurationString(every); + const everyMs = parseDurationString(parseToolArguments(scheduleDurationInputSchema, every)); const expression = everyMsToFiveFieldCron(everyMs); if (expression) { return { type: "cron", expression }; } - throw new Error(`${every} cannot be represented faithfully by five-field cron`); + throw new PaseoToolInputError(`${every} cannot be represented faithfully by five-field cron`); } if (cron !== undefined) { return { @@ -471,7 +533,7 @@ function resolveScheduleUpdateCadence(input: ScheduleUpdateToolInput): ScheduleC function resolveScheduleUpdateExpiresAt(input: ScheduleUpdateToolInput): string | null | undefined { if (input.expiresIn !== undefined && input.clearExpires) { - throw new Error("Specify at most one of expiresIn or clearExpires"); + throw new PaseoToolInputError("Specify at most one of expiresIn or clearExpires"); } if (input.expiresIn !== undefined) { return new Date(Date.now() + parseDurationString(input.expiresIn)).toISOString(); @@ -593,7 +655,11 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase typeof (inputSchema as { safeParseAsync?: unknown }).safeParseAsync === "function" ? (inputSchema as z.ZodType) : z.object(inputSchema as z.ZodRawShape).passthrough(); - return schema.parseAsync(input); + const result = await schema.safeParseAsync(input); + if (!result.success) { + throw new PaseoToolInputError(result.error.message, result.error.issues); + } + return result.data; }; const tools = new Map(); @@ -627,7 +693,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase ): Promise { const tool = tools.get(name); if (!tool) { - throw new Error(`Paseo tool not found: ${name}`); + throw new PaseoToolInputError(`Paseo tool not found: ${name}`); } return tool.handler(await parseToolInput(tool, input), context); }, @@ -639,7 +705,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase }): ScheduleCadence => { const expression = input.cron?.trim() ?? ""; if (!expression) { - throw new Error("cron is required"); + throw new PaseoToolInputError("cron is required"); } const timezone = normalizeScheduleTimeZoneArg(input.timezone); return { @@ -661,7 +727,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase } const parentAgent = agentManager.getAgent(callerAgentId); if (!parentAgent) { - throw new Error(`Parent agent ${callerAgentId} not found`); + throw new PaseoToolInputError(`Parent agent ${callerAgentId} not found`); } return parentAgent; }; @@ -690,9 +756,9 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase const trimmedCwd = requestedCwd?.trim(); if (!trimmedCwd) { if (opts?.required) { - throw new Error("cwd is required"); + throw new PaseoToolInputError("cwd is required"); } - throw new Error("cwd is required outside an agent-scoped session"); + throw new PaseoToolInputError("cwd is required outside an agent-scoped session"); } return expandUserPath(trimmedCwd); @@ -707,11 +773,12 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase } if (!options.ensureWorkspaceForCreate) { - throw new Error( - callerAgentId - ? `Caller agent ${callerAgentId} has no workspace and workspace minting is not configured` - : "workspaceId is required outside an agent-scoped session", - ); + if (callerAgentId) { + throw new Error( + `Caller agent ${callerAgentId} has no workspace and workspace minting is not configured`, + ); + } + throw new PaseoToolInputError("workspaceId is required outside an agent-scoped session"); } return options.ensureWorkspaceForCreate(resolvedCwd); @@ -726,11 +793,11 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase if (callerAgentId) { const callerAgent = resolveCallerAgent(); if (!callerAgent?.workspaceId) { - throw new Error(`Caller agent ${callerAgentId} has no current workspace`); + throw new PaseoToolInputError(`Caller agent ${callerAgentId} has no current workspace`); } return callerAgent.workspaceId; } - throw new Error("workspaceId is required outside an agent-scoped session"); + throw new PaseoToolInputError("workspaceId is required outside an agent-scoped session"); } const buildCallerAgentScheduleConfigExtras = ( @@ -800,7 +867,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase } if (!params?.provider?.trim()) { - throw new Error("provider is required when target is new-agent"); + throw new PaseoToolInputError("provider is required when target is new-agent"); } const resolvedProviderModel = resolveScheduleProviderAndModel({ @@ -822,22 +889,41 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase if (!scheduleService) { throw new Error("Schedule service is not configured"); } - const schedule = await scheduleService.inspect(id); + let schedule: Awaited>; + try { + schedule = await scheduleService.inspect(id); + } catch (error) { + if (error instanceof ScheduleNotFoundError) { + throw new PaseoToolInputError(error.message); + } + throw error; + } if (schedule.target.type !== type) { - throw new Error( + throw new PaseoToolInputError( type === "agent" ? `Heartbeat not found: ${id}` : `Schedule not found: ${id}`, ); } return schedule; } + async function runScheduleAction(action: () => Promise): Promise { + try { + return await action(); + } catch (error) { + if (error instanceof ScheduleStateConflictError) { + throw new PaseoToolInputError(error.message); + } + throw error; + } + } + async function requireCallerHeartbeat(id: string) { if (!callerAgentId) { - throw new Error("Heartbeat operations require an agent-scoped session"); + throw new PaseoToolInputError("Heartbeat operations require an agent-scoped session"); } const schedule = await requireScheduleTarget(id, "agent"); if (schedule.target.type !== "agent" || schedule.target.agentId !== callerAgentId) { - throw new Error(`Heartbeat ${id} does not belong to caller ${callerAgentId}`); + throw new PaseoToolInputError(`Heartbeat ${id} does not belong to caller ${callerAgentId}`); } return schedule; } @@ -847,6 +933,9 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase }) .refine( (value) => { + if (!value.includes("/")) { + return true; + } try { resolveRequiredProviderModel(value); return true; @@ -1184,11 +1273,13 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase }, async (args, context) => { if (!callerAgentId) { - throw new Error("speak is only available to agent-scoped tool sessions"); + throw new PaseoToolInputError("speak is only available to agent-scoped tool sessions"); } const handler = resolveSpeakHandler?.(callerAgentId) ?? null; if (!handler) { - throw new Error(`No speak handler registered for your session '${callerAgentId}'`); + throw new PaseoToolInputError( + `No speak handler registered for your session '${callerAgentId}'`, + ); } await handler({ text: args.text, @@ -1289,7 +1380,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase if (isolation === "local") { const cwd = resolveScopedCwd(path, { required: true }); if (!(await isExistingDirectory(cwd))) { - throw new Error(`Directory not found: ${cwd}`); + throw new PaseoToolInputError(`Directory not found: ${cwd}`); } assertOptionsAbsent( [ @@ -1534,7 +1625,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase if (hasLegacyCreateAgentPlacement(args)) { // COMPAT(nestedCreateAgentPlacement): accept the old relationship/workspace shape without // advertising it to models. Added in v0.2.0; remove after 2027-01-17. - const parsed = legacyAgentToAgentCreateAgentArgsSchema.parse(args); + const parsed = parseToolArguments(legacyAgentToAgentCreateAgentArgsSchema, args); const { cwd, workspaceId, worktree } = await resolveCreateAgentWorkspace(parsed.workspace, { prompt: parsed.initialPrompt, }); @@ -1547,7 +1638,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase worktree, }; } - const parsed = agentToAgentCreateAgentArgsSchema.parse(args); + const parsed = parseToolArguments(agentToAgentCreateAgentArgsSchema, args); const { cwd, workspaceId } = await resolveCanonicalCreateAgentWorkspace(parsed.workspaceId, { prompt: parsed.initialPrompt, }); @@ -1563,13 +1654,15 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase if (hasLegacyCreateAgentPlacement(args)) { // COMPAT(nestedCreateAgentPlacement): see the agent-scoped branch above. const parsedArgs = normalizeTopLevelCreateAgentArgs( - legacyTopLevelCreateAgentArgsSchema.parse(args), + parseToolArguments(legacyTopLevelCreateAgentArgsSchema, args), ); if (parsedArgs.relationship?.kind === "subagent") { - throw new Error("relationship subagent requires an agent-scoped tool session"); + throw new PaseoToolInputError( + "relationship subagent requires an agent-scoped tool session", + ); } if (!parsedArgs.workspace) { - throw new Error("Legacy create_agent placement could not be resolved"); + throw new PaseoToolInputError("Legacy create_agent placement could not be resolved"); } const { cwd, workspaceId, worktree } = await resolveCreateAgentWorkspace( parsedArgs.workspace, @@ -1584,7 +1677,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase worktree, }; } - const parsedArgs = canonicalTopLevelCreateAgentArgsSchema.parse(args); + const parsedArgs = parseToolArguments(canonicalTopLevelCreateAgentArgsSchema, args); const { cwd, workspaceId } = await resolveCanonicalCreateAgentWorkspace( parsedArgs.workspaceId, { prompt: parsedArgs.initialPrompt }, @@ -1644,7 +1737,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase } const caller = resolveCallerAgent(); if (!caller?.workspaceId) { - throw new Error(`Caller agent ${callerAgentId} has no current workspace`); + throw new PaseoToolInputError(`Caller agent ${callerAgentId} has no current workspace`); } return { cwd: undefined, workspaceId: caller.workspaceId }; } @@ -1672,18 +1765,18 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase }; if (canonicalCandidate.relationship && canonicalCandidate.workspace) { - return legacyTopLevelCreateAgentArgsSchema.parse({ + return parseToolArguments(legacyTopLevelCreateAgentArgsSchema, { ...canonicalCandidate, ...(Object.keys(settings).length > 0 ? { settings } : {}), }); } if (canonicalCandidate.relationship || canonicalCandidate.workspace) { - throw new Error("relationship and workspace must be provided together"); + throw new PaseoToolInputError("relationship and workspace must be provided together"); } if (!cwd?.trim()) { - throw new Error("cwd is required for legacy top-level create_agent calls"); + throw new PaseoToolInputError("cwd is required for legacy top-level create_agent calls"); } const legacyWorktreeTarget = resolveLegacyCreateAgentWorktreeTarget({ @@ -1710,7 +1803,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase }, }; - return legacyTopLevelCreateAgentArgsSchema.parse({ + return parseToolArguments(legacyTopLevelCreateAgentArgsSchema, { ...canonicalCandidate, relationship: { kind: "detached" }, workspace, @@ -1763,11 +1856,11 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase }> { if (workspace.kind === "current") { if (!callerAgentId) { - throw new Error("workspace current requires an agent-scoped tool session"); + throw new PaseoToolInputError("workspace current requires an agent-scoped tool session"); } const callerAgent = resolveCallerAgent(); if (!callerAgent?.workspaceId) { - throw new Error(`Caller agent ${callerAgentId} has no current workspace`); + throw new PaseoToolInputError(`Caller agent ${callerAgentId} has no current workspace`); } return { cwd: workspace.cwd, @@ -1784,14 +1877,16 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase (candidate) => candidate.workspaceId === workspace.workspaceId, ); if (!existingWorkspace) { - throw new Error(`Workspace ${workspace.workspaceId} not found`); + throw new PaseoToolInputError(`Workspace ${workspace.workspaceId} not found`); } const cwd = workspace.cwd ? resolveScopedCwd(workspace.cwd, { required: true }) : existingWorkspace.cwd; const lockedCwd = callerContext?.lockedCwd?.trim(); if (lockedCwd && !isSameOrDescendantPath(expandUserPath(lockedCwd), cwd)) { - throw new Error(`Workspace ${workspace.workspaceId} is outside the allowed cwd`); + throw new PaseoToolInputError( + `Workspace ${workspace.workspaceId} is outside the allowed cwd`, + ); } return { cwd, @@ -1893,6 +1988,9 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase logger: childLogger, }).catch((error) => { stopNotification?.(); + if (error instanceof AgentNotFoundError) { + throw new PaseoToolInputError(error.message); + } throw error; }); @@ -1956,7 +2054,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase const record = await agentStorage.get(agentId); if (!record || record.internal) { - throw new Error(`Agent ${agentId} not found`); + throw new PaseoToolInputError(`Agent ${agentId} not found`); } const structuredSnapshot = buildStoredAgentPayload( @@ -2194,10 +2292,10 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase const workspaceId = resolveWorkspaceIdForRename(requestedWorkspaceId); const existing = await options.workspaceRegistry.get(workspaceId); if (!existing) { - throw new Error(`Workspace ${workspaceId} not found`); + throw new PaseoToolInputError(`Workspace ${workspaceId} not found`); } if (existing.archivedAt) { - throw new Error(`Workspace ${workspaceId} is archived`); + throw new PaseoToolInputError(`Workspace ${workspaceId} is archived`); } await options.workspaceRegistry.upsert({ @@ -2401,7 +2499,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase const terminal = terminalManager.getTerminal(terminalId); if (!terminal) { - throw new Error(`Terminal ${terminalId} not found`); + throw new PaseoToolInputError(`Terminal ${terminalId} not found`); } terminal.kill(); @@ -2437,7 +2535,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase } if (!terminalManager.getTerminal(terminalId)) { - throw new Error(`Terminal ${terminalId} not found`); + throw new PaseoToolInputError(`Terminal ${terminalId} not found`); } const capture = await terminalManager.captureTerminal(terminalId, { @@ -2478,7 +2576,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase const terminal = terminalManager.getTerminal(terminalId); if (!terminal) { - throw new Error(`Terminal ${terminalId} not found`); + throw new PaseoToolInputError(`Terminal ${terminalId} not found`); } terminal.send({ @@ -2498,24 +2596,26 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase { title: "Create schedule", description: "Create a recurring schedule that starts a new agent on a cron cadence.", - inputSchema: { - prompt: z.string().trim().min(1, "prompt is required"), - cron: z.string().trim().min(1, "cron is required"), - timezone: z - .string() - .trim() - .min(1) - .optional() - .describe("IANA time zone for the cron cadence. For example: America/New_York."), - name: z.string().optional(), - provider: (callerAgentId ? AgentProviderEnum.optional() : AgentProviderEnum).describe( - "Provider, or provider/model (for example: codex or codex/gpt-5.4). Defaults to the caller's provider in an agent-scoped session.", - ), - cwd: z.string().optional(), - isolation: z.enum(["local", "worktree"]).optional(), - maxRuns: z.number().int().positive().optional(), - expiresIn: z.string().optional(), - }, + inputSchema: z + .object({ + prompt: z.string().trim().min(1, "prompt is required"), + cron: z.string().trim().min(1, "cron is required"), + timezone: z + .string() + .trim() + .min(1) + .optional() + .describe("IANA time zone for the cron cadence. For example: America/New_York."), + name: z.string().optional(), + provider: (callerAgentId ? AgentProviderEnum.optional() : AgentProviderEnum).describe( + "Provider, or provider/model (for example: codex or codex/gpt-5.4). Defaults to the caller's provider in an agent-scoped session.", + ), + cwd: z.string().optional(), + isolation: z.enum(["local", "worktree"]).optional(), + maxRuns: z.number().int().positive().optional(), + expiresIn: scheduleDurationInputSchema.optional(), + }) + .superRefine(addScheduleCadenceIssue), outputSchema: ScheduleSummarySchema.shape, }, async ({ prompt, cron, timezone, name, provider, cwd, isolation, maxRuns, expiresIn }) => { @@ -2548,19 +2648,21 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase { title: "Create heartbeat", description: "Create a recurring heartbeat that sends you a prompt on a cron cadence.", - inputSchema: { - prompt: z.string().trim().min(1, "prompt is required"), - cron: z.string().trim().min(1, "cron is required"), - timezone: z - .string() - .trim() - .min(1) - .optional() - .describe("IANA time zone for the cron cadence. For example: America/New_York."), - name: z.string().optional(), - maxRuns: z.number().int().positive().optional(), - expiresIn: z.string().optional(), - }, + inputSchema: z + .object({ + prompt: z.string().trim().min(1, "prompt is required"), + cron: z.string().trim().min(1, "cron is required"), + timezone: z + .string() + .trim() + .min(1) + .optional() + .describe("IANA time zone for the cron cadence. For example: America/New_York."), + name: z.string().optional(), + maxRuns: z.number().int().positive().optional(), + expiresIn: scheduleDurationInputSchema.optional(), + }) + .superRefine(addScheduleCadenceIssue), outputSchema: ScheduleSummarySchema.shape, }, async ({ prompt, cron, timezone, name, maxRuns, expiresIn }) => { @@ -2568,7 +2670,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase throw new Error("Schedule service is not configured"); } if (!callerAgentId) { - throw new Error("create_heartbeat requires an agent-scoped session"); + throw new PaseoToolInputError("create_heartbeat requires an agent-scoped session"); } resolveCallerAgent(); @@ -2679,7 +2781,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase } await requireScheduleTarget(id, "new-agent"); - await scheduleService.pause(id); + await runScheduleAction(() => scheduleService.pause(id)); return { content: [], structuredContent: ensureValidJson({ success: true }), @@ -2705,7 +2807,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase } await requireScheduleTarget(id, "new-agent"); - await scheduleService.resume(id); + await runScheduleAction(() => scheduleService.resume(id)); return { content: [], structuredContent: ensureValidJson({ success: true }), @@ -2787,13 +2889,13 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase .optional() .describe("New mode for new-agent target (null to clear)."), cwd: z.string().trim().min(1).optional().describe("New cwd for new-agent target."), - expiresIn: z - .string() + expiresIn: scheduleDurationInputSchema .optional() .describe("New relative expiry duration (for example: 1h, 2d)."), clearExpires: z.boolean().optional().describe("Clear any schedule expiry."), }) - .passthrough(), + .passthrough() + .superRefine(addScheduleCadenceIssue), outputSchema: StoredScheduleSchema.shape, }, async (input) => { @@ -2850,7 +2952,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase throw new Error("Schedule service is not configured"); } await requireScheduleTarget(id, "new-agent"); - const schedule = await scheduleService.runOnce(id); + const schedule = await runScheduleAction(() => scheduleService.runOnce(id)); return { content: [], structuredContent: ensureValidJson(schedule), @@ -2963,10 +3065,10 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase }); const summary = toProviderSummary(entry); if (!entry.enabled) { - throw new Error(`Provider '${providerId}' is disabled`); + throw new PaseoToolInputError(`Provider '${providerId}' is disabled`); } if (entry.status !== "ready") { - throw new Error(entry.error ?? `Provider '${providerId}' is unavailable`); + throw new PaseoToolInputError(entry.error ?? `Provider '${providerId}' is unavailable`); } const selectedModel = settings?.model ?? resolvedProviderModel.model; const features = await agentManager.listDraftFeatures({ diff --git a/packages/server/src/server/agent/tools/types.ts b/packages/server/src/server/agent/tools/types.ts index 66a4a23da2..f82ad7549a 100644 --- a/packages/server/src/server/agent/tools/types.ts +++ b/packages/server/src/server/agent/tools/types.ts @@ -1,6 +1,18 @@ import type { z } from "zod"; import type { ProviderPaseoToolsPolicy } from "@getpaseo/protocol/provider-config"; +export class PaseoToolInputError extends Error { + readonly status = 400; + + constructor( + message: string, + readonly issues?: z.ZodError["issues"], + ) { + super(message); + this.name = "PaseoToolInputError"; + } +} + export interface PaseoToolExecutionContext { signal?: AbortSignal; sendUpdate?: (update: PaseoToolResult) => void; diff --git a/packages/server/src/server/daemon-e2e/claude-settings-mid-turn.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/claude-settings-mid-turn.real.e2e.test.ts new file mode 100644 index 0000000000..5211ff2a5a --- /dev/null +++ b/packages/server/src/server/daemon-e2e/claude-settings-mid-turn.real.e2e.test.ts @@ -0,0 +1,60 @@ +import { expect, test } from "vitest"; +import { mkdtemp, rm } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import pino from "pino"; +import { ClaudeAgentClient } from "../agent/providers/claude/agent.js"; +import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; +import { DaemonClient } from "../test-utils/daemon-client.js"; + +test("changing thinking then mode preserves a foreground Claude command", async () => { + const logger = pino({ level: "silent" }); + const cwd = await mkdtemp(path.join(tmpdir(), "claude-settings-mid-turn-")); + const daemon = await createTestPaseoDaemon({ + logger, + agentClients: { claude: new ClaudeAgentClient({ logger }) }, + }); + const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); + try { + await client.connect(); + await client.fetchAgents({ subscribe: {} }); + const agent = await client.createAgent({ + provider: "claude", + model: "claude-haiku-5-5", + modeId: "auto", + thinkingOptionId: "low", + cwd, + }); + await client.subscribeAgentTimeline(agent.id, () => {}).ready; + await client.sendMessage( + agent.id, + "Run exactly these three Bash commands in the foreground, one per tool call, in order: python3 -c \"from pathlib import Path; import time; Path('started').write_text('started'); time.sleep(20); print(1)\", then the same with print(2), then print(3). Then reply DONE.", + ); + // A tool-call row is emitted before Claude's permission check finishes. + // Wait for the command itself, so changing to acceptEdits cannot gate its start. + await expect.poll(() => existsSync(path.join(cwd, "started")), { timeout: 90_000 }).toBe(true); + const notice = await client.setAgentThinkingOption(agent.id, "medium"); + expect(notice).toEqual({ type: "warning", message: "Thinking level applies next turn" }); + await client.setAgentMode(agent.id, "acceptEdits"); + await expect + .poll( + async () => { + const timeline = await client.fetchAgentTimeline(agent.id, { limit: 100 }); + return timeline.entries.some( + ({ item }) => + item.type === "tool_call" && + item.detail.type === "shell" && + item.detail.command.includes("print(1)") && + item.status === "completed", + ); + }, + { timeout: 45_000, interval: 100 }, + ) + .toBe(true); + } finally { + await client.close(); + await daemon.close(); + await rm(cwd, { recursive: true, force: true }); + } +}, 180_000); diff --git a/packages/server/src/server/daemon-e2e/omp.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/omp.real.e2e.test.ts index ab407e31bc..247068220a 100644 --- a/packages/server/src/server/daemon-e2e/omp.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/omp.real.e2e.test.ts @@ -5,6 +5,8 @@ import path from "node:path"; import { promisify } from "node:util"; import pino from "pino"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { afterEach, beforeAll, describe, expect, test } from "vitest"; import type { AgentTimelineItem } from "../agent/agent-sdk-types.js"; @@ -131,6 +133,14 @@ async function timeline(client: DaemonClient, agentId: string): Promise entry.item); } +async function readAssistantText(harness: Harness, agentId: string): Promise { + const items = await timeline(harness.client, agentId); + return items + .filter((item) => item.type === "assistant_message") + .map((item) => item.text) + .join(""); +} + function compactionFiller(records = 1200): string { const teas = ["assam", "darjeeling", "sencha", "oolong"]; return Array.from( @@ -257,8 +267,80 @@ async function contextWindowUsedTokens(client: DaemonClient, agentId: string) { describe("daemon E2E (real OMP)", () => { test( - "a prompt OMP rejects before its agent runs fails the turn with OMP's error", + "an idle out-of-band follow-up starts a turn", async () => { + const harness = await createHarness(); + try { + const agent = await createAgent(harness, "idle-follow-up"); + await harness.client.sendMessage(agent.id, "/follow-up Reply exactly IDLE_FOLLOWUP_OK"); + await expect + .poll(() => readAssistantText(harness, agent.id), { timeout: 30_000 }) + .toContain("IDLE_FOLLOWUP_OK"); + const finish = await harness.client.waitForFinish(agent.id, TIMEOUT_MS); + expect(finish.status).toBe("idle"); + } finally { + await closeHarness(harness); + } + }, + TIMEOUT_MS, + ); + + test( + "an agent-sent idle follow-up starts a turn and notifies its parent", + async () => { + const harness = await createHarness(); + const mcp = new Client({ name: "idle-follow-up-test", version: "1.0.0" }); + try { + const parent = await createAgent(harness, "follow-up-parent"); + const child = await createAgent(harness, "follow-up-child"); + await mcp.connect( + new StreamableHTTPClientTransport( + new URL( + `http://127.0.0.1:${harness.daemon.port}/mcp/agents?callerAgentId=${parent.id}`, + ), + ), + ); + const response = await mcp.callTool({ + name: "send_agent_prompt", + arguments: { agentId: child.id, prompt: "/follow-up Reply exactly MCP_IDLE_FOLLOWUP_OK" }, + }); + expect(response.isError).not.toBe(true); + expect(response.structuredContent).toMatchObject({ + success: true, + disposition: "out_of_band", + }); + await expect + .poll(() => readAssistantText(harness, child.id), { timeout: 30_000 }) + .toContain("MCP_IDLE_FOLLOWUP_OK"); + await expect + .poll( + async () => { + const items = await timeline(harness.client, parent.id); + let notifications = 0; + for (const item of items) { + if ( + item.type === "tool_call" && + item.agentMessage?.sender.id === child.id && + item.agentMessage.event === "finished" + ) + notifications++; + } + return notifications; + }, + { timeout: 15_000 }, + ) + .toBe(1); + } finally { + await mcp.close(); + await closeHarness(harness); + } + }, + TIMEOUT_MS, + ); + + test.each(["Reply with ok.", "/follow-up Reply with ok."])( + "a rejected %s fails the turn with OMP's error", + async (prompt) => { const harness = await createHarness({ ompHome: createOfflineOmpHome() }); try { const agent = await harness.client.createAgent({ @@ -268,7 +350,7 @@ describe("daemon E2E (real OMP)", () => { model: "anthropic/claude-haiku-4-5", modeId: "full", }); - await harness.client.sendMessage(agent.id, "Reply with ok."); + await harness.client.sendMessage(agent.id, prompt); const finish = await harness.client.waitForFinish(agent.id, 60_000); expect(finish.status).toBe("error"); expect(finish.error).toContain("No API key found for anthropic"); diff --git a/packages/server/src/server/schedule/service.ts b/packages/server/src/server/schedule/service.ts index 7b5487c3f8..3b462cc5b1 100644 --- a/packages/server/src/server/schedule/service.ts +++ b/packages/server/src/server/schedule/service.ts @@ -38,6 +38,20 @@ export class ScheduleTargetGoneError extends Error { } } +export class ScheduleNotFoundError extends Error { + constructor(id: string) { + super(`Schedule not found: ${id}`); + this.name = "ScheduleNotFoundError"; + } +} + +export class ScheduleStateConflictError extends Error { + constructor(message: string) { + super(message); + this.name = "ScheduleStateConflictError"; + } +} + function advanceNextRunAtPast( cadence: StoredSchedule["cadence"], nextRunAt: Date, @@ -158,7 +172,7 @@ function shouldCompleteSchedule(schedule: StoredSchedule, now: Date): boolean { function requireSchedule(schedule: StoredSchedule | null, id: string): StoredSchedule { if (!schedule) { - throw new Error(`Schedule not found: ${id}`); + throw new ScheduleNotFoundError(id); } return schedule; } @@ -392,10 +406,7 @@ export class ScheduleService { async inspect(id: string): Promise { const schedule = await this.store.get(id); - if (!schedule) { - throw new Error(`Schedule not found: ${id}`); - } - return schedule; + return requireSchedule(schedule, id); } async logs(id: string): Promise { @@ -406,7 +417,7 @@ export class ScheduleService { async pause(id: string): Promise { const paused = await this.store.update(id, (schedule) => { if (schedule.status === "completed") { - throw new Error(`Schedule ${id} is already completed`); + throw new ScheduleStateConflictError(`Schedule ${id} is already completed`); } if (schedule.status === "paused") { return schedule; @@ -426,7 +437,7 @@ export class ScheduleService { async resume(id: string): Promise { const resumed = await this.store.update(id, (schedule) => { if (schedule.status === "completed") { - throw new Error(`Schedule ${id} is already completed`); + throw new ScheduleStateConflictError(`Schedule ${id} is already completed`); } if (schedule.status === "active") { return schedule; @@ -546,10 +557,10 @@ export class ScheduleService { async runOnce(id: string): Promise { const schedule = await this.inspect(id); if (schedule.status === "completed") { - throw new Error(`Schedule ${id} is already completed`); + throw new ScheduleStateConflictError(`Schedule ${id} is already completed`); } if (this.runningScheduleIds.has(id)) { - throw new Error(`Schedule ${id} is already running`); + throw new ScheduleStateConflictError(`Schedule ${id} is already running`); } await this.runSchedule(schedule, this.now(), { manual: true }); return this.inspect(id); diff --git a/packages/server/src/server/test-utils/question-response-daemon.ts b/packages/server/src/server/test-utils/question-response-daemon.ts new file mode 100644 index 0000000000..2899dc8ba3 --- /dev/null +++ b/packages/server/src/server/test-utils/question-response-daemon.ts @@ -0,0 +1,69 @@ +import { randomUUID } from "node:crypto"; +import { + MockLoadTestAgentClient, + MockLoadTestAgentSession, +} from "../agent/providers/mock-load-test-agent.js"; +import { createTestPaseoDaemon } from "./paseo-daemon.js"; + +const firstResponse = Promise.withResolvers(); + +class RetryQuestionSession extends MockLoadTestAgentSession { + private attempts = 0; + + override async respondToPermission( + requestId: string, + response: Parameters[1], + ) { + this.attempts += 1; + process.send?.({ type: "attempt", requestId, response, sessionId: this.id }); + if (this.attempts === 1) { + await firstResponse.promise; + throw new Error("Synthetic question response failure"); + } + return super.respondToPermission(requestId, response); + } +} + +class RetryQuestionClient extends MockLoadTestAgentClient { + override async createSession(config: Parameters[0]) { + const session = new RetryQuestionSession({ config, sessionId: randomUUID() }); + session.subscribe((event) => { + if (event.type === "permission_resolved") { + process.send?.({ + type: "resolved", + requestId: event.requestId, + response: event.resolution, + sessionId: session.id, + pendingRequestIds: session.getPendingPermissions().map((request) => request.id), + }); + } + }); + return session; + } +} + +async function main(): Promise { + const daemon = await createTestPaseoDaemon({ + agentClients: { mock: new RetryQuestionClient() }, + isDev: true, + mcpEnabled: false, + corsAllowedOrigins: [`http://localhost:${process.env.E2E_METRO_PORT}`], + }); + process.on("message", (message: unknown) => { + if (message === "release") firstResponse.resolve(); + }); + const shutdown = async () => { + firstResponse.resolve(); + await daemon.close(); + process.exit(0); + }; + process.once("SIGINT", () => void shutdown()); + process.once("SIGTERM", () => void shutdown()); + process.once("disconnect", () => void shutdown()); + process.send?.({ type: "ready", port: daemon.port, serverId: daemon.daemon.getServerId() }); +} + +void main().catch((error: unknown) => { + process.send?.({ type: "error", error: error instanceof Error ? error.message : String(error) }); + process.exit(1); +}); diff --git a/packages/website/package.json b/packages/website/package.json index 1677b3c0fc..3646d4da47 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/website", - "version": "0.11.2", + "version": "0.12.0-beta.1", "private": true, "type": "module", "scripts": { diff --git a/packages/website/vite.config.ts b/packages/website/vite.config.ts index 46dbc554de..d609be14bb 100644 --- a/packages/website/vite.config.ts +++ b/packages/website/vite.config.ts @@ -117,7 +117,7 @@ export default defineConfig((): UserConfig => { }, plugins: [ cloudflare({ viteEnvironment: { name: "ssr" } }), - tsConfigPaths(), + tsConfigPaths({ projects: [path.join(__dirname, "tsconfig.json")] }), tanstackStart({ router: { quoteStyle: "double", diff --git a/patches/react-native-draggable-flatlist+4.0.3.patch b/patches/react-native-draggable-flatlist+4.0.3.patch index 79838c55e2..ddbdb92224 100644 --- a/patches/react-native-draggable-flatlist+4.0.3.patch +++ b/patches/react-native-draggable-flatlist+4.0.3.patch @@ -58,10 +58,14 @@ index 2abb4a2..8ca967f 100644 onAnimValInit?: (animVals: ReturnType) => void; itemEnteringAnimation?: AnimateProps["entering"]; diff --git a/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx b/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx -index 7c88afc..6d5719c 100644 +index 7c88afc..a74140f 100644 --- a/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx +++ b/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx -@@ -19,6 +19,7 @@ import { +@@ -16,9 +16,11 @@ import { + FlatList, + Gesture, + GestureDetector, ++ State, } from "react-native-gesture-handler"; import Animated, { runOnJS, @@ -69,7 +73,7 @@ index 7c88afc..6d5719c 100644 useAnimatedReaction, useAnimatedScrollHandler, useSharedValue, -@@ -92,6 +93,17 @@ function DraggableFlatListInner(props: DraggableFlatListProps) { +@@ -92,6 +94,17 @@ function DraggableFlatListInner(props: DraggableFlatListProps) { activeCellOffset.value = -1; setActiveKey(null); }); @@ -87,7 +91,7 @@ index 7c88afc..6d5719c 100644 const { dragHitSlop = DEFAULT_PROPS.dragHitSlop, -@@ -144,19 +156,88 @@ function DraggableFlatListInner(props: DraggableFlatListProps) { +@@ -144,19 +157,88 @@ function DraggableFlatListInner(props: DraggableFlatListProps) { }); }, [props.data, keyExtractor, keyToIndexRef]); @@ -183,7 +187,7 @@ index 7c88afc..6d5719c 100644 setActiveKey(activeKey); onDragBegin?.(index); } -@@ -218,8 +299,25 @@ function DraggableFlatListInner(props: DraggableFlatListProps) { +@@ -218,8 +300,25 @@ function DraggableFlatListInner(props: DraggableFlatListProps) { props.onRelease?.(index); }); @@ -210,7 +214,7 @@ index 7c88afc..6d5719c 100644 const { onDragEnd, data } = props; const newData = [...data]; -@@ -238,26 +336,6 @@ function DraggableFlatListInner(props: DraggableFlatListProps) { +@@ -238,26 +337,6 @@ function DraggableFlatListInner(props: DraggableFlatListProps) { props.onPlaceholderIndexChange?.(index); }); @@ -237,7 +241,7 @@ index 7c88afc..6d5719c 100644 useAnimatedReaction( () => { return spacerIndexAnim.value; -@@ -271,11 +349,21 @@ function DraggableFlatListInner(props: DraggableFlatListProps) { +@@ -271,11 +350,24 @@ function DraggableFlatListInner(props: DraggableFlatListProps) { ); const gestureDisabled = useSharedValue(false); @@ -257,10 +261,13 @@ index 7c88afc..6d5719c 100644 + gestureSessionActive.value = true; + terminalHandled.value = false; + runOnJS(onGestureBegin)(sessionId); ++ panGestureState.value = evt.state; ++ }) ++ .onStart((evt) => { panGestureState.value = evt.state; }) .onUpdate((evt) => { -@@ -286,8 +374,15 @@ function DraggableFlatListInner(props: DraggableFlatListProps) { +@@ -286,8 +378,15 @@ function DraggableFlatListInner(props: DraggableFlatListProps) { : evt.translationY; touchTranslate.value = translation; }) @@ -277,7 +284,7 @@ index 7c88afc..6d5719c 100644 // Set touch val to current translate val isTouchActiveNative.value = false; const translation = horizontalAnim.value -@@ -299,6 +394,7 @@ function DraggableFlatListInner(props: DraggableFlatListProps) { +@@ -299,6 +398,7 @@ function DraggableFlatListInner(props: DraggableFlatListProps) { // Only call onDragEnd if actually dragging a cell if (activeIndexAnim.value === -1 || disabled.value) return; @@ -285,7 +292,7 @@ index 7c88afc..6d5719c 100644 disabled.value = true; runOnJS(onRelease)(activeIndexAnim.value); const springTo = placeholderOffset.value - activeCellOffset.value; -@@ -309,14 +405,64 @@ function DraggableFlatListInner(props: DraggableFlatListProps) { +@@ -309,22 +409,84 @@ function DraggableFlatListInner(props: DraggableFlatListProps) { runOnJS(onDragEnd)({ from: activeIndexAnim.value, to: spacerIndexAnim.value, @@ -316,6 +323,7 @@ index 7c88afc..6d5719c 100644 + } runOnJS(onContainerTouchStart)(); }) +- .onTouchesUp(() => { + .onTouchesMove((event, stateManager) => { + if (!coordinatesDragWithExternalGesture) return; + @@ -348,10 +356,20 @@ index 7c88afc..6d5719c 100644 + stateManager.activate(); + } + }) - .onTouchesUp(() => { ++ .onTouchesUp((event, stateManager) => { // Turning this into a worklet causes timing issues. We want it to run // just after the finger lifts. -@@ -324,7 +470,9 @@ function DraggableFlatListInner(props: DraggableFlatListProps) { + runOnJS(onContainerTouchEnd)(); ++ // A manually activated iOS pan can remain BEGAN after the last finger ++ // lifts. Fail it explicitly so onFinalize cancels the armed row. ++ if (event.numberOfTouches === 0 && panGestureState.value === State.BEGAN) { ++ stateManager.fail(); ++ } ++ }) ++ .onTouchesCancelled((_event, stateManager) => { ++ if (panGestureState.value === State.BEGAN) { ++ stateManager.fail(); ++ } }); if (dragHitSlop) panGesture.hitSlop(dragHitSlop); @@ -362,7 +380,7 @@ index 7c88afc..6d5719c 100644 const activeOffset = [-activationDistanceProp, activationDistanceProp]; if (props.horizontal) { panGesture.activeOffsetX(activeOffset); -@@ -332,6 +480,21 @@ function DraggableFlatListInner(props: DraggableFlatListProps) { +@@ -332,6 +494,21 @@ function DraggableFlatListInner(props: DraggableFlatListProps) { panGesture.activeOffsetY(activeOffset); } } @@ -384,7 +402,7 @@ index 7c88afc..6d5719c 100644 const onScroll = useStableCallback((scrollOffset: number) => { props.onScrollOffsetChange?.(scrollOffset); -@@ -373,8 +536,9 @@ function DraggableFlatListInner(props: DraggableFlatListProps) { +@@ -373,8 +550,9 @@ function DraggableFlatListInner(props: DraggableFlatListProps) { horizontal={!!props.horizontal} layoutAnimationDisabled={layoutAnimationDisabled} > diff --git a/patches/react-native-enriched-markdown+1.2.0-nightly-20261014-766fc0e67.patch b/patches/react-native-enriched-markdown+1.2.0-nightly-20261014-766fc0e67.patch new file mode 100644 index 0000000000..b41700d65b --- /dev/null +++ b/patches/react-native-enriched-markdown+1.2.0-nightly-20261014-766fc0e67.patch @@ -0,0 +1,2930 @@ +// UPSTREAM: https://github.com/software-mansion/enriched-markdown/pull/997 +diff --git a/node_modules/react-native-enriched-markdown/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp b/node_modules/react-native-enriched-markdown/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +index 77465ad..94997f7 100644 +--- a/node_modules/react-native-enriched-markdown/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp ++++ b/node_modules/react-native-enriched-markdown/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +@@ -17,6 +17,7 @@ void EnrichedMarkdownEventEmitter::onLinkPress(OnLinkPress event) const { + dispatchEvent("linkPress", [event=std::move(event)](jsi::Runtime &runtime) { + auto payload = jsi::Object(runtime); + payload.setProperty(runtime, "url", event.url); ++ payload.setProperty(runtime, "sourceType", event.sourceType); + return payload; + }); + } +@@ -362,6 +363,7 @@ void EnrichedMarkdownTextEventEmitter::onLinkPress(OnLinkPress event) const { + dispatchEvent("linkPress", [event=std::move(event)](jsi::Runtime &runtime) { + auto payload = jsi::Object(runtime); + payload.setProperty(runtime, "url", event.url); ++ payload.setProperty(runtime, "sourceType", event.sourceType); + return payload; + }); + } +diff --git a/node_modules/react-native-enriched-markdown/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h b/node_modules/react-native-enriched-markdown/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +index 0a1c6c2..5935112 100644 +--- a/node_modules/react-native-enriched-markdown/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h ++++ b/node_modules/react-native-enriched-markdown/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +@@ -19,6 +19,7 @@ class EnrichedMarkdownEventEmitter : public ViewEventEmitter { + + struct OnLinkPress { + std::string url; ++ std::string sourceType; + }; + + struct OnLinkLongPress { +@@ -298,6 +299,7 @@ class EnrichedMarkdownTextEventEmitter : public ViewEventEmitter { + + struct OnLinkPress { + std::string url; ++ std::string sourceType; + }; + + struct OnLinkLongPress { +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +@@ -95,7 +95,7 @@ + private var accessibilityLabels = AccessibilityLabels() + private var textBreakStrategy: String = BreakStrategyUtils.DEFAULT_STRATEGY + +- private var onLinkPressCallback: ((String) -> Unit)? = null ++ private var onLinkPressCallback: ((String, String) -> Unit)? = null + private var onLinkLongPressCallback: ((String) -> Unit)? = null + private var onTaskListItemPressCallback: ((Int, Boolean, String) -> Unit)? = null + private var onLatexErrorCallback: LatexErrorReporter? = null +@@ -296,7 +296,7 @@ + } + } + +- fun setOnLinkPressCallback(callback: (String) -> Unit) { ++ fun setOnLinkPressCallback(callback: (String, String) -> Unit) { + onLinkPressCallback = callback + } + +@@ -405,7 +405,7 @@ + val codeLinkRegex = inlineCodeLinkRegex + val renderId = ++currentRenderId + +- executor.execute { ++ dispatchMarkdownRender(segmentViews.isNotEmpty(), executor) { + try { + val filtered = + if (isStreaming) { +@@ -418,13 +418,13 @@ + + if (renderableMarkdown.isEmpty()) { + postToMain(renderId) { applyRenderedSegments(emptyList(), false) } +- return@execute ++ return@dispatchMarkdownRender + } + + val ast = + parser.parseMarkdown(renderableMarkdown, md4cFlags, isGFM, textLinkRegex, codeLinkRegex) ?: run { + postToMain(renderId) { applyRenderedSegments(emptyList(), false) } +- return@execute ++ return@dispatchMarkdownRender + } + + val segments = splitASTIntoSegments(ast) +@@ -646,8 +646,12 @@ + renderId: Long, + action: () -> Unit, + ) { +- mainHandler.post { ++ if (Looper.myLooper() == Looper.getMainLooper()) { + if (renderId == currentRenderId) action() ++ } else { ++ mainHandler.post { ++ if (renderId == currentRenderId) action() ++ } + } + } + +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +index 5dd5828..757d2bd 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +@@ -50,8 +50,8 @@ class EnrichedMarkdownManager : + emitContextMenuItemPress(view, itemText, selectedText, selectionStart, selectionEnd) + } + +- view.setOnLinkPressCallback { url -> +- emitLinkPress(view, url) ++ view.setOnLinkPressCallback { url, sourceType -> ++ emitLinkPress(view, url, sourceType) + } + + view.setOnLinkLongPressCallback { url -> +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +index 8745349..f3275b9 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +@@ -69,7 +69,7 @@ class EnrichedMarkdownText + CodeBlockPressHost { + private val parser = Parser.shared + private val renderer = Renderer() +- private var onLinkPressCallback: ((String) -> Unit)? = null ++ private var onLinkPressCallback: ((String, String) -> Unit)? = null + private var onLinkLongPressCallback: ((String) -> Unit)? = null + private var onLatexErrorCallback: LatexErrorReporter? = null + +@@ -455,8 +455,8 @@ class EnrichedMarkdownText + } + } + +- fun emitOnLinkPress(url: String) { +- emitLinkPressEvent(url) ++ fun emitOnLinkPress(url: String, sourceType: String) { ++ emitLinkPressEvent(url, sourceType) + } + + fun emitOnLinkLongPress(url: String) { +@@ -491,7 +491,7 @@ class EnrichedMarkdownText + codeBlockPressEnabled = enabled + } + +- fun setOnLinkPressCallback(callback: (String) -> Unit) { ++ fun setOnLinkPressCallback(callback: (String, String) -> Unit) { + onLinkPressCallback = callback + } + +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +index f1e3345..2549508 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +@@ -47,8 +47,8 @@ class EnrichedMarkdownTextManager : + emitContextMenuItemPress(view, itemText, selectedText, selectionStart, selectionEnd) + } + +- view.setOnLinkPressCallback { url -> +- emitLinkPress(view, url) ++ view.setOnLinkPressCallback { url, sourceType -> ++ emitLinkPress(view, url, sourceType) + } + + view.setOnLinkLongPressCallback { url -> +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/events/LinkPressEvent.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/events/LinkPressEvent.kt +index 93c763e..fbe1c68 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/events/LinkPressEvent.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/events/LinkPressEvent.kt +@@ -8,12 +8,14 @@ class LinkPressEvent( + surfaceId: Int, + viewId: Int, + private val url: String, ++ private val sourceType: String, + ) : Event(surfaceId, viewId) { + override fun getEventName(): String = EVENT_NAME + + override fun getEventData(): WritableMap { + val eventData: WritableMap = Arguments.createMap() + eventData.putString("url", url) ++ eventData.putString("sourceType", sourceType) + return eventData + } + +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/BaselineShiftRenderer.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/BaselineShiftRenderer.kt +index cb4605a..510af94 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/BaselineShiftRenderer.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/BaselineShiftRenderer.kt +@@ -14,7 +14,7 @@ abstract class BaselineShiftRenderer : NodeRenderer { + override fun render( + node: MarkdownASTNode, + builder: SpannableStringBuilder, +- onLinkPress: ((String) -> Unit)?, ++ onLinkPress: ((String, String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + factory: RendererFactory, + ) { +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlankLineRenderer.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlankLineRenderer.kt +index 0a57557..25eb0dc 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlankLineRenderer.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlankLineRenderer.kt +@@ -15,7 +15,7 @@ class BlankLineRenderer( + override fun render( + node: MarkdownASTNode, + builder: SpannableStringBuilder, +- onLinkPress: ((String) -> Unit)?, ++ onLinkPress: ((String, String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + factory: RendererFactory, + ) { +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteRenderer.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteRenderer.kt +index f5804a5..6292452 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteRenderer.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteRenderer.kt +@@ -19,7 +19,7 @@ class BlockquoteRenderer( + override fun render( + node: MarkdownASTNode, + builder: SpannableStringBuilder, +- onLinkPress: ((String) -> Unit)?, ++ onLinkPress: ((String, String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + factory: RendererFactory, + ) { +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteTextRenderer.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteTextRenderer.kt +index 91f4a7c..4eacec8 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteTextRenderer.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteTextRenderer.kt +@@ -24,7 +24,7 @@ class BlockquoteTextRenderer( + fun render( + renderer: Renderer, + nodes: List, +- onLinkPress: ((String) -> Unit)?, ++ onLinkPress: ((String, String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + ): SpannableStringBuilder = renderer.renderContent(nodes, onLinkPress, onLinkLongPress, block = this) + +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeBlockRenderer.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeBlockRenderer.kt +index 4fcf57d..606c1a5 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeBlockRenderer.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeBlockRenderer.kt +@@ -18,7 +18,7 @@ class CodeBlockRenderer( + override fun render( + node: MarkdownASTNode, + builder: SpannableStringBuilder, +- onLinkPress: ((String) -> Unit)?, ++ onLinkPress: ((String, String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + factory: RendererFactory, + ) { +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeRenderer.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeRenderer.kt +index 0008d9b..6b54eff 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeRenderer.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeRenderer.kt +@@ -12,7 +12,7 @@ class CodeRenderer( + override fun render( + node: MarkdownASTNode, + builder: SpannableStringBuilder, +- onLinkPress: ((String) -> Unit)?, ++ onLinkPress: ((String, String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + factory: RendererFactory, + ) { +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/EmphasisRenderer.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/EmphasisRenderer.kt +index 8029d2d..6811dc4 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/EmphasisRenderer.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/EmphasisRenderer.kt +@@ -11,7 +11,7 @@ class EmphasisRenderer( + override fun render( + node: MarkdownASTNode, + builder: SpannableStringBuilder, +- onLinkPress: ((String) -> Unit)?, ++ onLinkPress: ((String, String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + factory: RendererFactory, + ) { +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt +index 1a8c71b..432cfae 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt +@@ -17,7 +17,7 @@ class HeadingRenderer( + override fun render( + node: MarkdownASTNode, + builder: SpannableStringBuilder, +- onLinkPress: ((String) -> Unit)?, ++ onLinkPress: ((String, String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + factory: RendererFactory, + ) { +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/HighlightRenderer.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/HighlightRenderer.kt +index a99a31f..acfcce5 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/HighlightRenderer.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/HighlightRenderer.kt +@@ -9,7 +9,7 @@ class HighlightRenderer : NodeRenderer { + override fun render( + node: MarkdownASTNode, + builder: SpannableStringBuilder, +- onLinkPress: ((String) -> Unit)?, ++ onLinkPress: ((String, String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + factory: RendererFactory, + ) { +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/ImageRenderer.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/ImageRenderer.kt +index 0047361..58e07f3 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/ImageRenderer.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/ImageRenderer.kt +@@ -9,7 +9,7 @@ class ImageRenderer : NodeRenderer { + override fun render( + node: MarkdownASTNode, + builder: SpannableStringBuilder, +- onLinkPress: ((String) -> Unit)?, ++ onLinkPress: ((String, String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + factory: RendererFactory, + ) { +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/LineBreakRenderer.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/LineBreakRenderer.kt +index 0cfad74..df88c5d 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/LineBreakRenderer.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/LineBreakRenderer.kt +@@ -7,7 +7,7 @@ class LineBreakRenderer : NodeRenderer { + override fun render( + node: MarkdownASTNode, + builder: SpannableStringBuilder, +- onLinkPress: ((String) -> Unit)?, ++ onLinkPress: ((String, String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + factory: RendererFactory, + ) { +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt +index 360a436..2bca5be 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt +@@ -19,7 +19,7 @@ class LinkRenderer( + override fun render( + node: MarkdownASTNode, + builder: SpannableStringBuilder, +- onLinkPress: ((String) -> Unit)?, ++ onLinkPress: ((String, String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + factory: RendererFactory, + ) { +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListItemRenderer.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListItemRenderer.kt +index b849f78..24c06f3 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListItemRenderer.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListItemRenderer.kt +@@ -19,7 +19,7 @@ class ListItemRenderer( + override fun render( + node: MarkdownASTNode, + builder: SpannableStringBuilder, +- onLinkPress: ((String) -> Unit)?, ++ onLinkPress: ((String, String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + factory: RendererFactory, + ) { +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListRenderer.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListRenderer.kt +index 2bac5b1..ff2d2c2 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListRenderer.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListRenderer.kt +@@ -16,7 +16,7 @@ class ListRenderer( + override fun render( + node: MarkdownASTNode, + builder: SpannableStringBuilder, +- onLinkPress: ((String) -> Unit)?, ++ onLinkPress: ((String, String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + factory: RendererFactory, + ) { +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt +index 354b31a..77b1e14 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt +@@ -15,7 +15,7 @@ interface NodeRenderer { + fun render( + node: MarkdownASTNode, + builder: SpannableStringBuilder, +- onLinkPress: ((String) -> Unit)?, ++ onLinkPress: ((String, String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + factory: RendererFactory, + ) +@@ -175,7 +175,7 @@ class RendererFactory( + fun renderChildren( + node: MarkdownASTNode, + builder: SpannableStringBuilder, +- onLinkPress: ((String) -> Unit)?, ++ onLinkPress: ((String, String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + ) { + renderNodes(node.children, builder, onLinkPress, onLinkLongPress) +@@ -190,7 +190,7 @@ class RendererFactory( + fun renderNodes( + nodes: List, + builder: SpannableStringBuilder, +- onLinkPress: ((String) -> Unit)?, ++ onLinkPress: ((String, String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + ) { + nodes.forEach { node -> +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/ParagraphRenderer.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/ParagraphRenderer.kt +index 305acc9..cda43ac 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/ParagraphRenderer.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/ParagraphRenderer.kt +@@ -16,7 +16,7 @@ class ParagraphRenderer( + override fun render( + node: MarkdownASTNode, + builder: SpannableStringBuilder, +- onLinkPress: ((String) -> Unit)?, ++ onLinkPress: ((String, String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + factory: RendererFactory, + ) { +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/Renderer.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/Renderer.kt +index ae4be04..01dac8a 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/Renderer.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/Renderer.kt +@@ -36,7 +36,7 @@ class Renderer { + + fun renderDocument( + document: MarkdownASTNode, +- onLinkPress: ((String) -> Unit)? = null, ++ onLinkPress: ((String, String) -> Unit)? = null, + onLinkLongPress: ((String) -> Unit)? = null, + ): SpannableStringBuilder = renderContent(document.children, onLinkPress, onLinkLongPress) + +@@ -59,7 +59,7 @@ class Renderer { + */ + fun renderContent( + nodes: List, +- onLinkPress: ((String) -> Unit)? = null, ++ onLinkPress: ((String, String) -> Unit)? = null, + onLinkLongPress: ((String) -> Unit)? = null, + block: BlockquoteTextRenderer? = null, + ): SpannableStringBuilder { +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/SoftBreakRenderer.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/SoftBreakRenderer.kt +index 334b545..3ae58ff 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/SoftBreakRenderer.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/SoftBreakRenderer.kt +@@ -7,7 +7,7 @@ class SoftBreakRenderer : NodeRenderer { + override fun render( + node: MarkdownASTNode, + builder: SpannableStringBuilder, +- onLinkPress: ((String) -> Unit)?, ++ onLinkPress: ((String, String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + factory: RendererFactory, + ) { +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpoilerRenderer.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpoilerRenderer.kt +index 430f98e..8dbdca7 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpoilerRenderer.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpoilerRenderer.kt +@@ -9,7 +9,7 @@ class SpoilerRenderer : NodeRenderer { + override fun render( + node: MarkdownASTNode, + builder: SpannableStringBuilder, +- onLinkPress: ((String) -> Unit)?, ++ onLinkPress: ((String, String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + factory: RendererFactory, + ) { +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrikethroughRenderer.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrikethroughRenderer.kt +index 8ffa61e..c6b562c 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrikethroughRenderer.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrikethroughRenderer.kt +@@ -11,7 +11,7 @@ class StrikethroughRenderer( + override fun render( + node: MarkdownASTNode, + builder: SpannableStringBuilder, +- onLinkPress: ((String) -> Unit)?, ++ onLinkPress: ((String, String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + factory: RendererFactory, + ) { +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrongRenderer.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrongRenderer.kt +index 584d79b..702b41e 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrongRenderer.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrongRenderer.kt +@@ -11,7 +11,7 @@ class StrongRenderer( + override fun render( + node: MarkdownASTNode, + builder: SpannableStringBuilder, +- onLinkPress: ((String) -> Unit)?, ++ onLinkPress: ((String, String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + factory: RendererFactory, + ) { +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/TextRenderer.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/TextRenderer.kt +index 54e324e..f2856d2 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/TextRenderer.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/TextRenderer.kt +@@ -9,7 +9,7 @@ class TextRenderer : NodeRenderer { + override fun render( + node: MarkdownASTNode, + builder: SpannableStringBuilder, +- onLinkPress: ((String) -> Unit)?, ++ onLinkPress: ((String, String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + factory: RendererFactory, + ) { +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/ThematicBreakRenderer.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/ThematicBreakRenderer.kt +index fa63d17..7c43b57 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/ThematicBreakRenderer.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/ThematicBreakRenderer.kt +@@ -11,7 +11,7 @@ class ThematicBreakRenderer( + override fun render( + node: MarkdownASTNode, + builder: SpannableStringBuilder, +- onLinkPress: ((String) -> Unit)?, ++ onLinkPress: ((String, String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + factory: RendererFactory, + ) { +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/UnderlineRenderer.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/UnderlineRenderer.kt +index d658913..4437eb0 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/UnderlineRenderer.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/renderer/UnderlineRenderer.kt +@@ -11,7 +11,7 @@ class UnderlineRenderer( + override fun render( + node: MarkdownASTNode, + builder: SpannableStringBuilder, +- onLinkPress: ((String) -> Unit)?, ++ onLinkPress: ((String, String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + factory: RendererFactory, + ) { +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/RenderedSegment.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/RenderedSegment.kt +index 0afa2c0..4454e2c 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/RenderedSegment.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/RenderedSegment.kt +@@ -64,7 +64,7 @@ object MarkdownSegmentRenderer { + segments: List, + style: StyleConfig, + context: Context, +- onLinkPress: ((String) -> Unit)?, ++ onLinkPress: ((String, String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + blockquoteStyle: BlockquoteStyle? = null, + onLatexError: LatexErrorReporter? = null, +@@ -107,7 +107,7 @@ object MarkdownSegmentRenderer { + nodes: List, + style: StyleConfig, + context: Context, +- onLinkPress: ((String) -> Unit)?, ++ onLinkPress: ((String, String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + blockquoteStyle: BlockquoteStyle?, + onLatexError: LatexErrorReporter?, +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/SegmentViewCreators.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/SegmentViewCreators.kt +index c9644d1..58b2f4e 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/SegmentViewCreators.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/SegmentViewCreators.kt +@@ -67,7 +67,7 @@ data class SegmentViewConfig( + val selectionHandleColor: Int?, + val contextMenuItemTexts: List, + val dynamicProps: DynamicBlockProps, +- val onLinkPress: ((String) -> Unit)?, ++ val onLinkPress: ((String, String) -> Unit)?, + val onLinkLongPress: ((String) -> Unit)?, + val onTaskListItemPress: ((taskIndex: Int, checked: Boolean, itemText: String) -> Unit)?, + val onContextMenuItemPress: ((itemText: String, selectedText: String, selectionStart: Int, selectionEnd: Int) -> Unit)?, +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/TableContainerView.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/TableContainerView.kt +index e190548..b2e6cf2 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/TableContainerView.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/TableContainerView.kt +@@ -19,7 +19,6 @@ import android.view.Gravity + import android.view.MotionEvent + import android.view.View + import android.widget.FrameLayout +-import android.widget.HorizontalScrollView + import androidx.core.view.ViewCompat + import com.swmansion.enriched.markdown.accessibility.AccessibilityLabels + import com.swmansion.enriched.markdown.parser.MarkdownASTNode +@@ -38,6 +37,7 @@ import com.swmansion.enriched.markdown.utils.text.span.prepareWidthAwareSpans + import com.swmansion.enriched.markdown.utils.text.view.LinkLongPressMovementMethod + import com.swmansion.enriched.markdown.utils.text.view.cancelJSTouchForLinkTap + import com.swmansion.enriched.markdown.utils.text.view.reallowParentInterceptIfLinkReleased ++import com.swmansion.enriched.markdown.views.ClaimingHorizontalScrollView + import com.swmansion.enriched.markdown.views.ContextMenuPopup + import kotlin.math.ceil + import kotlin.math.max +@@ -57,7 +57,7 @@ class TableContainerView( + + var allowFontScaling = true + var maxFontSizeMultiplier = 0f +- var onLinkPress: ((String) -> Unit)? = null ++ var onLinkPress: ((String, String) -> Unit)? = null + var onLinkLongPress: ((String) -> Unit)? = null + var accessibilityLabels: AccessibilityLabels = AccessibilityLabels() + set(value) { +@@ -70,7 +70,7 @@ class TableContainerView( + var dynamicProps: DynamicBlockProps = DynamicBlockProps() + + private val scrollView = +- HorizontalScrollView(context).apply { ++ ClaimingHorizontalScrollView(context).apply { + isHorizontalScrollBarEnabled = true + overScrollMode = View.OVER_SCROLL_NEVER + importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt +index ff00069..f3e13aa 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt +@@ -12,7 +12,7 @@ import com.swmansion.enriched.markdown.utils.text.extensions.applyBlockStyleFont + + class LinkSpan( + val url: String, +- private val onLinkPress: ((String) -> Unit)?, ++ private val onLinkPress: ((String, String) -> Unit)?, + private val onLinkLongPress: ((String) -> Unit)?, + private val styleCache: SpanStyleCache, + private val blockStyle: BlockStyle, +@@ -30,7 +30,8 @@ class LinkSpan( + return + } + +- onLinkPress?.invoke(url) ?: (widget as? EnrichedMarkdownText)?.emitOnLinkPress(url) ++ val sourceType = if (keepsCodeFont) "inline-code" else if (recognizedLink) "text" else "markdown" ++ onLinkPress?.invoke(url, sourceType) ?: (widget as? EnrichedMarkdownText)?.emitOnLinkPress(url, sourceType) + } + + fun onLongClick(widget: View): Boolean { +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +index b4d3cdc..4652b10 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +@@ -37,11 +37,12 @@ fun markdownEventTypeConstants(): MutableMap { + fun emitLinkPress( + view: View, + url: String, ++ sourceType: String, + ) { + val context = view.context as com.facebook.react.bridge.ReactContext + val surfaceId = UIManagerHelper.getSurfaceId(context) + val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id) +- eventDispatcher?.dispatchEvent(LinkPressEvent(surfaceId, view.id, url)) ++ eventDispatcher?.dispatchEvent(LinkPressEvent(surfaceId, view.id, url, sourceType)) + } + + fun emitLinkLongPress( +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/LinkEvents.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/LinkEvents.kt +index fde4b04..b524ea7 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/LinkEvents.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/LinkEvents.kt +@@ -10,11 +10,11 @@ import com.swmansion.enriched.markdown.events.ImagePressEvent + import com.swmansion.enriched.markdown.events.LinkLongPressEvent + import com.swmansion.enriched.markdown.events.LinkPressEvent + +-fun View.emitLinkPressEvent(url: String) { ++fun View.emitLinkPressEvent(url: String, sourceType: String) { + val reactContext = context as? ReactContext ?: return + val surfaceId = UIManagerHelper.getSurfaceId(reactContext) + val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id) +- dispatcher?.dispatchEvent(LinkPressEvent(surfaceId, id, url)) ++ dispatcher?.dispatchEvent(LinkPressEvent(surfaceId, id, url, sourceType)) + } + + fun View.emitImagePressEvent( +diff --git a/node_modules/react-native-enriched-markdown/cpp/parser/MD4CParser.cpp b/node_modules/react-native-enriched-markdown/cpp/parser/MD4CParser.cpp +index c99a52d..c93aaf7 100644 +--- a/node_modules/react-native-enriched-markdown/cpp/parser/MD4CParser.cpp ++++ b/node_modules/react-native-enriched-markdown/cpp/parser/MD4CParser.cpp +@@ -307,6 +307,10 @@ public: + if (!url.empty()) { + node->setAttribute(ATTR_URL, url); + } ++ const auto title = impl->getAttributeText(&linkDetail->title); ++ if (!title.empty()) { ++ node->setAttribute(ATTR_TITLE, title); ++ } + } + impl->pushNode(node); + break; +diff --git a/node_modules/react-native-enriched-markdown/ios/EnrichedMarkdown.mm b/node_modules/react-native-enriched-markdown/ios/EnrichedMarkdown.mm +index 5a0b7e3..be7b30a 100644 +--- a/node_modules/react-native-enriched-markdown/ios/EnrichedMarkdown.mm ++++ b/node_modules/react-native-enriched-markdown/ios/EnrichedMarkdown.mm +@@ -82,7 +82,7 @@ static char kENRMSegmentFadeAnimatorKey; + + @interface EnrichedMarkdown () + + (ENRMMd4cFlags *)flagsFromProps:(const EnrichedMarkdownMd4cFlagsStruct &)props; +-- (void)emitLinkPress:(NSString *)url; ++- (void)emitLinkPress:(NSString *)url sourceType:(NSString *)sourceType; + - (void)emitLinkLongPress:(NSString *)url; + - (void)emitLinkContextMenuItemPress:(NSString *)itemText pattern:(NSString *)pattern url:(NSString *)url; + - (void)emitImagePress:(NSString *)url altText:(NSString *)altText; +@@ -411,10 +411,10 @@ static char kENRMSegmentFadeAnimatorKey; + if (strongSelf) + [strongSelf emitCodeBlockPress:code language:language]; + }; +- view.onLinkPress = ^(NSString *url) { ++ view.onLinkPress = ^(NSString *url, NSString *sourceType) { + EnrichedMarkdown *strongSelf = weakSelf; + if (strongSelf && url) +- [strongSelf emitLinkPress:url]; ++ [strongSelf emitLinkPress:url sourceType:sourceType]; + }; + view.onLinkLongPress = ^(NSString *url) { + EnrichedMarkdown *strongSelf = weakSelf; +@@ -986,10 +986,10 @@ static char kENRMSegmentFadeAnimatorKey; + + __weak EnrichedMarkdown *weakSelf = self; + +- tableView.onLinkPress = ^(NSString *url) { ++ tableView.onLinkPress = ^(NSString *url, NSString *sourceType) { + EnrichedMarkdown *strongSelf = weakSelf; + if (strongSelf && url) +- [strongSelf emitLinkPress:url]; ++ [strongSelf emitLinkPress:url sourceType:sourceType]; + }; + + tableView.onLinkLongPress = ^(NSString *url) { +@@ -1475,11 +1475,11 @@ Class EnrichedMarkdownCls(void) + } + #endif + +-- (void)emitLinkPress:(NSString *)url ++- (void)emitLinkPress:(NSString *)url sourceType:(NSString *)sourceType + { + auto emitter = std::static_pointer_cast(_eventEmitter); + if (emitter) +- emitter->onLinkPress({.url = std::string(url.UTF8String)}); ++ emitter->onLinkPress({.url = std::string(url.UTF8String), .sourceType = std::string(sourceType.UTF8String)}); + } + + - (void)emitImagePress:(NSString *)url altText:(NSString *)altText +@@ -1575,7 +1575,9 @@ Class EnrichedMarkdownCls(void) + } + } + +- if (ENRMHandleTapOnTextView(textView, recognizer, ^(NSString *url) { [self emitLinkPress:url]; })) { ++ if (ENRMHandleTapOnTextView(textView, recognizer, ^(NSString *url, NSString *sourceType) { ++ [self emitLinkPress:url sourceType:sourceType]; ++ })) { + return; + } + +diff --git a/node_modules/react-native-enriched-markdown/ios/EnrichedMarkdownText.mm b/node_modules/react-native-enriched-markdown/ios/EnrichedMarkdownText.mm +index b7b588b..2fa091f 100644 +--- a/node_modules/react-native-enriched-markdown/ios/EnrichedMarkdownText.mm ++++ b/node_modules/react-native-enriched-markdown/ios/EnrichedMarkdownText.mm +@@ -62,7 +62,7 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) { + - (void)applyRenderedText:(NSMutableAttributedString *)attributedText; + - (void)textTapped:(ENRMTapRecognizer *)recognizer; + - (void)setupLayoutManager; +-- (void)emitLinkPress:(NSString *)url; ++- (void)emitLinkPress:(NSString *)url sourceType:(NSString *)sourceType; + - (void)emitLinkLongPress:(NSString *)url; + - (void)emitLinkContextMenuItemPress:(NSString *)itemText pattern:(NSString *)pattern url:(NSString *)url; + - (void)emitImagePress:(NSString *)url altText:(NSString *)altText; +@@ -909,11 +909,11 @@ Class EnrichedMarkdownTextCls(void) + return [super touchEventEmitterAtPoint:point]; + } + +-- (void)emitLinkPress:(NSString *)url ++- (void)emitLinkPress:(NSString *)url sourceType:(NSString *)sourceType + { + auto emitter = std::static_pointer_cast(_eventEmitter); + if (emitter) +- emitter->onLinkPress({.url = std::string(url.UTF8String)}); ++ emitter->onLinkPress({.url = std::string(url.UTF8String), .sourceType = std::string(sourceType.UTF8String)}); + } + + - (void)emitLinkLongPress:(NSString *)url +@@ -994,7 +994,9 @@ Class EnrichedMarkdownTextCls(void) + return; + } + +- if (ENRMHandleTapOnTextView(textView, recognizer, ^(NSString *url) { [self emitLinkPress:url]; })) { ++ if (ENRMHandleTapOnTextView(textView, recognizer, ^(NSString *url, NSString *sourceType) { ++ [self emitLinkPress:url sourceType:sourceType]; ++ })) { + return; + } + +diff --git a/node_modules/react-native-enriched-markdown/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp b/node_modules/react-native-enriched-markdown/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +index 77465ad..94997f7 100644 +--- a/node_modules/react-native-enriched-markdown/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp ++++ b/node_modules/react-native-enriched-markdown/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +@@ -17,6 +17,7 @@ void EnrichedMarkdownEventEmitter::onLinkPress(OnLinkPress event) const { + dispatchEvent("linkPress", [event=std::move(event)](jsi::Runtime &runtime) { + auto payload = jsi::Object(runtime); + payload.setProperty(runtime, "url", event.url); ++ payload.setProperty(runtime, "sourceType", event.sourceType); + return payload; + }); + } +@@ -362,6 +363,7 @@ void EnrichedMarkdownTextEventEmitter::onLinkPress(OnLinkPress event) const { + dispatchEvent("linkPress", [event=std::move(event)](jsi::Runtime &runtime) { + auto payload = jsi::Object(runtime); + payload.setProperty(runtime, "url", event.url); ++ payload.setProperty(runtime, "sourceType", event.sourceType); + return payload; + }); + } +diff --git a/node_modules/react-native-enriched-markdown/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h b/node_modules/react-native-enriched-markdown/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +index 0a1c6c2..5935112 100644 +--- a/node_modules/react-native-enriched-markdown/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h ++++ b/node_modules/react-native-enriched-markdown/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +@@ -19,6 +19,7 @@ class EnrichedMarkdownEventEmitter : public ViewEventEmitter { + + struct OnLinkPress { + std::string url; ++ std::string sourceType; + }; + + struct OnLinkLongPress { +@@ -298,6 +299,7 @@ class EnrichedMarkdownTextEventEmitter : public ViewEventEmitter { + + struct OnLinkPress { + std::string url; ++ std::string sourceType; + }; + + struct OnLinkLongPress { +diff --git a/node_modules/react-native-enriched-markdown/ios/renderer/LinkRenderer.m b/node_modules/react-native-enriched-markdown/ios/renderer/LinkRenderer.m +index c9fd57d..66daf6b 100644 +--- a/node_modules/react-native-enriched-markdown/ios/renderer/LinkRenderer.m ++++ b/node_modules/react-native-enriched-markdown/ios/renderer/LinkRenderer.m +@@ -96,6 +96,8 @@ static NSRange ENRMCollapseLinkIntoPill(NSMutableAttributedString *output, NSRan + options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired + usingBlock:^(NSDictionary *attrs, NSRange subrange, BOOL *stop) { + NSMutableDictionary *newAttributes = [NSMutableDictionary dictionary]; ++ newAttributes[@"linkSourceType"] = ++ recognizedLink ? (attrs[CodeAttributeName] ? @"inline-code" : @"text") : @"markdown"; + if (recognizedLink) { + newAttributes[ENRMRecognizedLinkAttributeName] = @YES; + newAttributes[ENRMRecognizedLinkOriginalUnderlineAttributeName] = +diff --git a/node_modules/react-native-enriched-markdown/ios/segments/ENRMBlockquoteContainerView.h b/node_modules/react-native-enriched-markdown/ios/segments/ENRMBlockquoteContainerView.h +index 1657128..85b0c54 100644 +--- a/node_modules/react-native-enriched-markdown/ios/segments/ENRMBlockquoteContainerView.h ++++ b/node_modules/react-native-enriched-markdown/ios/segments/ENRMBlockquoteContainerView.h +@@ -68,7 +68,7 @@ typedef void (^ENRMBlockquoteLinkBlock)(NSString *url); + + // Bridged up to the JS onLinkPress/onLinkLongPress events; propagated to nested + // quotes so links at any depth fire. +-@property (nonatomic, copy, nullable) ENRMBlockquoteLinkBlock onLinkPress; ++@property (nonatomic, copy, nullable) void (^onLinkPress)(NSString *url, NSString *sourceType); + @property (nonatomic, copy, nullable) ENRMBlockquoteLinkBlock onLinkLongPress; + @property (nonatomic, copy, nullable) void (^onImagePress)(NSString *url, NSString *altText); + +diff --git a/node_modules/react-native-enriched-markdown/ios/segments/ENRMBlockquoteContainerView.mm b/node_modules/react-native-enriched-markdown/ios/segments/ENRMBlockquoteContainerView.mm +index 79c79cf..96c7a70 100644 +--- a/node_modules/react-native-enriched-markdown/ios/segments/ENRMBlockquoteContainerView.mm ++++ b/node_modules/react-native-enriched-markdown/ios/segments/ENRMBlockquoteContainerView.mm +@@ -229,10 +229,10 @@ static UIEdgeInsets ENRMBlockquoteContentInsets(StyleConfig *config) + if (strongSelf) { + view.dynamicProps = strongSelf.dynamicProps; + view.accessibilityLabels = strongSelf.accessibilityLabels; +- view.onLinkPress = ^(NSString *url) { ++ view.onLinkPress = ^(NSString *url, NSString *sourceType) { + ENRMBlockquoteContainerView *s = weakSelf; + if (s.onLinkPress && url) +- s.onLinkPress(url); ++ s.onLinkPress(url, sourceType); + }; + view.onLinkLongPress = ^(NSString *url) { + ENRMBlockquoteContainerView *s = weakSelf; +@@ -299,10 +299,10 @@ static UIEdgeInsets ENRMBlockquoteContentInsets(StyleConfig *config) + if (s.onImagePress && url) + s.onImagePress(url, altText); + }; +- view.onLinkPress = ^(NSString *url) { ++ view.onLinkPress = ^(NSString *url, NSString *sourceType) { + ENRMBlockquoteContainerView *s = weakSelf; + if (s.onLinkPress && url) +- s.onLinkPress(url); ++ s.onLinkPress(url, sourceType); + }; + view.onLinkLongPress = ^(NSString *url) { + ENRMBlockquoteContainerView *s = weakSelf; +@@ -374,10 +374,10 @@ static UIEdgeInsets ENRMBlockquoteContentInsets(StyleConfig *config) + { + ENRMPlatformTextView *textView = (ENRMPlatformTextView *)recognizer.view; + __weak ENRMBlockquoteContainerView *weakSelf = self; +- if (ENRMHandleTapOnTextView(textView, recognizer, ^(NSString *url) { ++ if (ENRMHandleTapOnTextView(textView, recognizer, ^(NSString *url, NSString *sourceType) { + ENRMBlockquoteContainerView *strongSelf = weakSelf; + if (strongSelf.onLinkPress && url) +- strongSelf.onLinkPress(url); ++ strongSelf.onLinkPress(url, sourceType); + })) { + return; + } +diff --git a/node_modules/react-native-enriched-markdown/ios/segments/ENRMTableIOSGridView.h b/node_modules/react-native-enriched-markdown/ios/segments/ENRMTableIOSGridView.h +index bda3a64..0960bad 100644 +--- a/node_modules/react-native-enriched-markdown/ios/segments/ENRMTableIOSGridView.h ++++ b/node_modules/react-native-enriched-markdown/ios/segments/ENRMTableIOSGridView.h +@@ -18,6 +18,7 @@ typedef NS_ENUM(NSInteger, ENRMTableIOSItemKind) { + @interface ENRMTableIOSItemHit : NSObject + @property (nonatomic, assign) ENRMTableIOSItemKind kind; + @property (nonatomic, copy) NSString *url; ++@property (nonatomic, copy) NSString *sourceType; + /// The link's visible label, or an image's alt text. + @property (nonatomic, copy, nullable) NSString *title; + /// The item's frame in grid coordinates. +@@ -34,7 +35,7 @@ typedef NS_ENUM(NSInteger, ENRMTableIOSItemKind) { + /// cause multi-second main-thread hangs on large tables. + @interface ENRMTableIOSGridView : UIView + +-@property (nonatomic, copy, nullable) ENRMTableIOSLinkBlock onLinkTap; ++@property (nonatomic, copy, nullable) void (^onLinkTap)(NSString *url, NSString *sourceType); + @property (nonatomic, copy, nullable) ENRMTableIOSLinkBlock onLinkLongTap; + @property (nonatomic, copy, nullable) void (^onImageTap)(NSString *url, NSString *altText); + /// Asked when a touch lands on a link: a link with a menu is left to the context-menu +diff --git a/node_modules/react-native-enriched-markdown/ios/segments/ENRMTableIOSGridView.m b/node_modules/react-native-enriched-markdown/ios/segments/ENRMTableIOSGridView.m +index 3d7bee5..8b31c8b 100644 +--- a/node_modules/react-native-enriched-markdown/ios/segments/ENRMTableIOSGridView.m ++++ b/node_modules/react-native-enriched-markdown/ios/segments/ENRMTableIOSGridView.m +@@ -193,6 +193,7 @@ static ENRMTableIOSItemHit *itemInAttributedString(NSAttributedString *text, CGR + ENRMTableIOSItemHit *hit = [[ENRMTableIOSItemHit alloc] init]; + hit.kind = linkURL ? ENRMTableIOSItemKindLink : ENRMTableIOSItemKindImage; + hit.url = linkURL ?: imageURL; ++ hit.sourceType = [text attribute:@"linkSourceType" atIndex:charIndex effectiveRange:NULL] ?: @"markdown"; + hit.title = linkURL ? linkTitleAtIndex(text, charIndex) + : [text attribute:@"imageAltText" atIndex:charIndex effectiveRange:NULL]; + hit.frame = CGRectOffset(frame, textRect.origin.x, textRect.origin.y); +@@ -240,7 +241,7 @@ static ENRMTableIOSItemHit *itemInAttributedString(NSAttributedString *text, CGR + return; + if (hit.kind == ENRMTableIOSItemKindLink) { + if (self.onLinkTap) +- self.onLinkTap(hit.url); ++ self.onLinkTap(hit.url, hit.sourceType); + } else if (self.onImageTap) { + self.onImageTap(hit.url, hit.title ?: @""); + } +diff --git a/node_modules/react-native-enriched-markdown/ios/segments/TableContainerView.h b/node_modules/react-native-enriched-markdown/ios/segments/TableContainerView.h +index 0da3e9b..b8f9856 100644 +--- a/node_modules/react-native-enriched-markdown/ios/segments/TableContainerView.h ++++ b/node_modules/react-native-enriched-markdown/ios/segments/TableContainerView.h +@@ -38,7 +38,7 @@ typedef void (^TableLinkPressBlock)(NSString *url); + @property (nonatomic, assign) BOOL allowFontScaling; + @property (nonatomic, assign) CGFloat maxFontSizeMultiplier; + +-@property (nonatomic, copy, nullable) TableLinkPressBlock onLinkPress; ++@property (nonatomic, copy, nullable) void (^onLinkPress)(NSString *url, NSString *sourceType); + @property (nonatomic, copy, nullable) TableLinkPressBlock onLinkLongPress; + @property (nonatomic, copy, nullable) void (^onImagePress)(NSString *url, NSString *altText); + +diff --git a/node_modules/react-native-enriched-markdown/ios/segments/TableContainerView.m b/node_modules/react-native-enriched-markdown/ios/segments/TableContainerView.m +index 987eb21..ec93a4e 100644 +--- a/node_modules/react-native-enriched-markdown/ios/segments/TableContainerView.m ++++ b/node_modules/react-native-enriched-markdown/ios/segments/TableContainerView.m +@@ -315,10 +315,10 @@ static void ENRMTableComputeLayout(NSArray *> *rows, NS + #else + ENRMTableIOSGridView *iosGridView = [[ENRMTableIOSGridView alloc] initWithFrame:CGRectZero]; + __weak TableContainerView *weakSelf = self; +- iosGridView.onLinkTap = ^(NSString *url) { ++ iosGridView.onLinkTap = ^(NSString *url, NSString *sourceType) { + TableContainerView *strongSelf = weakSelf; + if (strongSelf && strongSelf.onLinkPress) +- strongSelf.onLinkPress(url); ++ strongSelf.onLinkPress(url, sourceType); + }; + iosGridView.onImageTap = ^(NSString *url, NSString *altText) { + TableContainerView *strongSelf = weakSelf; +diff --git a/node_modules/react-native-enriched-markdown/ios/utils/ENRMTextInteractionUtils.h b/node_modules/react-native-enriched-markdown/ios/utils/ENRMTextInteractionUtils.h +index 65dc272..c5af8c1 100644 +--- a/node_modules/react-native-enriched-markdown/ios/utils/ENRMTextInteractionUtils.h ++++ b/node_modules/react-native-enriched-markdown/ios/utils/ENRMTextInteractionUtils.h +@@ -11,7 +11,7 @@ extern "C" { + /// If a link is found, calls onLinkPress and returns YES. + /// If no link is found, calls ENRMClearSelection and returns NO. + BOOL ENRMHandleTapOnTextView(ENRMPlatformTextView *textView, ENRMTapRecognizer *recognizer, +- void (^onLinkPress)(NSString *url)); ++ void (^onLinkPress)(NSString *url, NSString *sourceType)); + + #ifdef __cplusplus + } +diff --git a/node_modules/react-native-enriched-markdown/ios/utils/ENRMTextInteractionUtils.m b/node_modules/react-native-enriched-markdown/ios/utils/ENRMTextInteractionUtils.m +index 88d69da..7bb3234 100644 +--- a/node_modules/react-native-enriched-markdown/ios/utils/ENRMTextInteractionUtils.m ++++ b/node_modules/react-native-enriched-markdown/ios/utils/ENRMTextInteractionUtils.m +@@ -2,7 +2,7 @@ + #import "LinkTapUtils.h" + + BOOL ENRMHandleTapOnTextView(ENRMPlatformTextView *textView, ENRMTapRecognizer *recognizer, +- void (^onLinkPress)(NSString *url)) ++ void (^onLinkPress)(NSString *url, NSString *sourceType)) + { + NSString *url = linkURLAtTapLocation(textView, recognizer); + if (!url) { +@@ -10,6 +10,6 @@ BOOL ENRMHandleTapOnTextView(ENRMPlatformTextView *textView, ENRMTapRecognizer * + return NO; + } + if (onLinkPress) +- onLinkPress(url); ++ onLinkPress(url, linkSourceTypeAtTapLocation(textView, recognizer)); + return YES; + } +diff --git a/node_modules/react-native-enriched-markdown/ios/utils/LinkTapUtils.h b/node_modules/react-native-enriched-markdown/ios/utils/LinkTapUtils.h +index 3bab20f..136d07e 100644 +--- a/node_modules/react-native-enriched-markdown/ios/utils/LinkTapUtils.h ++++ b/node_modules/react-native-enriched-markdown/ios/utils/LinkTapUtils.h +@@ -11,6 +11,9 @@ extern "C" { + /// Returns the link URL at the tap location, or nil if no link was tapped. + NSString *_Nullable linkURLAtTapLocation(ENRMPlatformTextView *textView, ENRMTapRecognizer *recognizer); + ++/// Returns the syntax of the link at the tap location. ++NSString *linkSourceTypeAtTapLocation(ENRMPlatformTextView *textView, ENRMTapRecognizer *recognizer); ++ + /// Returns the original link URL at a point in text-view coordinates. + NSString *_Nullable linkURLAtPoint(ENRMPlatformTextView *textView, CGPoint point); + +diff --git a/node_modules/react-native-enriched-markdown/ios/utils/LinkTapUtils.m b/node_modules/react-native-enriched-markdown/ios/utils/LinkTapUtils.m +index 20d8739..6a6ffaa 100644 +--- a/node_modules/react-native-enriched-markdown/ios/utils/LinkTapUtils.m ++++ b/node_modules/react-native-enriched-markdown/ios/utils/LinkTapUtils.m +@@ -13,6 +13,14 @@ NSString *_Nullable linkURLAtTapLocation(ENRMPlatformTextView *textView, ENRMTap + return [attrText attribute:@"linkURL" atIndex:characterIndex effectiveRange:NULL]; + } + ++NSString *linkSourceTypeAtTapLocation(ENRMPlatformTextView *textView, ENRMTapRecognizer *recognizer) ++{ ++ NSUInteger index = ENRMCharacterIndexForTap(textView, recognizer); ++ if (index == NSNotFound) ++ return @"markdown"; ++ return [ENRMGetAttributedText(textView) attribute:@"linkSourceType" atIndex:index effectiveRange:NULL] ?: @"markdown"; ++} ++ + NSString *_Nullable linkURLAtPoint(ENRMPlatformTextView *textView, CGPoint point) + { + NSUInteger index = ENRMCharacterIndexAtPoint(textView, point); +diff --git a/node_modules/react-native-enriched-markdown/lib/module/EnrichedMarkdownNativeComponent.ts b/node_modules/react-native-enriched-markdown/lib/module/EnrichedMarkdownNativeComponent.ts +index 32ff768..8e89249 100644 +--- a/node_modules/react-native-enriched-markdown/lib/module/EnrichedMarkdownNativeComponent.ts ++++ b/node_modules/react-native-enriched-markdown/lib/module/EnrichedMarkdownNativeComponent.ts +@@ -275,6 +275,7 @@ export interface MarkdownStyleInternal { + + export interface LinkPressEvent { + url: string; ++ sourceType: string; + } + + export interface LinkLongPressEvent { +diff --git a/node_modules/react-native-enriched-markdown/lib/module/EnrichedMarkdownTextNativeComponent.ts b/node_modules/react-native-enriched-markdown/lib/module/EnrichedMarkdownTextNativeComponent.ts +index 6f31ccc..7b08b62 100644 +--- a/node_modules/react-native-enriched-markdown/lib/module/EnrichedMarkdownTextNativeComponent.ts ++++ b/node_modules/react-native-enriched-markdown/lib/module/EnrichedMarkdownTextNativeComponent.ts +@@ -276,6 +276,7 @@ export interface MarkdownStyleInternal { + + export interface LinkPressEvent { + url: string; ++ sourceType: string; + } + + export interface LinkLongPressEvent { +diff --git a/node_modules/react-native-enriched-markdown/lib/module/index.js.map b/node_modules/react-native-enriched-markdown/lib/module/index.js.map +index bef09da..c23b50f 100644 +--- a/node_modules/react-native-enriched-markdown/lib/module/index.js.map ++++ b/node_modules/react-native-enriched-markdown/lib/module/index.js.map +@@ -1 +1 @@ +-{"version":3,"names":["default","EnrichedMarkdownText","DEFAULT_ACCESSIBILITY_LABELS","resolveAccessibilityLabels","EnrichedMarkdownTextInput"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAMA,SAASA,OAAO,IAAIC,oBAAoB,QAAQ,kCAA+B;AAuB/E,SACEC,4BAA4B,EAC5BC,0BAA0B,QACrB,iCAA8B;AAErC,SAASC,yBAAyB,QAAQ,gCAA6B","ignoreList":[]} ++{"version":3,"names":["default","EnrichedMarkdownText","DEFAULT_ACCESSIBILITY_LABELS","resolveAccessibilityLabels","EnrichedMarkdownTextInput"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAUA,SAASA,OAAO,IAAIC,oBAAoB,QAAQ,kCAA+B;AAuB/E,SACEC,4BAA4B,EAC5BC,0BAA0B,QACrB,iCAA8B;AAErC,SAASC,yBAAyB,QAAQ,gCAA6B","ignoreList":[]} +diff --git a/node_modules/react-native-enriched-markdown/lib/module/index.web.js.map b/node_modules/react-native-enriched-markdown/lib/module/index.web.js.map +index 2691825..acac81c 100644 +--- a/node_modules/react-native-enriched-markdown/lib/module/index.web.js.map ++++ b/node_modules/react-native-enriched-markdown/lib/module/index.web.js.map +@@ -1 +1 @@ +-{"version":3,"names":["EnrichedMarkdownText","default"],"sourceRoot":"../../src","sources":["index.web.tsx"],"mappings":";;AAAA,SAASA,oBAAoB,EAAEC,OAAO,QAAQ,+BAA4B","ignoreList":[]} ++{"version":3,"names":["EnrichedMarkdownText","default"],"sourceRoot":"../../src","sources":["index.web.tsx"],"mappings":";;AAIA,SAASA,oBAAoB,EAAEC,OAAO,QAAQ,+BAA4B","ignoreList":[]} +diff --git a/node_modules/react-native-enriched-markdown/lib/module/native/EnrichedMarkdownText.js b/node_modules/react-native-enriched-markdown/lib/module/native/EnrichedMarkdownText.js +index 9fb1ad2..2532d7c 100644 +--- a/node_modules/react-native-enriched-markdown/lib/module/native/EnrichedMarkdownText.js ++++ b/node_modules/react-native-enriched-markdown/lib/module/native/EnrichedMarkdownText.js +@@ -106,6 +106,7 @@ export const EnrichedMarkdownText = ({ + writingDirection = 'first-strong', + numberOfLines, + ellipsizeMode, ++ renderers: _renderers, + ...rest + }) => { + // An equal config keeps the previous object, so an inline literal does not +@@ -205,10 +206,12 @@ export const EnrichedMarkdownText = ({ + }, []); + const handleLinkPress = useCallback(e => { + const { +- url ++ url, ++ sourceType + } = e.nativeEvent; + onLinkPress?.({ +- url ++ url, ++ sourceType: sourceType + }); + }, [onLinkPress]); + const handleLinkLongPress = useCallback(e => { +diff --git a/node_modules/react-native-enriched-markdown/lib/module/native/EnrichedMarkdownText.js.map b/node_modules/react-native-enriched-markdown/lib/module/native/EnrichedMarkdownText.js.map +index 2f27901..fc55b09 100644 +--- a/node_modules/react-native-enriched-markdown/lib/module/native/EnrichedMarkdownText.js.map ++++ b/node_modules/react-native-enriched-markdown/lib/module/native/EnrichedMarkdownText.js.map +@@ -1 +1 @@ +-{"version":3,"names":["useMemo","useCallback","useRef","useEffect","EnrichedMarkdownTextNativeComponent","EnrichedMarkdownNativeComponent","isLinkRecognitionEqual","toNativeLinkRecognition","normalizeMarkdownStyle","isLinkPillContentEqual","normalizeLinkPillContent","dispatchLinkContextMenuItem","isLinkContextMenuItemsEqual","normalizeLinkContextMenuItems","resolveAccessibilityLabels","normalizeMenuItem","normalizeLegacyBooleanMenuItem","jsx","_jsx","DEFAULT_COPY_LABEL","DEFAULT_COPY_AS_MARKDOWN_LABEL","DEFAULT_COPY_IMAGE_URL_LABEL","DEFAULT_COPY_IMAGE_URLS_LABEL","buildPluralTemplates","pluralLabels","singularLabel","IntlRef","Intl","PluralRules","pluralRules","other","byCategory","zero","one","two","few","many","templates","n","push","select","defaultMd4cFlags","underline","superscript","subscript","latexMath","highlight","hardSoftBreaks","preserveBlankLines","admonitions","EnrichedMarkdownText","markdown","markdownStyle","containerStyle","onLinkPress","onLinkLongPress","onImagePress","onTaskListItemPress","enableTaskListItemToggle","onCopyPress","onLatexError","onCodeBlockPress","enableBlockContextMenu","enableLinkPreview","selectable","md4cFlags","allowFontScaling","maxFontSizeMultiplier","allowTrailingMargin","flavor","streamingAnimation","streamingConfig","spoilerOverlay","contextMenuItems","linkContextMenuItems","imageRequestHeaders","linkPillContent","linkRecognition","selectionMenuConfig","accessibilityLabels","selectionColor","selectionHandleColor","textBreakStrategy","lineBreakStrategyIOS","writingDirection","numberOfLines","ellipsizeMode","rest","linkRecognitionRef","undefined","nativeLinkRecognition","next","current","normalizedStyleRef","normalized","normalizedStyle","normalizedMd4cFlags","contextMenuCallbacksRef","Map","callbacksMap","item","set","text","onPress","nativeContextMenuItems","filter","visible","map","icon","linkContextMenuItemsRef","nativeLinkContextMenuItemsRef","nativeLinkContextMenuItems","handleLinkContextMenuItemPress","event","url","pattern","itemText","nativeEvent","nativeImageRequestHeaders","Object","entries","name","value","sort","a","b","linkPillContentRef","nativeLinkPillContent","handleContextMenuItemPress","e","selectedText","selectionStart","selectionEnd","callback","get","selection","start","end","handleLinkPress","handleLinkLongPress","handleImagePress","altText","handleTaskListItemPress","index","checked","handleCopyPress","code","language","handleLatexError","source","message","displayMode","handleCodeBlockPress","tableMode","codeBlockMode","normalizedStreamingConfig","normalizedSelectionMenuConfig","config","copy","copyAsMarkdown","copyImageUrl","enabled","copyLabel","label","copyAsMarkdownLabel","copyImageUrlLabel","copyImageUrlsLabel","copyImageUrlPluralTemplates","resolvedAccessibilityLabels","sharedProps","enableImagePress","enableCodeBlockPress","isGFM","style","onLinkContextMenuItemPress","onContextMenuItemPress"],"sourceRoot":"../../../src","sources":["native/EnrichedMarkdownText.tsx"],"mappings":"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","ignoreList":[]} ++{"version":3,"names":["useMemo","useCallback","useRef","useEffect","EnrichedMarkdownTextNativeComponent","EnrichedMarkdownNativeComponent","isLinkRecognitionEqual","toNativeLinkRecognition","normalizeMarkdownStyle","isLinkPillContentEqual","normalizeLinkPillContent","dispatchLinkContextMenuItem","isLinkContextMenuItemsEqual","normalizeLinkContextMenuItems","resolveAccessibilityLabels","normalizeMenuItem","normalizeLegacyBooleanMenuItem","jsx","_jsx","DEFAULT_COPY_LABEL","DEFAULT_COPY_AS_MARKDOWN_LABEL","DEFAULT_COPY_IMAGE_URL_LABEL","DEFAULT_COPY_IMAGE_URLS_LABEL","buildPluralTemplates","pluralLabels","singularLabel","IntlRef","Intl","PluralRules","pluralRules","other","byCategory","zero","one","two","few","many","templates","n","push","select","defaultMd4cFlags","underline","superscript","subscript","latexMath","highlight","hardSoftBreaks","preserveBlankLines","admonitions","EnrichedMarkdownText","markdown","markdownStyle","containerStyle","onLinkPress","onLinkLongPress","onImagePress","onTaskListItemPress","enableTaskListItemToggle","onCopyPress","onLatexError","onCodeBlockPress","enableBlockContextMenu","enableLinkPreview","selectable","md4cFlags","allowFontScaling","maxFontSizeMultiplier","allowTrailingMargin","flavor","streamingAnimation","streamingConfig","spoilerOverlay","contextMenuItems","linkContextMenuItems","imageRequestHeaders","linkPillContent","linkRecognition","selectionMenuConfig","accessibilityLabels","selectionColor","selectionHandleColor","textBreakStrategy","lineBreakStrategyIOS","writingDirection","numberOfLines","ellipsizeMode","renderers","_renderers","rest","linkRecognitionRef","undefined","nativeLinkRecognition","next","current","normalizedStyleRef","normalized","normalizedStyle","normalizedMd4cFlags","contextMenuCallbacksRef","Map","callbacksMap","item","set","text","onPress","nativeContextMenuItems","filter","visible","map","icon","linkContextMenuItemsRef","nativeLinkContextMenuItemsRef","nativeLinkContextMenuItems","handleLinkContextMenuItemPress","event","url","pattern","itemText","nativeEvent","nativeImageRequestHeaders","Object","entries","name","value","sort","a","b","linkPillContentRef","nativeLinkPillContent","handleContextMenuItemPress","e","selectedText","selectionStart","selectionEnd","callback","get","selection","start","end","handleLinkPress","sourceType","handleLinkLongPress","handleImagePress","altText","handleTaskListItemPress","index","checked","handleCopyPress","code","language","handleLatexError","source","message","displayMode","handleCodeBlockPress","tableMode","codeBlockMode","normalizedStreamingConfig","normalizedSelectionMenuConfig","config","copy","copyAsMarkdown","copyImageUrl","enabled","copyLabel","label","copyAsMarkdownLabel","copyImageUrlLabel","copyImageUrlsLabel","copyImageUrlPluralTemplates","resolvedAccessibilityLabels","sharedProps","enableImagePress","enableCodeBlockPress","isGFM","style","onLinkContextMenuItemPress","onContextMenuItemPress"],"sourceRoot":"../../../src","sources":["native/EnrichedMarkdownText.tsx"],"mappings":"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","ignoreList":[]} +diff --git a/node_modules/react-native-enriched-markdown/lib/module/types/MarkdownRenderers.js b/node_modules/react-native-enriched-markdown/lib/module/types/MarkdownRenderers.js +new file mode 100644 +index 0000000..c04179d +--- /dev/null ++++ b/node_modules/react-native-enriched-markdown/lib/module/types/MarkdownRenderers.js +@@ -0,0 +1,4 @@ ++"use strict"; ++ ++export {}; ++//# sourceMappingURL=MarkdownRenderers.js.map +\ No newline at end of file +diff --git a/node_modules/react-native-enriched-markdown/lib/module/web/EnrichedMarkdownText.js b/node_modules/react-native-enriched-markdown/lib/module/web/EnrichedMarkdownText.js +index 2cf2708..8581623 100644 +--- a/node_modules/react-native-enriched-markdown/lib/module/web/EnrichedMarkdownText.js ++++ b/node_modules/react-native-enriched-markdown/lib/module/web/EnrichedMarkdownText.js +@@ -3,7 +3,7 @@ + import { useState, useEffect, useMemo, useRef } from 'react'; + import { normalizeMarkdownStyle } from "../normalizeMarkdownStyle.web.js"; + import { zeroTrailingMargins, parseErrorFallbackStyle, buildStyles } from "./styles.js"; +-import { parseMarkdown } from "./parseMarkdown.js"; ++import { parseMarkdown, parseMarkdownSync } from "./parseMarkdown.js"; + import { recognizeTextLinks } from "./recognizeTextLinks.js"; + import { RenderNode } from "./renderers/index.js"; + import { indexTaskItems, markInlineImages } from "./utils.js"; +@@ -16,6 +16,7 @@ export const EnrichedMarkdownText = ({ + markdownStyle = {}, + md4cFlags = {}, + linkRecognition, ++ renderers, + onLinkPress, + onLinkLongPress, + onImagePress, +@@ -31,7 +32,7 @@ export const EnrichedMarkdownText = ({ + ...rest + }) => { + const normalizedStyle = useMemo(() => normalizeMarkdownStyle(markdownStyle), [markdownStyle]); +- const [ast, setAst] = useState(null); ++ const [parsed, setParsed] = useState(null); + const [katex, setKatex] = useState(null); + const [parseError, setParseError] = useState(false); + const { +@@ -50,7 +51,39 @@ export const EnrichedMarkdownText = ({ + linkRecognitionRef.current = linkRecognition; + const textLinkKey = linkRecognition?.text?.toString() ?? null; + const inlineCodeLinkKey = linkRecognition?.inlineCode?.toString() ?? null; ++ ++ // Once WASM is ready, new/virtualized rows must have their text on the first ++ // commit. An effect-only parse briefly measures an empty row and moves readers. ++ const immediate = useMemo(() => { ++ try { ++ const result = parseMarkdownSync(markdown, { ++ underline, ++ latexMath, ++ superscript, ++ subscript, ++ highlight, ++ hardSoftBreaks, ++ preserveBlankLines, ++ admonitions ++ }); ++ if (!result) return null; ++ indexTaskItems(result); ++ markInlineImages(result); ++ return { ++ markdown, ++ ast: recognizeTextLinks(result, linkRecognitionRef.current?.text, linkRecognitionRef.current?.inlineCode) ++ }; ++ } catch { ++ return { ++ markdown, ++ ast: null ++ }; ++ } ++ // Regex keys track ref changes; parsed wakes this memo after WASM initialization. ++ // eslint-disable-next-line react-hooks/exhaustive-deps ++ }, [markdown, underline, latexMath, superscript, subscript, highlight, hardSoftBreaks, preserveBlankLines, admonitions, textLinkKey, inlineCodeLinkKey, parsed]); + useEffect(() => { ++ if (immediate && (!latexMath || katex)) return; + let cancelled = false; + const katexPromise = latexMath ? loadKaTeX() : Promise.resolve(null); + Promise.all([parseMarkdown(markdown, { +@@ -68,7 +101,10 @@ export const EnrichedMarkdownText = ({ + markInlineImages(result); + setParseError(false); + setKatex(katexInstance); +- setAst(recognizeTextLinks(result, linkRecognitionRef.current?.text, linkRecognitionRef.current?.inlineCode)); ++ setParsed({ ++ markdown, ++ ast: recognizeTextLinks(result, linkRecognitionRef.current?.text, linkRecognitionRef.current?.inlineCode) ++ }); + } + }).catch(error => { + if (!cancelled) { +@@ -76,14 +112,14 @@ export const EnrichedMarkdownText = ({ + console.error('[EnrichedMarkdownText] Parse failed:', error); + } + setParseError(true); +- setAst(null); ++ setParsed(null); + setKatex(null); + } + }); + return () => { + cancelled = true; + }; +- }, [markdown, underline, latexMath, superscript, subscript, highlight, hardSoftBreaks, preserveBlankLines, admonitions, textLinkKey, inlineCodeLinkKey]); ++ }, [markdown, underline, latexMath, superscript, subscript, highlight, hardSoftBreaks, preserveBlankLines, admonitions, textLinkKey, inlineCodeLinkKey, immediate, katex]); + const callbacks = useMemo(() => ({ + onLinkPress, + onLinkLongPress, +@@ -93,8 +129,9 @@ export const EnrichedMarkdownText = ({ + }), [onLinkPress, onLinkLongPress, onImagePress, onTaskListItemPress, onCodeBlockPress]); + const capabilities = useMemo(() => ({ + katex, +- enableTaskListItemToggle +- }), [katex, enableTaskListItemToggle]); ++ enableTaskListItemToggle, ++ renderers ++ }), [katex, enableTaskListItemToggle, renderers]); + const lastChildStyle = useMemo(() => allowTrailingMargin ? normalizedStyle : zeroTrailingMargins(normalizedStyle), [normalizedStyle, allowTrailingMargin]); + const styles = useMemo(() => buildStyles(normalizedStyle), [normalizedStyle]); + const lastChildStyles = useMemo(() => buildStyles(lastChildStyle), [lastChildStyle]); +@@ -110,7 +147,8 @@ export const EnrichedMarkdownText = ({ + } : null) + }), [containerStyle, selectable, selectionColor]); + const domProps = filterNativeOnlyProps(rest); +- if (parseError) { ++ const visibleParsed = immediate ?? parsed; ++ if (immediate ? immediate.ast === null : parseError) { + return /*#__PURE__*/_jsx("div", { + className: ENRM_TEXT_CLASS, + style: wrapperStyle, +@@ -123,10 +161,10 @@ export const EnrichedMarkdownText = ({ + }) + }); + } +- if (!ast) return null; +- const children = ast.children ?? []; ++ const children = visibleParsed?.ast?.children ?? []; + const lastIdx = children.length - 1; + return /*#__PURE__*/_jsx("div", { ++ "aria-busy": visibleParsed?.markdown !== markdown, + className: ENRM_TEXT_CLASS, + style: wrapperStyle, + dir: dir, +diff --git a/node_modules/react-native-enriched-markdown/lib/module/web/EnrichedMarkdownText.js.map b/node_modules/react-native-enriched-markdown/lib/module/web/EnrichedMarkdownText.js.map +index 1c49b41..921a946 100644 +--- a/node_modules/react-native-enriched-markdown/lib/module/web/EnrichedMarkdownText.js.map ++++ b/node_modules/react-native-enriched-markdown/lib/module/web/EnrichedMarkdownText.js.map +@@ -1 +1 @@ +-{"version":3,"names":["useState","useEffect","useMemo","useRef","normalizeMarkdownStyle","zeroTrailingMargins","parseErrorFallbackStyle","buildStyles","parseMarkdown","recognizeTextLinks","RenderNode","indexTaskItems","markInlineImages","loadKaTeX","ENRM_TEXT_CLASS","ENRM_SELECTION_BG_VAR","filterNativeOnlyProps","jsx","_jsx","EnrichedMarkdownText","markdown","markdownStyle","md4cFlags","linkRecognition","onLinkPress","onLinkLongPress","onImagePress","onTaskListItemPress","onCodeBlockPress","enableTaskListItemToggle","allowTrailingMargin","containerStyle","selectable","dir","selectionColor","testID","rest","normalizedStyle","ast","setAst","katex","setKatex","parseError","setParseError","underline","latexMath","superscript","subscript","highlight","hardSoftBreaks","preserveBlankLines","admonitions","linkRecognitionRef","current","textLinkKey","text","toString","inlineCodeLinkKey","inlineCode","cancelled","katexPromise","Promise","resolve","all","then","result","katexInstance","catch","error","__DEV__","console","callbacks","capabilities","lastChildStyle","styles","lastChildStyles","wrapperStyle","display","flexDirection","undefined","userSelect","domProps","className","style","children","lastIdx","length","map","child","index","node","type"],"sourceRoot":"../../../src","sources":["web/EnrichedMarkdownText.tsx"],"mappings":"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","ignoreList":[]} ++{"version":3,"names":["useState","useEffect","useMemo","useRef","normalizeMarkdownStyle","zeroTrailingMargins","parseErrorFallbackStyle","buildStyles","parseMarkdown","parseMarkdownSync","recognizeTextLinks","RenderNode","indexTaskItems","markInlineImages","loadKaTeX","ENRM_TEXT_CLASS","ENRM_SELECTION_BG_VAR","filterNativeOnlyProps","jsx","_jsx","EnrichedMarkdownText","markdown","markdownStyle","md4cFlags","linkRecognition","renderers","onLinkPress","onLinkLongPress","onImagePress","onTaskListItemPress","onCodeBlockPress","enableTaskListItemToggle","allowTrailingMargin","containerStyle","selectable","dir","selectionColor","testID","rest","normalizedStyle","parsed","setParsed","katex","setKatex","parseError","setParseError","underline","latexMath","superscript","subscript","highlight","hardSoftBreaks","preserveBlankLines","admonitions","linkRecognitionRef","current","textLinkKey","text","toString","inlineCodeLinkKey","inlineCode","immediate","result","ast","cancelled","katexPromise","Promise","resolve","all","then","katexInstance","catch","error","__DEV__","console","callbacks","capabilities","lastChildStyle","styles","lastChildStyles","wrapperStyle","display","flexDirection","undefined","userSelect","domProps","visibleParsed","className","style","children","lastIdx","length","map","child","index","node","type"],"sourceRoot":"../../../src","sources":["web/EnrichedMarkdownText.tsx"],"mappings":"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","ignoreList":[]} +diff --git a/node_modules/react-native-enriched-markdown/lib/module/web/parseMarkdown.js b/node_modules/react-native-enriched-markdown/lib/module/web/parseMarkdown.js +--- a/node_modules/react-native-enriched-markdown/lib/module/web/parseMarkdown.js ++++ b/node_modules/react-native-enriched-markdown/lib/module/web/parseMarkdown.js +@@ -1,14 +1,19 @@ ++import createMd4c from './wasm/md4c'; + "use strict"; + + // Caching the Promise (not the resolved value) means concurrent callers share + // a single WASM initialization — no duplicate loading. ++let readyParser = null; + let parserPromise = null; + + // SINGLE_FILE=1 inlines the WASM binary as base64 inside md4c.js, so no + // network fetch is needed — only a one-time decode + compile on first call. + function initializeParser() { + if (!parserPromise) { +- parserPromise = import('./wasm/md4c').then(module => module.default()).then(wasmModule => wasmModule.cwrap('parseMarkdown', 'string', ['string', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number'])).catch(error => { ++ parserPromise = Promise.resolve().then(() => createMd4c()).then(wasmModule => wasmModule.cwrap('parseMarkdown', 'string', ['string', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number'])).then(parse => { ++ readyParser = parse; ++ return parse; ++ }).catch(error => { + parserPromise = null; + throw error; + }); +@@ -18,7 +23,7 @@ + function isASTNode(value) { + return typeof value === 'object' && value !== null && 'type' in value && typeof value.type === 'string'; + } +-export async function parseMarkdown(markdown, { ++export function parseMarkdownSync(markdown, { + underline = false, + latexMath = true, + superscript = false, +@@ -28,11 +33,16 @@ + preserveBlankLines = false, + admonitions = true + } = {}) { +- const parse = await initializeParser(); ++ const parse = readyParser; ++ if (!parse) return null; + const result = JSON.parse(parse(markdown, underline ? 1 : 0, latexMath ? 1 : 0, superscript ? 1 : 0, subscript ? 1 : 0, highlight ? 1 : 0, hardSoftBreaks ? 1 : 0, preserveBlankLines ? 1 : 0, admonitions ? 1 : 0)); + if (!isASTNode(result)) { + throw new Error('WASM parser returned invalid AST'); + } + return result; + } ++export async function parseMarkdown(markdown, flags = {}) { ++ await initializeParser(); ++ return parseMarkdownSync(markdown, flags); ++} + //# sourceMappingURL=parseMarkdown.js.map +\ No newline at end of file +diff --git a/node_modules/react-native-enriched-markdown/lib/module/web/parseMarkdown.js.map b/node_modules/react-native-enriched-markdown/lib/module/web/parseMarkdown.js.map +index 250fa18..ea79a10 100644 +--- a/node_modules/react-native-enriched-markdown/lib/module/web/parseMarkdown.js.map ++++ b/node_modules/react-native-enriched-markdown/lib/module/web/parseMarkdown.js.map +@@ -1 +1 @@ +-{"version":3,"names":["parserPromise","initializeParser","then","module","default","wasmModule","cwrap","catch","error","isASTNode","value","type","parseMarkdown","markdown","underline","latexMath","superscript","subscript","highlight","hardSoftBreaks","preserveBlankLines","admonitions","parse","result","JSON","Error"],"sourceRoot":"../../../src","sources":["web/parseMarkdown.ts"],"mappings":";;AAeA;AACA;AACA,IAAIA,aAAsC,GAAG,IAAI;;AAEjD;AACA;AACA,SAASC,gBAAgBA,CAAA,EAAqB;EAC5C,IAAI,CAACD,aAAa,EAAE;IAClBA,aAAa,GAAG,MAAM,CAAC,aAAa,CAAC,CAClCE,IAAI,CAAEC,MAAM,IAAKA,MAAM,CAACC,OAAO,CAAC,CAAC,CAAC,CAClCF,IAAI,CAAEG,UAAU,IACfA,UAAU,CAACC,KAAK,CAAC,eAAe,EAAE,QAAQ,EAAE,CAC1C,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,QAAQ,CACT,CACH,CAAC,CACAC,KAAK,CAAEC,KAAK,IAAK;MAChBR,aAAa,GAAG,IAAI;MACpB,MAAMQ,KAAK;IACb,CAAC,CAAqB;EAC1B;EACA,OAAOR,aAAa;AACtB;AAEA,SAASS,SAASA,CAACC,KAAc,EAAoB;EACnD,OACE,OAAOA,KAAK,KAAK,QAAQ,IACzBA,KAAK,KAAK,IAAI,IACd,MAAM,IAAIA,KAAK,IACf,OAAQA,KAAK,CAAaC,IAAI,KAAK,QAAQ;AAE/C;AAEA,OAAO,eAAeC,aAAaA,CACjCC,QAAgB,EAChB;EACEC,SAAS,GAAG,KAAK;EACjBC,SAAS,GAAG,IAAI;EAChBC,WAAW,GAAG,KAAK;EACnBC,SAAS,GAAG,KAAK;EACjBC,SAAS,GAAG,KAAK;EACjBC,cAAc,GAAG,KAAK;EACtBC,kBAAkB,GAAG,KAAK;EAC1BC,WAAW,GAAG;AACL,CAAC,GAAG,CAAC,CAAC,EACC;EAClB,MAAMC,KAAK,GAAG,MAAMrB,gBAAgB,CAAC,CAAC;EAEtC,MAAMsB,MAAe,GAAGC,IAAI,CAACF,KAAK,CAChCA,KAAK,CACHT,QAAQ,EACRC,SAAS,GAAG,CAAC,GAAG,CAAC,EACjBC,SAAS,GAAG,CAAC,GAAG,CAAC,EACjBC,WAAW,GAAG,CAAC,GAAG,CAAC,EACnBC,SAAS,GAAG,CAAC,GAAG,CAAC,EACjBC,SAAS,GAAG,CAAC,GAAG,CAAC,EACjBC,cAAc,GAAG,CAAC,GAAG,CAAC,EACtBC,kBAAkB,GAAG,CAAC,GAAG,CAAC,EAC1BC,WAAW,GAAG,CAAC,GAAG,CACpB,CACF,CAAC;EAED,IAAI,CAACZ,SAAS,CAACc,MAAM,CAAC,EAAE;IACtB,MAAM,IAAIE,KAAK,CAAC,kCAAkC,CAAC;EACrD;EAEA,OAAOF,MAAM;AACf","ignoreList":[]} ++{"version":3,"names":["readyParser","parserPromise","initializeParser","then","module","default","wasmModule","cwrap","parse","catch","error","isASTNode","value","type","parseMarkdownSync","markdown","underline","latexMath","superscript","subscript","highlight","hardSoftBreaks","preserveBlankLines","admonitions","result","JSON","Error","parseMarkdown","flags"],"sourceRoot":"../../../src","sources":["web/parseMarkdown.ts"],"mappings":";;AAeA;AACA;AACA,IAAIA,WAA2B,GAAG,IAAI;AACtC,IAAIC,aAAsC,GAAG,IAAI;;AAEjD;AACA;AACA,SAASC,gBAAgBA,CAAA,EAAqB;EAC5C,IAAI,CAACD,aAAa,EAAE;IAClBA,aAAa,GAAG,MAAM,CAAC,aAAa,CAAC,CAClCE,IAAI,CAAEC,MAAM,IAAKA,MAAM,CAACC,OAAO,CAAC,CAAC,CAAC,CAClCF,IAAI,CAAEG,UAAU,IACfA,UAAU,CAACC,KAAK,CAAC,eAAe,EAAE,QAAQ,EAAE,CAC1C,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,QAAQ,CACT,CACH,CAAC,CACAJ,IAAI,CAAEK,KAAK,IAAK;MACfR,WAAW,GAAGQ,KAAgB;MAC9B,OAAOA,KAAK;IACd,CAAC,CAAC,CACDC,KAAK,CAAEC,KAAK,IAAK;MAChBT,aAAa,GAAG,IAAI;MACpB,MAAMS,KAAK;IACb,CAAC,CAAqB;EAC1B;EACA,OAAOT,aAAa;AACtB;AAEA,SAASU,SAASA,CAACC,KAAc,EAAoB;EACnD,OACE,OAAOA,KAAK,KAAK,QAAQ,IACzBA,KAAK,KAAK,IAAI,IACd,MAAM,IAAIA,KAAK,IACf,OAAQA,KAAK,CAAaC,IAAI,KAAK,QAAQ;AAE/C;AAEA,OAAO,SAASC,iBAAiBA,CAC/BC,QAAgB,EAChB;EACEC,SAAS,GAAG,KAAK;EACjBC,SAAS,GAAG,IAAI;EAChBC,WAAW,GAAG,KAAK;EACnBC,SAAS,GAAG,KAAK;EACjBC,SAAS,GAAG,KAAK;EACjBC,cAAc,GAAG,KAAK;EACtBC,kBAAkB,GAAG,KAAK;EAC1BC,WAAW,GAAG;AACL,CAAC,GAAG,CAAC,CAAC,EACD;EAChB,MAAMf,KAAK,GAAGR,WAAW;EACzB,IAAI,CAACQ,KAAK,EAAE,OAAO,IAAI;EAEvB,MAAMgB,MAAe,GAAGC,IAAI,CAACjB,KAAK,CAChCA,KAAK,CACHO,QAAQ,EACRC,SAAS,GAAG,CAAC,GAAG,CAAC,EACjBC,SAAS,GAAG,CAAC,GAAG,CAAC,EACjBC,WAAW,GAAG,CAAC,GAAG,CAAC,EACnBC,SAAS,GAAG,CAAC,GAAG,CAAC,EACjBC,SAAS,GAAG,CAAC,GAAG,CAAC,EACjBC,cAAc,GAAG,CAAC,GAAG,CAAC,EACtBC,kBAAkB,GAAG,CAAC,GAAG,CAAC,EAC1BC,WAAW,GAAG,CAAC,GAAG,CACpB,CACF,CAAC;EAED,IAAI,CAACZ,SAAS,CAACa,MAAM,CAAC,EAAE;IACtB,MAAM,IAAIE,KAAK,CAAC,kCAAkC,CAAC;EACrD;EAEA,OAAOF,MAAM;AACf;AAEA,OAAO,eAAeG,aAAaA,CACjCZ,QAAgB,EAChBa,KAAgB,GAAG,CAAC,CAAC,EACH;EAClB,MAAM1B,gBAAgB,CAAC,CAAC;EACxB,OAAOY,iBAAiB,CAACC,QAAQ,EAAEa,KAAK,CAAC;AAC3C","ignoreList":[]} +diff --git a/node_modules/react-native-enriched-markdown/lib/module/web/renderers/BlockRenderers.js b/node_modules/react-native-enriched-markdown/lib/module/web/renderers/BlockRenderers.js +index dd70713..ca0385a 100644 +--- a/node_modules/react-native-enriched-markdown/lib/module/web/renderers/BlockRenderers.js ++++ b/node_modules/react-native-enriched-markdown/lib/module/web/renderers/BlockRenderers.js +@@ -101,12 +101,20 @@ function AdmonitionRenderer({ + }); + } + function CodeBlockRenderer({ ++ capabilities, + node, + styles, + renderChildren, + callbacks + }) { + const language = node.attributes?.language; ++ const CustomCodeBlock = capabilities.renderers?.codeBlock; ++ if (CustomCodeBlock) { ++ return /*#__PURE__*/_jsx(CustomCodeBlock, { ++ code: extractNodeText(node), ++ language: language ?? '' ++ }); ++ } + const label = language ? `Code block: ${language}` : 'Code block'; + if (callbacks.onCodeBlockPress == null) { + return /*#__PURE__*/_jsx("pre", { +diff --git a/node_modules/react-native-enriched-markdown/lib/module/web/renderers/BlockRenderers.js.map b/node_modules/react-native-enriched-markdown/lib/module/web/renderers/BlockRenderers.js.map +index dc917c4..cf59150 100644 +--- a/node_modules/react-native-enriched-markdown/lib/module/web/renderers/BlockRenderers.js.map ++++ b/node_modules/react-native-enriched-markdown/lib/module/web/renderers/BlockRenderers.js.map +@@ -1 +1 @@ +-{"version":3,"names":["extractNodeText","filenameFromUrl","toHeadingLevel","KaTeXRenderer","ADMONITION_ICON_PATHS","ADMONITION_ICON_VIEWBOX","ADMONITION_TITLES","ENRM_ADMONITION_CLASS","Fragment","_Fragment","jsx","_jsx","jsxs","_jsxs","ParagraphRenderer","node","styles","parentType","renderChildren","isImageOnly","children","length","type","style","paragraphInBlockquote","paragraph","HeadingRenderer","Tag","attributes","level","BlockquoteRenderer","blockquote","AdmonitionRenderer","admonitionType","colors","admonitions","note","tint","color","iconSize","Math","ceil","fontSize","boxStyle","borderInlineStart","borderWidth","backgroundColor","headerStyle","display","alignItems","gap","round","marginBottom","fontWeight","lineHeight","className","width","height","viewBox","fill","d","CodeBlockRenderer","callbacks","language","label","onCodeBlockPress","codeBlock","codeBlockFont","press","code","replace","handleClick","selection","window","getSelection","isCollapsed","handleKeyDown","event","repeat","key","preventDefault","cursor","role","tabIndex","onClick","onKeyDown","ThematicBreakRenderer","thematicBreak","BlankLineRenderer","count","Number","parseInt","lines","isFinite","max","ImageRenderer","url","title","markdownAlt","trim","alt","imgStyle","isInline","inlineImage","image","interactive","onImagePress","src","altText","LatexMathDisplayRenderer","capabilities","content","katex","displayMode","mathDisplay","VideoRenderer","controls","playsInline","preload","video","blockRenderers","Paragraph","Heading","Blockquote","Admonition","CodeBlock","ThematicBreak","BlankLine","Image","LatexMathDisplay","Video"],"sourceRoot":"../../../../src","sources":["web/renderers/BlockRenderers.tsx"],"mappings":"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","ignoreList":[]} ++{"version":3,"names":["extractNodeText","filenameFromUrl","toHeadingLevel","KaTeXRenderer","ADMONITION_ICON_PATHS","ADMONITION_ICON_VIEWBOX","ADMONITION_TITLES","ENRM_ADMONITION_CLASS","Fragment","_Fragment","jsx","_jsx","jsxs","_jsxs","ParagraphRenderer","node","styles","parentType","renderChildren","isImageOnly","children","length","type","style","paragraphInBlockquote","paragraph","HeadingRenderer","Tag","attributes","level","BlockquoteRenderer","blockquote","AdmonitionRenderer","admonitionType","colors","admonitions","note","tint","color","iconSize","Math","ceil","fontSize","boxStyle","borderInlineStart","borderWidth","backgroundColor","headerStyle","display","alignItems","gap","round","marginBottom","fontWeight","lineHeight","className","width","height","viewBox","fill","d","CodeBlockRenderer","capabilities","callbacks","language","CustomCodeBlock","renderers","codeBlock","code","label","onCodeBlockPress","codeBlockFont","press","replace","handleClick","selection","window","getSelection","isCollapsed","handleKeyDown","event","repeat","key","preventDefault","cursor","role","tabIndex","onClick","onKeyDown","ThematicBreakRenderer","thematicBreak","BlankLineRenderer","count","Number","parseInt","lines","isFinite","max","ImageRenderer","url","title","markdownAlt","trim","alt","imgStyle","isInline","inlineImage","image","interactive","onImagePress","src","altText","LatexMathDisplayRenderer","content","katex","displayMode","mathDisplay","VideoRenderer","controls","playsInline","preload","video","blockRenderers","Paragraph","Heading","Blockquote","Admonition","CodeBlock","ThematicBreak","BlankLine","Image","LatexMathDisplay","Video"],"sourceRoot":"../../../../src","sources":["web/renderers/BlockRenderers.tsx"],"mappings":"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","ignoreList":[]} +diff --git a/node_modules/react-native-enriched-markdown/lib/module/web/renderers/InlineRenderers.js.map b/node_modules/react-native-enriched-markdown/lib/module/web/renderers/InlineRenderers.js.map +index 72a29d9..aa61748 100644 +--- a/node_modules/react-native-enriched-markdown/lib/module/web/renderers/InlineRenderers.js.map ++++ b/node_modules/react-native-enriched-markdown/lib/module/web/renderers/InlineRenderers.js.map +@@ -1 +1 @@ +-{"version":3,"names":["extractNodeText","KaTeXRenderer","linkStyleForUrl","Fragment","_Fragment","jsx","_jsx","TextRenderer","node","children","content","LineBreakRenderer","_props","SoftBreakRenderer","StrongRenderer","styles","renderChildren","style","strong","EmphasisRenderer","emphasis","StrikethroughRenderer","strikethrough","UnderlineRenderer","underline","SuperscriptRenderer","superscript","SubscriptRenderer","subscript","HighlightRenderer","highlight","CodeRenderer","code","LinkRenderer","callbacks","url","attributes","handleClick","event","onLinkPress","preventDefault","handleContextMenu","onLinkLongPress","href","target","rel","onClick","onContextMenu","LatexMathInlineRenderer","capabilities","katex","displayMode","mathInline","inlineRenderers","Text","LineBreak","SoftBreak","Strong","Emphasis","Strikethrough","Underline","Superscript","Subscript","Highlight","Code","Link","LatexMathInline"],"sourceRoot":"../../../../src","sources":["web/renderers/InlineRenderers.tsx"],"mappings":";;AAEA,SAASA,eAAe,QAAQ,aAAU;AAC1C,SAASC,aAAa,QAAQ,oBAAiB;AAC/C,SAASC,eAAe,QAAQ,cAAW;AAAC,SAAAC,QAAA,IAAAC,SAAA,EAAAC,GAAA,IAAAC,IAAA;AAE5C,SAASC,YAAYA,CAAC;EAAEC;AAAoB,CAAC,EAAE;EAC7C,oBAAOF,IAAA,CAAAF,SAAA;IAAAK,QAAA,EAAGD,IAAI,CAACE,OAAO,IAAI;EAAE,CAAG,CAAC;AAClC;AAEA,SAASC,iBAAiBA,CAACC,MAAqB,EAAE;EAChD,oBAAON,IAAA,SAAK,CAAC;AACf;AAEA,SAASO,iBAAiBA,CAACD,MAAqB,EAAE;EAChD,oBAAON,IAAA,CAAAF,SAAA;IAAAK,QAAA,EAAE;EAAC,CAAE,CAAC;AACf;AAEA,SAASK,cAAcA,CAAC;EAAEN,IAAI;EAAEO,MAAM;EAAEC;AAA8B,CAAC,EAAE;EACvE,oBAAOV,IAAA;IAAQW,KAAK,EAAEF,MAAM,CAACG,MAAO;IAAAT,QAAA,EAAEO,cAAc,CAACR,IAAI;EAAC,CAAS,CAAC;AACtE;AAEA,SAASW,gBAAgBA,CAAC;EAAEX,IAAI;EAAEO,MAAM;EAAEC;AAA8B,CAAC,EAAE;EACzE,oBAAOV,IAAA;IAAIW,KAAK,EAAEF,MAAM,CAACK,QAAS;IAAAX,QAAA,EAAEO,cAAc,CAACR,IAAI;EAAC,CAAK,CAAC;AAChE;AAEA,SAASa,qBAAqBA,CAAC;EAC7Bb,IAAI;EACJO,MAAM;EACNC;AACa,CAAC,EAAE;EAChB,oBAAOV,IAAA;IAAGW,KAAK,EAAEF,MAAM,CAACO,aAAc;IAAAb,QAAA,EAAEO,cAAc,CAACR,IAAI;EAAC,CAAI,CAAC;AACnE;AAEA,SAASe,iBAAiBA,CAAC;EAAEf,IAAI;EAAEO,MAAM;EAAEC;AAA8B,CAAC,EAAE;EAC1E,oBAAOV,IAAA;IAAGW,KAAK,EAAEF,MAAM,CAACS,SAAU;IAAAf,QAAA,EAAEO,cAAc,CAACR,IAAI;EAAC,CAAI,CAAC;AAC/D;AAEA,SAASiB,mBAAmBA,CAAC;EAAEjB,IAAI;EAAEO,MAAM;EAAEC;AAA8B,CAAC,EAAE;EAC5E,oBAAOV,IAAA;IAAKW,KAAK,EAAEF,MAAM,CAACW,WAAY;IAAAjB,QAAA,EAAEO,cAAc,CAACR,IAAI;EAAC,CAAM,CAAC;AACrE;AAEA,SAASmB,iBAAiBA,CAAC;EAAEnB,IAAI;EAAEO,MAAM;EAAEC;AAA8B,CAAC,EAAE;EAC1E,oBAAOV,IAAA;IAAKW,KAAK,EAAEF,MAAM,CAACa,SAAU;IAAAnB,QAAA,EAAEO,cAAc,CAACR,IAAI;EAAC,CAAM,CAAC;AACnE;AAEA,SAASqB,iBAAiBA,CAAC;EAAErB,IAAI;EAAEO,MAAM;EAAEC;AAA8B,CAAC,EAAE;EAC1E,oBAAOV,IAAA;IAAMW,KAAK,EAAEF,MAAM,CAACe,SAAU;IAAArB,QAAA,EAAEO,cAAc,CAACR,IAAI;EAAC,CAAO,CAAC;AACrE;AAEA,SAASuB,YAAYA,CAAC;EAAEvB,IAAI;EAAEO,MAAM;EAAEC;AAA8B,CAAC,EAAE;EACrE,oBACEV,IAAA;IAAMW,KAAK,EAAEF,MAAM,CAACiB,IAAK;IAAAvB,QAAA,EAAED,IAAI,CAACE,OAAO,IAAIM,cAAc,CAACR,IAAI;EAAC,CAAO,CAAC;AAE3E;AAEA,SAASyB,YAAYA,CAAC;EACpBzB,IAAI;EACJS,KAAK;EACLiB,SAAS;EACTlB;AACa,CAAC,EAAE;EAChB,MAAMmB,GAAG,GAAG3B,IAAI,CAAC4B,UAAU,EAAED,GAAG;EAEhC,IAAI,CAACA,GAAG,EAAE,oBAAO7B,IAAA,CAAAF,SAAA;IAAAK,QAAA,EAAGO,cAAc,CAACR,IAAI;EAAC,CAAG,CAAC;EAE5C,MAAM6B,WAAW,GAAIC,KAAiB,IAAK;IACzC,IAAIJ,SAAS,CAACK,WAAW,EAAE;MACzBD,KAAK,CAACE,cAAc,CAAC,CAAC;MACtBN,SAAS,CAACK,WAAW,CAAC;QAAEJ;MAAI,CAAC,CAAC;IAChC;EACF,CAAC;EAED,MAAMM,iBAAiB,GAAIH,KAAiB,IAAK;IAC/C,IAAIJ,SAAS,CAACQ,eAAe,EAAE;MAC7BJ,KAAK,CAACE,cAAc,CAAC,CAAC;MACtBN,SAAS,CAACQ,eAAe,CAAC;QAAEP;MAAI,CAAC,CAAC;IACpC;EACF,CAAC;EAED,oBACE7B,IAAA;IACEqC,IAAI,EAAER,GAAI;IACVlB,KAAK,EAAEf,eAAe,CAACe,KAAK,EAAEkB,GAAG,CAAE;IACnCS,MAAM,EAAC,QAAQ;IACfC,GAAG,EAAC,qBAAqB;IACzBC,OAAO,EAAET,WAAY;IACrBU,aAAa,EAAEN,iBAAkB;IAAAhC,QAAA,EAEhCO,cAAc,CAACR,IAAI;EAAC,CACpB,CAAC;AAER;AAEA,SAASwC,uBAAuBA,CAAC;EAC/BxC,IAAI;EACJO,MAAM;EACNkC;AACa,CAAC,EAAE;EAChB,MAAMvC,OAAO,GAAGV,eAAe,CAACQ,IAAI,CAAC;EAErC,oBACEF,IAAA,CAACL,aAAa;IACZS,OAAO,EAAEA,OAAQ;IACjBwC,KAAK,EAAED,YAAY,CAACC,KAAM;IAC1BC,WAAW,EAAE,KAAM;IACnBlC,KAAK,EAAEF,MAAM,CAACqC;EAAW,CAC1B,CAAC;AAEN;AAEA,OAAO,MAAMC,eAA4B,GAAG;EAC1CC,IAAI,EAAE/C,YAAY;EAClBgD,SAAS,EAAE5C,iBAAiB;EAC5B6C,SAAS,EAAE3C,iBAAiB;EAC5B4C,MAAM,EAAE3C,cAAc;EACtB4C,QAAQ,EAAEvC,gBAAgB;EAC1BwC,aAAa,EAAEtC,qBAAqB;EACpCuC,SAAS,EAAErC,iBAAiB;EAC5BsC,WAAW,EAAEpC,mBAAmB;EAChCqC,SAAS,EAAEnC,iBAAiB;EAC5BoC,SAAS,EAAElC,iBAAiB;EAC5BmC,IAAI,EAAEjC,YAAY;EAClBkC,IAAI,EAAEhC,YAAY;EAClBiC,eAAe,EAAElB;AACnB,CAAC","ignoreList":[]} ++{"version":3,"names":["extractNodeText","KaTeXRenderer","linkStyleForUrl","Fragment","_Fragment","jsx","_jsx","TextRenderer","node","children","content","LineBreakRenderer","_props","SoftBreakRenderer","StrongRenderer","styles","renderChildren","style","strong","EmphasisRenderer","emphasis","StrikethroughRenderer","strikethrough","UnderlineRenderer","underline","SuperscriptRenderer","superscript","SubscriptRenderer","subscript","HighlightRenderer","highlight","CodeRenderer","code","LinkRenderer","callbacks","url","attributes","handleClick","event","onLinkPress","preventDefault","sourceType","recognizedLink","some","child","type","handleContextMenu","onLinkLongPress","href","title","target","rel","onClick","onContextMenu","LatexMathInlineRenderer","capabilities","katex","displayMode","mathInline","inlineRenderers","Text","LineBreak","SoftBreak","Strong","Emphasis","Strikethrough","Underline","Superscript","Subscript","Highlight","Code","Link","LatexMathInline"],"sourceRoot":"../../../../src","sources":["web/renderers/InlineRenderers.tsx"],"mappings":"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","ignoreList":[]} +diff --git a/node_modules/react-native-enriched-markdown/lib/module/web/renderers/ListRenderers.js b/node_modules/react-native-enriched-markdown/lib/module/web/renderers/ListRenderers.js +index 7718d0a..ed95c24 100644 +--- a/node_modules/react-native-enriched-markdown/lib/module/web/renderers/ListRenderers.js ++++ b/node_modules/react-native-enriched-markdown/lib/module/web/renderers/ListRenderers.js +@@ -16,6 +16,7 @@ function ListRenderer({ + const hasTaskChild = !isNested && node.children?.some(child => child.attributes?.isTask === 'true'); + const resolvedStyle = isNested ? styles.listNested : hasTaskChild ? styles.listTask : styles.list; + return /*#__PURE__*/_jsx(ListTag, { ++ start: ListTag === 'ol' ? Number(node.attributes?.start ?? 1) : undefined, + style: resolvedStyle, + children: renderChildren(node) + }); +diff --git a/node_modules/react-native-enriched-markdown/lib/module/web/renderers/ListRenderers.js.map b/node_modules/react-native-enriched-markdown/lib/module/web/renderers/ListRenderers.js.map +index 049421a..c05b993 100644 +--- a/node_modules/react-native-enriched-markdown/lib/module/web/renderers/ListRenderers.js.map ++++ b/node_modules/react-native-enriched-markdown/lib/module/web/renderers/ListRenderers.js.map +@@ -1 +1 @@ +-{"version":3,"names":["useState","useEffect","extractNodeText","listItemStyle","checkedTaskTextStyle","jsx","_jsx","jsxs","_jsxs","NESTED_LIST_TYPES","Set","ListRenderer","node","styles","parentType","renderChildren","listTag","ListTag","isNested","hasTaskChild","children","some","child","attributes","isTask","resolvedStyle","listNested","listTask","list","style","UnorderedListRenderer","props","OrderedListRenderer","isNestedList","has","type","ListItemRenderer","index","callbacks","capabilities","initialChecked","taskChecked","taskText","isChecked","setIsChecked","handleChange","enableTaskListItemToggle","taskIndex","undefined","newChecked","onTaskListItemPress","checked","text","hasNestedList","checkedStyle","inlineNode","filter","nestedNode","onChange","readOnly","taskCheckbox","taskCheckboxDisabled","listRenderers","UnorderedList","OrderedList","ListItem"],"sourceRoot":"../../../../src","sources":["web/renderers/ListRenderers.tsx"],"mappings":"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","ignoreList":[]} ++{"version":3,"names":["useState","useEffect","extractNodeText","listItemStyle","checkedTaskTextStyle","jsx","_jsx","jsxs","_jsxs","NESTED_LIST_TYPES","Set","ListRenderer","node","styles","parentType","renderChildren","listTag","ListTag","isNested","hasTaskChild","children","some","child","attributes","isTask","resolvedStyle","listNested","listTask","list","start","Number","undefined","style","UnorderedListRenderer","props","OrderedListRenderer","isNestedList","has","type","ListItemRenderer","index","callbacks","capabilities","initialChecked","taskChecked","taskText","isChecked","setIsChecked","handleChange","enableTaskListItemToggle","taskIndex","newChecked","onTaskListItemPress","checked","text","hasNestedList","checkedStyle","inlineNode","filter","nestedNode","onChange","readOnly","taskCheckbox","taskCheckboxDisabled","listRenderers","UnorderedList","OrderedList","ListItem"],"sourceRoot":"../../../../src","sources":["web/renderers/ListRenderers.tsx"],"mappings":";;AAAA,SAASA,QAAQ,EAAEC,SAAS,QAAQ,OAAO;AAC3C,SAASC,eAAe,QAAQ,aAAU;AAE1C,SAASC,aAAa,EAAEC,oBAAoB,QAAQ,cAAW;AAAC,SAAAC,GAAA,IAAAC,IAAA,EAAAC,IAAA,IAAAC,KAAA;AAEhE,MAAMC,iBAAgC,GAAG,IAAIC,GAAG,CAAC,CAC/C,eAAe,EACf,aAAa,CACd,CAAC;AAEF,SAASC,YAAYA,CAAC;EACpBC,IAAI;EACJC,MAAM;EACNC,UAAU;EACVC,cAAc;EACdC,OAAO,EAAEC;AAC+B,CAAC,EAAE;EAC3C,MAAMC,QAAQ,GAAGJ,UAAU,KAAK,UAAU;EAC1C,MAAMK,YAAY,GAChB,CAACD,QAAQ,IACTN,IAAI,CAACQ,QAAQ,EAAEC,IAAI,CAAEC,KAAK,IAAKA,KAAK,CAACC,UAAU,EAAEC,MAAM,KAAK,MAAM,CAAC;EAErE,MAAMC,aAAa,GAAGP,QAAQ,GAC1BL,MAAM,CAACa,UAAU,GACjBP,YAAY,GACVN,MAAM,CAACc,QAAQ,GACfd,MAAM,CAACe,IAAI;EACjB,oBACEtB,IAAA,CAACW,OAAO;IACNY,KAAK,EAAEZ,OAAO,KAAK,IAAI,GAAGa,MAAM,CAAClB,IAAI,CAACW,UAAU,EAAEM,KAAK,IAAI,CAAC,CAAC,GAAGE,SAAU;IAC1EC,KAAK,EAAEP,aAAc;IAAAL,QAAA,EAEpBL,cAAc,CAACH,IAAI;EAAC,CACd,CAAC;AAEd;AAEA,SAASqB,qBAAqBA,CAACC,KAAoB,EAAE;EACnD,oBAAO5B,IAAA,CAACK,YAAY;IAAA,GAAKuB,KAAK;IAAElB,OAAO,EAAC;EAAI,CAAE,CAAC;AACjD;AAEA,SAASmB,mBAAmBA,CAACD,KAAoB,EAAE;EACjD,oBAAO5B,IAAA,CAACK,YAAY;IAAA,GAAKuB,KAAK;IAAElB,OAAO,EAAC;EAAI,CAAE,CAAC;AACjD;AAEA,SAASoB,YAAYA,CAACd,KAAc,EAAW;EAC7C,OAAOb,iBAAiB,CAAC4B,GAAG,CAACf,KAAK,CAACgB,IAAI,CAAC;AAC1C;AAEA,SAASC,gBAAgBA,CAAC;EACxB3B,IAAI;EACJoB,KAAK;EACLnB,MAAM;EACN2B,KAAK;EACLC,SAAS;EACTC,YAAY;EACZ3B;AACa,CAAC,EAAE;EAChB,MAAMS,MAAM,GAAGZ,IAAI,CAACW,UAAU,EAAEC,MAAM,KAAK,MAAM;EACjD,MAAMmB,cAAc,GAAG/B,IAAI,CAACW,UAAU,EAAEqB,WAAW,KAAK,MAAM;EAC9D,MAAMC,QAAQ,GAAGrB,MAAM,GAAGtB,eAAe,CAACU,IAAI,CAAC,GAAG,EAAE;EACpD,MAAM,CAACkC,SAAS,EAAEC,YAAY,CAAC,GAAG/C,QAAQ,CAAC2C,cAAc,CAAC;EAE1D1C,SAAS,CAAC,MAAM;IACd8C,YAAY,CAACJ,cAAc,CAAC;EAC9B,CAAC,EAAE,CAACA,cAAc,CAAC,CAAC;EAEpB,MAAMK,YAAY,GAAGA,CAAA,KAAM;IACzB,IAAI,CAACN,YAAY,CAACO,wBAAwB,EAAE;IAE5C,MAAMC,SAAS,GAAGtC,IAAI,CAACW,UAAU,EAAE2B,SAAS;IAC5C,IAAIA,SAAS,KAAKnB,SAAS,EAAE;IAE7B,MAAMoB,UAAU,GAAG,CAACL,SAAS;IAC7BC,YAAY,CAACI,UAAU,CAAC;IAExBV,SAAS,CAACW,mBAAmB,GAAG;MAC9BZ,KAAK,EAAEU,SAAS;MAChBG,OAAO,EAAEF,UAAU;MACnBG,IAAI,EAAET;IACR,CAAC,CAAC;EACJ,CAAC;EAED,MAAMU,aAAa,GAAG3C,IAAI,CAACQ,QAAQ,EAAEC,IAAI,CAACe,YAAY,CAAC;EACvD,MAAMoB,YAAY,GAChBhC,MAAM,IAAIsB,SAAS,GAAG1C,oBAAoB,CAAC4B,KAAK,CAAC,GAAGD,SAAS;EAE/D,MAAM0B,UAAmB,GAAGF,aAAa,GACrC;IACE,GAAG3C,IAAI;IACPQ,QAAQ,EAAER,IAAI,CAACQ,QAAQ,EAAEsC,MAAM,CAAEpC,KAAK,IAAK,CAACc,YAAY,CAACd,KAAK,CAAC;EACjE,CAAC,GACDV,IAAI;EACR,MAAM+C,UAA0B,GAAGJ,aAAa,GAC5C;IAAE,GAAG3C,IAAI;IAAEQ,QAAQ,EAAER,IAAI,CAACQ,QAAQ,EAAEsC,MAAM,CAACtB,YAAY;EAAE,CAAC,GAC1D,IAAI;EAER,oBACE5B,KAAA;IAAIwB,KAAK,EAAE7B,aAAa,CAAC6B,KAAK,EAAER,MAAM,EAAE,CAACgB,KAAK,IAAI,CAAC,MAAM,CAAC,CAAE;IAAApB,QAAA,gBAC1DZ,KAAA;MAAMwB,KAAK,EAAEwB,YAAa;MAAApC,QAAA,GACvBI,MAAM,iBACLlB,IAAA;QACEgC,IAAI,EAAC,UAAU;QACfe,OAAO,EAAEP,SAAU;QACnBc,QAAQ,EAAEZ,YAAa;QACvBa,QAAQ,EAAE,CAACnB,YAAY,CAACO,wBAAyB;QACjD,iBACEP,YAAY,CAACO,wBAAwB,GAAGlB,SAAS,GAAG,IACrD;QACDC,KAAK,EACHU,YAAY,CAACO,wBAAwB,GACjCpC,MAAM,CAACiD,YAAY,GACnBjD,MAAM,CAACkD,oBACZ;QACD,cAAY,SAASlB,QAAQ;MAAG,CACjC,CACF,EACA9B,cAAc,CAAC0C,UAAU,CAAC;IAAA,CACvB,CAAC,EACNE,UAAU,IAAI5C,cAAc,CAAC4C,UAAU,CAAC;EAAA,CACvC,CAAC;AAET;AAEA,OAAO,MAAMK,aAA0B,GAAG;EACxCC,aAAa,EAAEhC,qBAAqB;EACpCiC,WAAW,EAAE/B,mBAAmB;EAChCgC,QAAQ,EAAE5B;AACZ,CAAC","ignoreList":[]} +diff --git a/node_modules/react-native-enriched-markdown/lib/module/web/wasm/md4c.js b/node_modules/react-native-enriched-markdown/lib/module/web/wasm/md4c.js +index 978a8c7..950cccd 100644 +--- a/node_modules/react-native-enriched-markdown/lib/module/web/wasm/md4c.js ++++ b/node_modules/react-native-enriched-markdown/lib/module/web/wasm/md4c.js +@@ -3,10 +3,11 @@ + var createMd4cModule = (() => { + var _scriptName = globalThis.document?.currentScript?.src; + return async function (moduleArg = {}) { ++ var moduleRtn; + var Module = moduleArg; + var ENVIRONMENT_IS_WEB = true; + var ENVIRONMENT_IS_WORKER = false; +- var programArgs = []; ++ var arguments_ = []; + var thisProgram = "./this.program"; + var scriptDirectory = ""; + var readAsync; +@@ -30,8 +31,6 @@ var createMd4cModule = (() => { + var err = console.error.bind(console); + var wasmBinary; + var ABORT = false; +- class EmscriptenEH {} +- class EmscriptenSjLj extends EmscriptenEH {} + function binaryDecode(bin) { + for (var i = 0, l = bin.length, o = new Uint8Array(l), c; i < l; ++i) { + c = bin.charCodeAt(i); +@@ -39,13 +38,12 @@ var createMd4cModule = (() => { + } + return o; + } ++ var readyPromiseResolve, readyPromiseReject; ++ var HEAP8, HEAPU8, HEAP16, HEAPU16, HEAP32, HEAPU32, HEAPF32, HEAPF64; ++ var HEAP64, HEAPU64; + var runtimeInitialized = false; +- function getMemoryBuffer() { +- return wasmMemory.buffer; +- } + function updateMemoryViews() { +- if (HEAP8?.buffer?.resizable) return; +- var b = getMemoryBuffer(); ++ var b = wasmMemory.buffer; + HEAP8 = new Int8Array(b); + HEAP16 = new Int16Array(b); + HEAPU8 = new Uint8Array(b); +@@ -58,10 +56,11 @@ var createMd4cModule = (() => { + HEAPU64 = new BigUint64Array(b); + } + function preRun() { +- var preRun = Module["preRun"]; +- if (preRun) { +- if (typeof preRun == "function") preRun = [preRun]; +- onPreRuns.push(...preRun); ++ if (Module["preRun"]) { ++ if (typeof Module["preRun"] == "function") Module["preRun"] = [Module["preRun"]]; ++ while (Module["preRun"].length) { ++ addOnPreRun(Module["preRun"].shift()); ++ } + } + callRuntimeCallbacks(onPreRuns); + } +@@ -70,25 +69,27 @@ var createMd4cModule = (() => { + wasmExports["__wasm_call_ctors"](); + } + function postRun() { +- var postRun = Module["postRun"]; +- if (postRun) { +- if (typeof postRun == "function") postRun = [postRun]; +- onPostRuns.push(...postRun); ++ if (Module["postRun"]) { ++ if (typeof Module["postRun"] == "function") Module["postRun"] = [Module["postRun"]]; ++ while (Module["postRun"].length) { ++ addOnPostRun(Module["postRun"].shift()); ++ } + } + callRuntimeCallbacks(onPostRuns); + } + function abort(what) { + Module["onAbort"]?.(what); +- what = `Aborted(${what})`; ++ what = "Aborted(" + what + ")"; + err(what); + ABORT = true; + what += ". Build with -sASSERTIONS for more info."; + var e = new WebAssembly.RuntimeError(what); ++ readyPromiseReject?.(e); + throw e; + } + var wasmBinaryFile; + function findWasmBinary() { +- return binaryDecode('asm¬`````````````` ````||`~`|``Tenv __cxa_throwenv\r__assert_failenv _abort_js\nenvemscripten_resize_heap˜–\n\r\n\n\n   \r     \r \n  \n  p44€€@ A à€ ¢memory__wasm_call_ctors\rparseMarkdown__indirect_function_table_emscripten_stack_restore—_emscripten_stack_alloc˜emscripten_stack_get_current™ JA 3” \rq !"#$%&\'()s*B=DZfghq*‚‚„’†*‘އ*‹‰*“•–• (\nªº–æAàÚB7AèÚA6A÷ÚA:AìÚAØ\r(6AðÚAÜ\r-:AñÚA:AƒÛA:AøÚA±\r/;AúÚA³\r-:AûÚA:AÛA:A„ÛAŠ(6AˆÛAŽ-:A‰ÛA:A›ÛA :AÛA™ )7A˜ÛA¡ -:A™ÛA:A ÛB칻֮سå7A«ÛA:A¨ÛA:A·ÛA:A¬ÛAá\r(6A°ÛAå\r/;A²ÛA:A¸ÛAt"6A¼ÛB‹€€€€‚€€€7 AÆ(6 A¿)7 A: AÏÛA:AÄÛA¦ (6AÈÛAª -:AÉÛA:AÛÛA:AÐÛAÈ (6AÔÛAÌ -:AÕÛA:AÜÛAt"6AàÛBŽ€€€€‚€€€7 AÖ)7 AÐ)7 A: AëÚ,AH@AèÚ(AàÚ(k õ”!~#A0k"$@ E@@A€\\"AèÚ("AÿÿÿÿqAkA\nAëÚ,"AH""M@AàÚ(AàÚ !@  @ A€ ü\n AëÚ- Av ÀAH@AäÚ 6  AëÚ Aÿq:  jA:  AàÚ   kAäÚ( "A  A€x AàÚAàÚ(AëÚ,AN  A:,  AG:/  AG:.  AG:-  AG:+  AG:*  AG:)  AG:(  AG:\'A$t"A6 B7 B7 B7 B7 A j" 6 ! \\"A÷ÿÿÿO\r@@ A O@ Ar"Ajt!  Aÿÿÿÿk6  6  6   : A j! E\r E\r  ü\n  jA: Aj! A\'j!#AÐk"$@ A j"( , " AH"E@A<t"B7 Aì6 B7 B7 B7 B7$ B74 B€€€ü7,  6  A j6  (! AéOA4 Aèn" A4OA jA !A<t" B7 Aì6 B7 B7 B7 B7$ B74 B€€€ü7,  A j6 (!  6@ E\r (" Ak6 \r (( r Aj! ("\n! ( " \nG@@@ Ak(" E\r ("Ak6 \r (( r \n Ak" G\r (!  \n6 @A€  A€O" ( kAuM\r At"t! \n k"\n@ \nü\n   j6  \n j6  6 E\r k !@@@@ (" ("\nI@ (6 ("6 @  (Aj6 Aj!  ("k" Au"Aj" A€€€€O\rAÿÿÿÿ \n k"\nAu"  K \nAøÿÿÿO" A€€€€O\r At"\nt" j" (6 ("6  \nj!\n   (Aj6 ( ( ("k" Au  Atk! @   ü\n  \n6  Aj" 6  6 E\r k  6    Aj! @ ,AH@ (A: A6  A: A: A€z (" (  , AH6 -! -! -! -! -! -! -!\r -! -! B7( A6$ A6 A6 A6 A6 A6  A tAìžAà– r \rAtr Atr Atr Atr Atr Atr Atr6 (  , "AH"! ( ! !\n#A€k" $@ A j"(@A! ("E\rA›    A4jAAüü 6 6 )7 )7 )7 )7$ ( 6, \n60 AA ("Aq6œ A„j"AA€ü A;ã A:® A;à A:ß A: A;¿ A:ª A:¥ A:„ A€„q@ A:‚ A€€q@ A:â A€ q@ A:¨ A€€€q@ A:Á A€€€q@ A:¯ Aq@ A:Ä Aq@ A:¾ A€q@ A:² A€Âq@ A:€ Aq@A!@ A G A GqE@ jA: Ar"AýqA F@  jA: Aj"A€G\r A E\rA  jAk-"A\rF\r A\nF ! A6` A 6D 64 A€€  A€€OAt6X A„jAÿAÔü B7ä B7Ü@AA \n ("E@ AÌj!" A°jAr!# Aüj!$ AèjAr!% A°j!\rAè! A!@@ \r!@@@@ (" K@ B7ø B7ð B7èA! A°jAA \r A°jF"j \r \rF"!\r (¨!& (!@@ j-"A G A Gq\r A|qAj Aj A F! Aj" G\r ! A°jA,A j Aj " 6 A°jA$A j A j " 6A! A°jA A j Aj " A6@ (”"AL\r (œ! (! !\n@@  Alj"-A>F@ \n O\r O\r j-A>G\r Aj"!@ Aj" O\r@ j-"\nA G \nA Gq\r A|qAj Aj \nA F! Aj" G\r !   kAkA  G"\n6  6 !  \n ( "I\r  \n k"\n6 ! Aj" G\r ! ! 6ä@@ O\r j-A\nk  L\r (!@  Alj-A>F\r Aj" G\r ! " #  Aj !A!\nA!A! !@A k!A k! („"\' (üjAk!( (¬E! (!! ( !) (œ! (¤!@@ ("AG@ !   6@  ("M@ !  !@ O\r  ( j-!@  j- AÿqG\r Aj" G\r ! @  k )I\r@  O\r@@  j-A k Aj" G\r ! 6ä \rA6  6ä  F@   (k"A M6 \rA6  (! @@@@ AG\r AL\r  L\rA! A6¤  O\r  j"-A\nk @ Aj  Aäj+AL\r A6¤ AþÿÿÿqAG\r \rA6 A6 ! \rA6  @ AG\r  (”G\r \rA6  (" (œk"A O6  \rA6@@ AL"\r G\r ( AljAk-"A>G6¨  A6¨ @ ! ( AljAk-! AÿqA>F@ !  G@ !  (€\r („"A H\r (ü jAk-AG\r A6¬  @ Aq@ !    AL\r  (” G\r  ! Alj"Ak-A>F\r  G\r  (€\r  \'A H\r  (-AG\r  6A AF\r   A$k(k"A M6 Ak ! A6¬ A6¨@ (" O\r@ AG\r@ -"A-k  G\r@@ Aj" O\r@  j-F@  Aj"K\r  @@ j-A k Aj" G\r ! 6ä \rA6 AA A=F6   \nI\r -"E\rA­ ÀAPE\rA! Aj"\n O\r@@@  \nj-" AÿqF"\r A F\r A G\r j! \nAj"\n G\r !\n AI\r  AI\r A\nk @@@  L\r G\r@ Aj   Aàj Aèj,E\r -è"A>F\r ! Alj"-G\r (ð" ( K\r (ô" k j" !@ (à" O\r@ j-"A G A Gq\r A|qAj Aj A F! Aj" G\r 6ä   k6  6 !  6ä   k" 6  6 O\r@ j-A\nk M@ j"6ô A6  Aj"6ô  Ak6  ! @  O@ (AF@ !  \rA6   k6 A6 ! \n Aj   Aäj Aèj,! (!@@@@@ @ (ä!@ AG\r  G\r@@ O\r j-"A\rF\r -è! A\nG\r AÿqA>F\r  A>! -èA>G\r Aÿq"A.G A)Gq\r (ìAG\r (ô" (ðk j"!@ O\r@ j-"A G A Gq\r A|qAj Aj A F! Aj" G\r ! 6ä   k"6  6  ,è6@@@ O\r j-A\nk Aj6ô   M@  j6ô A6  Aj6ô  Ak6 E@ Aj j-\r (”! @ (˜" J@ (!  A Av j AL"6˜ ( All"E@ (("E\rAõ (0   6 (”! Aè  F! Aj! Aj6”  Alj" )ø7  )ð7  )è7 (”! (! (! ! \n A F  Fq\r (ä! ! O@ !  @ j"\n-"A#G@ ! AþF\r AàF\r !  Ak"\nF\rA!  Aj"j-A#F\r  @@  j- G\r Aj" G\r !  k"AI@ !  6 @@@  O\r@  j-A G\r Aj" G\r   O\r@@  j-"A\nk @ AàG\r \n-AàG\r !  Aj" G\r ! 6ä \rA6 A6 A6 \r \rA 6  \nF\rA!  Aj"j-A#G\r  \nF\rA!  Aj"j-A#G\r  \nF\rA!  Aj"j-A#G\r  \nF\rA!  Aj"j-A#G\r  \nF\rA!  Aj"j-A#G\r  \nF\rA! Aj!   k! ! AJ@ !  @  O"\n\r (Aq\r  j-A k"AK@ !  A tA“€€q\r !  @ \n\r@  j-"A G A Gq\r Aj" G\r !  6 6ä \rA6  6 @@@@@  O\r  j-AF\r Aj"\n O\r A/G\r \n j-A>F\r  AjM@A!  A! AA   A j>E\rA! ( "\n O\r (!@@  \nj-"A k"AK\rA tA€€qE\r \nAj"\n G\r  AAA A\nF A\rF! Aj$ 6¤@  (AG\r A6¤ ("A€qE\r (AG\r  (O\r ( j-"A-k Aj  Aäj+ F@ A6¤ A6 \rA6 AüG\r  (”G\r (€"E\r (AG\r Aäj! Aàj!A!A!@ Aj"(" M\r (" j-AüG\rA! Aj" O\r@  j-A k"\nAK\rA \ntA€€qE\r Aj" G\r ! @@@@  I@  ( j-A:Fj!  O\r ("\n j-A-G\r@@@  \nj-"A-G\r Aj" G\r Aj!  A! Aj!@  A:Fj" O\r@@  \nj-"A k"AK\rA tA€€qE\r  Aj"G\r  AüG\rA!A! Aj" O\r@  \nj-A k"AK\rA tA€€qE\r Aj" I\r !   O\r@  \nj-A\nk E\r  ! E\r  6  6A  A E\r  (à6 \rA\n6 \rA6 (”"  F  (AF! A€qE\r jAL\r ( Alj"Ak-"E\r A?K\rB ­†B€€€€ÀƒP\r@ (ä" ("\n Aj"  \nK"O\r (!@ j-"A G A Gq\r Aj" G\r ! Aj" \nO\r (" j-AÛG\r  Aj"j,"E\rAÓ APE\r  j-AÝG\r \n Aj"G@ j-A k"AK\r A tA“€€qE\r Ak % " -Ar: Ak $  6@ ("O\r (!@ j-A k"AK\rA tA€€qE\r Aj" G\r ! 6ä  6  Aj"6ô  6  6Aè!A! ! ! Aj.A! Aj A@k/\r -Aq@ Aj AÜj/\r AjA-\r Aj0\rA!#Ak" $ A6$ Aj"\rA6@ \r(€AJ@ Aøj!@@@@@@ \r(ø j" ("AÿqAk AsA\nvAq6 /:  (6 (AsA\nvAq6 /:  AÿÿK6 /: (6  A6 /At(€\\"6 /! 6 At(€6 Aj6 A$j6 @@ ("A€q@@ A€qE\r Aÿq A j \r(, \r("@ \r($"E\rA¿ \r(,   ("Aÿq"AK\rA tAŽ€qE\r \r \r(Ak6 (! A€qE\r Aÿq A j \r(, \r("@ \r($"E\rAš \r(,   @ ("AÿqAk \r(Œ \r(Alj" -Ar: \r \r(Aj6  B7` B7X B7P B7H B7@ B78 B70 B7(@ ("AÿqA\nG\r A€€„I\r ("A€ Av"nM\r Aq! Aj!A!\nA!@ "AO@ A|q!A!@  Atj"( j ( j (j (j ( ( ( (jjjk! Aj! Aj" G\r E\r @  Atj"( j (k! Aj! \nAj"\n G\r  n AvO\r \r($"A¸ \r(,  (  A€~qA r"6 A! A6ˆ B7€ B7x B7p B7h \r("@ \r(Œ AljAk-As! A!@@@@@@@ AÿqAk Av6h  A€€I\r \r("\n ("j-! ( !@@@  \r("O\r@  \nj- AÿqG\r Aj" G\r  M\r@  \nj-A G\r Aj" G\r ! @  M\r@  \njAk-A G\r Ak" K\r ! A! \r  \nj  kA Aèj AÈj9\r \r(! !\n@  M\r@  \nj-A k"AMAA tA€€q\r \nAj"\n G\r !\n \r  j \n kA  A(j9\r :ˆ (!A!  A6l Av6h (Ak6p  Av6h @@@@@@@  Aÿq"A FqE@  Aèj \r(, \r("\r (! AÿqAk \r($"E\rAš \r(,   (!@ AÿÿM@ E\r Aj!@ ( (F@ A j! Ak"\r  @  A lj"A k( Ak(G\r Ak"\r  Ak"E\r Aj! \rA  :"E\r  \rA Aj (:"E\r  (!@@ Av"Atj"E@A! \r($"E\rAä \r(,  k  @ (! ("! ! !@ !@ M\r \r(!@  j-A-F\r Aj" G\r ! A!  K@ \r( jAk-A:F! @ M\r \r(!@@  j-"\nA-G\r Aj" G\r !  Ar  \nA:F!  At( 96 Ak! Aj! AJ\r A A \r(, \r("@ \r($"E\rAš \r(,  k  \rA ( (  ;"\rA A \r(, \r("@ \r($"E\rA¿ \r(,  k  AI\r@A A \r(, \r("E@ Aj!A!  \r($"E\rAš \r(,  k  @ \rA  Atj"( (  ;"\r Aj" G\r A A \r(, \r("E\r \r($"E\rA¿ \r(,  k  k  \r Aj (<"\r A!  -"A Fq\r  Aèj \r(, \r("E\r \r($"@A¿ \r(,  ! \r (X@ (Hk (Lk (Pk (8E\r ((k (,k (0k \r (! -AkAM@ A l j!  At j!  \r(Œ \r(Alj" -Aþq A\nvAqr: \r \r(Aj6 Aj" \r(€H\r \rA6€A! Aj$ "\r -Aq@A!A!#A@j"\n$@ Aj"(`"E\r (pE\r (X AAR@@AA (, ("E@ (`E\r \nA0j!  ($"E\rAš (,   @ (X Alj"( E\r \nB7 \nB7 \nB7 \nB7 B7 B7 \n ( 6( \n (6,@ ( (A  \nAj9@A!  A \nA(j (, ("@ ($"E\rAš (,   ( (<"\rA \nA(j (, ("E@A!  ($"E\rA¿ (,  \n(@ \n(k \n( k \n(k \r Aj" (`I\r A!AA (, ("E\r ($"@A¿ (,  ! \nA@k$ "\r A!AA (0 ("E\r (("@A¿ (0  !  \n6ä \rA6 !  ! !  A6¨ ! @ (ä"Aj" ("O\r (!@@ j"-A\nk @ -A\nk @ -A\nk @  j-A\nk Aj! Aj!  I\r @ I@ (!@A!@ j-A\nk Aj" G\r ! A! A°jA(A j Aj "\n 6 " \r("AG\r@@ ("M\r (!@  jAk-"A G A Gq\r Ak" K\r  @  O\r (!@  jAk-A#G\r Ak" K\r   F\r ( jAk-"A F@ !  A F@ !  ! -AqE\r \n 6  !@ AkAI\r  ("M\r (!@  jAk-"A G A Gq\r \n Ak"6  K\r @ ( j-A\rFj! I@ ( j-A\nFj! @ &E\r E\r  j"AL\r ( Alj"Ak-A>F\r (ü Ak(j" (A€r6 @ \r  j" (”N\r Aj -\r AJ@ AjA Al" (j"(" -Aq ( j,A A1\r AjA (ü" -éAq" ( j,A A1\r ( j" -Aýq r: ( j 6 @ (”!@ -AqE\r ( Alj"Ak-A>G\r \r(AG\r \n(" ("k"AkA K\r (" j"-AÛG\r -A!G\r  jAk-AÝG\r Ak! Aj!A!@@@@ Ak @  j-"A k  AákAÿqAIAÿq -¹"A k  AákAÿqAIAÿqG\r Aj" G\r A  @  j-"A k  AákAÿqAIAÿq -Î "A k  AákAÿqAIAÿqG@ A\nk Aj" G\r A  A!@  j-"A k  AákAÿqAIAÿq -ú "A k  AákAÿqAIAÿqG@ A\nG\r  Aj" G\r A  A!@  j-"A k  AákAÿqAIAÿq -‡"A k  AákAÿqAIAÿqG@ A\nG\r -"A k  AákAÿqAIAÿqAÃG\r -"A k  AákAÿqAIAÿqAÁG\r -"A k  AákAÿqAIAÿqAÕG\r -"A k  AákAÿqAIAÿqAÔG\r -"A k  AákAÿqAIAÿqAÉG\r -"A k  AákAÿqAIAÿqAÏG\r -"A k  AákAÿqAIAÿqAÎG\rA  Aj" G\r A ! Ak" -Ar: ( (”AljAk" -AÇq r: \rA6 (”!  k!@A!@@A ( Alj"-A)kt"AqE@ A!qE\rA! Aj.  („6 Aj  ( ,A1\r AjA (" -Aq ( j,A A1\r  AjAA -"AqA AvAqA1\r Aj" (”H\r @ \r(E@ Aj.@A!  A!Aè! -AÀqE\r ( \n(I\r (ŒAj6Œ  (Œ@@ („"Aj" (ˆ"J@ A€ Av j AL"6ˆ (ü l"E@A! (("E\rAõ (0   (€"@   (ükj6€ 6ü („"Aj6„  6„ (ü"\rA!   j"A; (Œ! A6  AtAr6 A6Œ @ (E\r Aj.E\rA!  @@@@ \r("Ak\n A! Aj.\r („"Aj" (ˆ"J@ A€ Av j AL"6ˆ (ü l"E@ (("E\r\nAõ (0  \n (€"@   (ükj6€ („"Aj6„ 6ü  j  6„ (ü"E\r  j " \r(AÃÌj-"; (! A6   Atr6 6€ Aj \r2\r Aj.\rA!Aè!  (€A: (€ /; (€" (A€r6A! Aj \r2\r Aj.\rA! (€E@Aè!  \rA6 \r!  (€A\n: (€ /; A 6AA Aj \r2!  @  (F\r Aj.E\rA!  @ (€\r \r! @ Aj"(€"Aj"\n („"J@ A€ Av j AL"6„ (ø l"E@A ($"E\rAõ (, A  (ü"\n@   \n (økj6ü  6ø  (€"\nAj6€  \nj   \n6€ (ø"E\r  j " \r(AÃÌj-"\n; \r(! A6  \n Atr6  6üA  A E\rA!  Aj \r2! A! E\r  A!  (("E\rAš \n  (P"@ (T"A!@@ (P Atj("E\r (@" M@   (D (HljI\r k (T! Aj" I\r (P k (H@A!@ (@ Atj"-"Aq (k -  Aq@ ( k Aj" (HI\r (@k (l"@ (p"A!@@ (l Atj("E\r (\\" M@   (` (dljI\r k (p! Aj" I\r (l k (d@A!@ (\\ Alj(k Aj" (dI\r (\\k (8k (xk (ük (k A€j$ @A<t"B7 Aì6 B7 B7 B7 B7$ B74 B€€€ü7,  6  A j6  (@ (("E\r ((" ($"G A$j!A!\r@@@  \rAt"\nj("(AF@ ($" (("G\r \rAj!\r   k"Au!@@ ((AF\rA! AG@@ Aj!  Atj"((AF@  O\r Ak(!  Atj(! \r \r  "G\r A! A68 B70A!  A! A68 B70 A6L B7D  A0j6@  AÄj6<@@@@@@  At"j"(" (AG\r  Ak(A !  Aj"K@  Atj( rAG\r AF\r @ Aj" O\r -"A k  AákAÿqAIAÿqAÓG\r -"A k  AákAÿqAIAÿqAÃG\r -"A k  AákAÿqAIAÿqAÒG\r -"A k  AákAÿqAIAÿqAÉG\r -"A k  AákAÿqAIAÿqAÐG\r -"A k  AákAÿqAIAÿqAÔG\r  j-A>F\r @ Aj" O\r -"A k  AákAÿqAIAÿqAÓG\r -"A k  AákAÿqAIAÿqAÔG\r -"A k  AákAÿqAIAÿqAÙG\r -"A k  AákAÿqAIAÿqAÌG\r -"A k  AákAÿqAIAÿqAÅG\r  j-A>F\r A\nj" O\r -"A k AákAÿqAIAÿqAÔG\r -"A k AákAÿqAIAÿqAÅG\r -"A k AákAÿqAIAÿqAØG\r -"A k AákAÿqAIAÿqAÔG\r -"A k AákAÿqAIAÿqAÁG\r -"A k AákAÿqAIAÿqAÒG\r -"A k AákAÿqAIAÿqAÅG\r - "A k AákAÿqAIAÿqAÁG\r  j-A>F\r\r Aj" G\r !  6A Aj" O\r Ak! (!@@  j"-A\nk\n\n /A­Ús -A>srE\r Aj! Aj!  I\r !  Aj" O\r Ak! (!@@  j"-A\nk /A¿üF\r Aj! Aj!  I\r !  Aj" O\r Ak! (!@@@  j-"A\nk  A>F\r Aj! Aj!  I\r !  Aj" O\r Ak! (!@@  j"-A\nk /Aݺs -A>srE\r Aj! Aj!  I\r !  @@  O\rA! ( j-A\nk  6 ( !   6A  6A  6A  6A  6A  Aj6A Å@  ("O\r  (˜O\r @ (" j-"E\r A>F@ A>:  6  -Aüq: Aj! Aj  A?K\rB ­†B€€€€€ ƒP\r  Aj"K@  j-A k"AK\rA tA“€€qE\r  :  6  -Aüq: Aj  A! A6  A j"  I"\nO\rA! !@@ j-" A0kAÿqA K\r  A\nljA0k"6 Aj" \nG\r \n! M\r O\r@ j-" A)k  Aj"K@  j-A k"AK\rA tA“€€qE\r  :  6  -Aüq:  k jAj 6  6A!  ç  ("H@@A!@@@A (Œ Alj"Ak"-A)kt"AqE@ A!qE\rA! Ak(!A! Ak-Aq@ ( j,! A! A  A1\r A ,A1\r  AA Ak-"AqA AvAqA1E\rA!  (Ak"6  H\r A Ú#A0k"$@ (ü"E\r@@@ ("AÿqAk A€qE\r (" (O\r ( j-AÛG\r@ ("E\r Aj! AG\r !  @ \r@ A/G\r Aj" O\r \n j-A>F\r AL\r@ AßqAÁkAÿqAI\r AßF\r A:G\r  Aj"M@A!  @@@ \nj,"A0kAÿqA\nI\r A_qAÁkAÿqAI\r E@A!  A  APE\r A!  Aj"G\r  A!  A! @@ Ak A=G\r Aj!A!  A+!@@@@ A"k E\r Aá AP\r A\rF\r A)!  A*! Aj! I\r A! E\r \rF\rA  A)FA !  Aj" Atj"(! (" I\r   M@A A!  Aj6 „#Ak"$@  O\r@@@ j"  k A j4"AÿK@A!A!@@@  jAm"AtA j("\nAu j"AtA j(Aÿÿÿq K@ Ak!   \nAvAq j"AtA j(AÿÿÿqM\r Aj!  L\r  AH\r  A F\r A F\r A kAI\r -A\nk ( j" I\r Aj$  à@@@ AÿK@A­!@@@  jAm"AtAÀj("Au j"AtAÀj("Aÿÿÿq K@ Ak!  AvAq j"AtAÀj(AÿÿÿqM\r Aj!  N\r  AH\rAÐ8!  A6  A r AÁkAI6 A!A4!@@@  jAm"AtAÀ1j("Au j"AtAÀ1j("Aÿÿÿq M@ AvAq j"AtAÀ1j(Aÿÿÿq O@ AH\rAà8!  Aj!  Ak!  N\r A!A!@@@  jAm"AtAÐ6j("Au j"AtAÐ6j("Aÿÿÿq M@ AvAq j"AtAÐ6j(Aÿÿÿq O@ AH\rAð8!  Aj!  Ak!  N\r A6  6  ( ( " lAtj! At"@   ü\n  6 F\r ( Aÿÿÿq"AjF@  sAsAq j6  ( kj6 õ #A k"$ B7 B7 B7 B7 A ?! A ?! @@@  K@   M\r @ \nE@   AjC!A! AqE@   C! A!  Atj( Aj Atj(k"\r Aj" ( " I! Aj" (" I!\n  K\r  I\r  K\r  I\r A! A j$  iA!@ ("(" ("("I\rA!  K\r ( ( ( (A"\rA! (" ("I\r K!  ‰#Ak"$@@@  O\r j  k A j4! ( j! AÿK@A!@@@  jAm"AtA j("\nAu j" AtA j(Aÿÿÿq K@ Ak!   \nAvAq j"AtA j(AÿÿÿqM\r Aj!  L\r  AN\r  A F\r A F\r A kAO\r A6 A 6  ?!   @ Aj$  D ("(" ("("I@A  K@A ( ( ( (A ›”#A0k"$ A6x#Ak"$ AjAA€ü @@ E\r Ak! A€j!  AtjAk!@  Atj"(!@ !@ !@ Aj" ("O@ !  (! @  j"-j-@ !  -j-@ !  -j-@ !   j-j-@ !  Aj! Aj"!  K\r @  K@ (!@  j"-" j-\r Aj" G\r Aj" I\r  @AA@@@@@@ AÜF@A! Aj" (O\r@  j-"A!kAÿqAI\r@ AÿqA\nku Aj I\r Aj! @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"AÜ ; B7  Aj"6  6 A!@@@@@@@@@@@@ A!k\\ \r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\n\r \r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r Aj"\r!\n@  \rM\r@  \nj- G\r \nAj"\n I\r !\n A!A!A!  (F\r N"AÿM\rA!A!@@@@  jAm" AtA j("Au j"AtA j(Aÿÿÿq K@ Ak!   AvAq j"AtA j(AÿÿÿqM\r Aj!  N\r  A! AN\r A!Aä!@@@  jAm" AtA°9j("Au j"AtA°9j(Aÿÿÿq K@ Ak!   AvAq j"AtA°9j(AÿÿÿqM\r Aj!  N\r A! AN   k! !@@  j-" AàG\r Aj" G\r !A  A F ! !  k"A K\r At j( "\r  AtjAk(O\rA!A!@ E@ ! !\n  !\n \r " I\r @@@@ \nO\r@  j-"\rA G@ \rAàF\rA! Aj" \nG\r A! \n  !\r \nO@A! !  @  \rj-AàG"@ ! \r!  \rAj"\r \nG\r ! \n! @  k"\rF@A!\n  Atj"("\r I@  jAk-A F!\n As  Gq\r \r \rF \nrE\r E\r Aj  @ F\rA! \rAK\r \rAt jA j"\r \r("\r \rK6 \r  Aj"M@A!   Atj"\n(! \n(!\n  ( ! \n@ Ak!  Ak(" ("O\r@  j-"A G A Gq\r Aj" G\r ! @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"Aà; B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"Aà; B7  6  6 (t (x"Alj"Ak" Ak6 A(k" Ak6  -Ar:  -Ar:A! ! ! ! (M\r@@@   jAv"Atj"( K@ E\r  AtjAk( I\r Ak!  (K\r ! !  Aj! @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"A¦; B7  Aj"6  6  (x"AL\r (t" AljAk-A&G\r (|" L@ AÀ Av j AL"6|  All"E@A! ($" E\rAõ (,   6t (x! Aj6x  Alj"A»; B7  Aj6  6  ("\n - AÀq\r\n   k"  \n A j>\r (" AjM\r -A!G\r  Aj"j-A-G\r -A-G\r A! (è" K  \nAkOq\r @ Aj" \n \nI"\rM@  j" /A­Ús -A>srE\r\r@@ Aj! Aj" \rK\r " j" /A­Ús -A>sr\r  !  \nI  Aj" Kq@  Atj"(! (!  6è (! A! -AqE\r Aj" O\r  j-AüG\r@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"Aü; B7  Aj"6  6  ("Aj K\r  Ak"\rj"/A÷îs -A÷sr\r @  \rF\r \rN"AÿK@A!A!@@@@  jAm" AtA j("\nAu j"AtA j(Aÿÿÿq K@ Ak!   \nAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r  AN\r A!Aä!@@@  jAm" AtA°9j("\nAu j"AtA°9j(Aÿÿÿq K@ Ak!   \nAvAq j"AtA°9j(AÿÿÿqM\r Aj!  N\r  AH\r\r  A F\r A F\r A\rkA}K\r A!kAI\r A:kAI\r AÛkAI\r AÿG AúKqE\r @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"A®; B7  Aj"6  \r6@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" (6 (! AÄ; B7  6  Aj!@@ (" Aj K\r  Ak"\nj(AèèуG\r  M\r /A¯ÞF\r @ Aj K\r  Ak"\nj"\r(Aèèуs \r-Aósr\r  M\r /A¯ÞF\r Aj K\r  Ak"\nj"/Aæès -Aðsr\r  M\r /A¯ÞG\r @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"Aº; B7  6  \n6@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" (6 (! AÄ; B7  6 Aj!  (Aj K\r @ Ak-"AßqAÁkAÿqAO@ Aj O\r A0kAÿqA\nO\r  Aj O\r\n  Aj"j-"A0kAÿqA\nO@ A_qAÁkAÿqAK\r\n @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"AÀ; B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" (6 (! AÄ; B7  6 Aj!  @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"AÝ; B7  Aj"6  6 \r @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" 6A!A  Aj" O\r  j-AÛG\r@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" 6A!!A !  :  : B7   j"6@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"AÄ; B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"AÄ; B7  6  6 \n @@ E@  -\rA qAvq\r  E\r @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"A:  : B7  Aj"6  6 \n @@@@@@@@@@@AA@@AA@@@@@@@@@@@@@@ >\r"""""""" " """"""""""""""""""" """""""""""""""""""""""""" AÞF\r AþG\r!A! Aj"\n!@  \nM"\r@  j-AþG" \r Aj" I\r !  k"AG\r ( "A€€qE\r \rA! A!  \nj ( \nkA4"AÿK\r A F\r A F\r A kAM\rA  A! Aj" I\r -AqE\r Aj"!@  M\r@@  j-AÞG\r Aj" I\r ! !  kAG\r%A! A!  j ( kA4"AÿM@ A F\r A F\r A kAM\rA  @@@  jAm"AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r A  \nAN\rA  A !  (F\r N"AÿM\rA!A!@@@  jAm" AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r !  \nAN\r !  -A qE\r Aj" O\r@  j" -A=F@  Aj"K\r  !  kAG\r"A!A!AA ( kA4"AÿM@A! A F\r A F\r A kAI  @@@  jAm" AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r A!\n \nAN !  -A€qE\r Aj" O\r@  j" -A+F@  Aj"K\r  !  kAG\r!A!A!AA ( kA4"AÿM@A! A F\r A F\r A kAI  @@@  jAm" AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r A!\n \nAN !  @@@  jAm"AtA j("Au j"\rAtA j(Aÿÿÿq K@ \rAk!   AvAq j"AtA j(AÿÿÿqM\r Aj!  N\r A  \rAN\rA  A !  (F\r\r N"AÿM\r A!A!@@@  jAm" AtA j("Au j"\rAtA j(Aÿÿÿq K@ \rAk!   AvAq j"AtA j(AÿÿÿqM\r Aj!  N\r !  \rAN\r\r !  ! AK\r ( ! ! A€qE\rA!  (M\r N"AÿM\rA!\nA!@@@@  \njAm"\rAtA j(" Au \rj"AtA j(Aÿÿÿq K@ Ak!   AvAq \rj"\nAtA j(AÿÿÿqM\r \nAj!\n  \nN\r  AN\r\n A!\nAä!@@@  \njAm"\rAtA°9j(" Au \rj"AtA°9j(Aÿÿÿq K@ Ak!   AvAq \rj"\nAtA°9j(AÿÿÿqM\r \nAj!\n  \nN\r A! AN  @  j-A$F@  Aj"K\r  A! !  kAK\rA!  (M\r N"AÿM\rA!A! @@@@  jAm"\nAtA j("Au \nj"\rAtA j(Aÿÿÿq K@ \rAk!   AvAq \nj"AtA j(AÿÿÿqM\r Aj!  L\r  \rAN\r A!Aä! @@@  jAm"\nAtA°9j("Au \nj"\rAtA°9j(Aÿÿÿq K@ \rAk!   AvAq \nj"AtA°9j(AÿÿÿqM\r Aj!  L\r A!\r \rAN  @ Aj" O\r@  j-A k"AK\rA tA€€qE\r Aj" I\r ! A F@ !  kAI\r @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\r Aõ (,  Aj6x (t"\rA!  6t (x"Aj6x  Alj"A:  : B7  6  6  @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"A€ ; B7  Aj"6  6  A F\r A F\r A\rkA}K\r AÿG AúKq A!kAI A:kAIr AÛkAIrr ! @ E\rA! A!@  j" ( k" A4"AÿM@ A F\r A F\r A kAO\r  @@@  jAm"AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r  \nAN\r A!Aä!  A4"AÿM@ AÿG AúKq A!kAI A:kAIr AÛkAIrr  @@@  jAm" AtA°9j("\rAu j"\nAtA°9j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA°9j(AÿÿÿqM\r Aj!  N\r A!\n \nAN ! !  Aq "E\r @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" : A$: B7  6  6  A F\r A F\r A\rkA}K\r AÿG AúKq A!kAI A:kAIr AÛkAIrr ! @ E\rA! A!@  j" ( k" A4"AÿM@ A F\r A F\r A kAO\r  @@@  jAm"AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r  \nAN\r A!Aä!  A4"AÿM@ AÿG AúKq A!kAI A:kAIr AÛkAIrr  @@@  jAm" AtA°9j("\rAu j"\nAtA°9j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA°9j(AÿÿÿqM\r Aj!  N\r A!\n \nAN ! !  Aq "E\r @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" : Aþ: B7  6  6  A F\r A F\r A kAM\r !  A! ! AqE\r @@ (x" (|" N@ AÀ Av j AL"6| (t All" \rA! ($" E\rAõ (,   Aj6x (t" \rA!  6t (x"Aj6x Alj" : Aþ: B7  \n6  6  A! !  kAG\r @@  (F\r N"AÿK@A!A!@@@  jAm" AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r !  \nAN\r !  A F\r A F\r A kAM\r !  A! ! AqE\r @@ (x" (|" N@ AÀ Av j AL"6| (t All" \rA! ($" E\rAõ (,   Aj6x (t" \rA!  6t (x"Aj6x Alj" : A+: B7  6  6  A! !  kAG\r @@  (F\r N"AÿK@A!A!@@@  jAm" AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r !  \nAN\r !  A F\r A F\r A kAM\r !  A! ! AqE\r @@ (x" (|" N@ AÀ Av j AL"6| (t All" \rA! ($" E\rAõ (,   Aj6x (t" \rA!  6t (x"Aj6x Alj" : A=: B7  6  6 A F\r A F\r A kAM\r !  A! ! AqE\r @@ (x" (|" N@ AÀ Av j AL"6| (t All" \rA! ($" E\rAõ (,   Aj6x (t" \rA!  6t (x"Aj6x Alj" : AÞ: B7  6  6 @ Aj" O\r -A?G\rA!@ (ì" M@ !  !  \nAkO\r  Aj" \n \nI"\rM@  j/A¿üF\r@@ Aj! Aj" \rK\r " j/A¿üG\r  !  \nI  Aj"Kq  Atj"(! (!  6ì ( ! @ M\r -A!G\r  j-AßqAÁkAÿqAK\r@ Aj" O\r@  j-AßqAÁkAÿqAK\r Aj" G\r ! A! (ð" K  \nAkOq\r Aj" \n \nI"\rM@  j-A>F\r@@ Aj! Aj" \rK\r " j-A>G\r  !  \nI  Aj" Kq  Atj"(! (!  6ð ( ! @ A j" O\r )B¼ÂìšÄ¨ªÁ… 1Bۅ„BR\rA!\r (ô" K  \nAkOq\r@ Aj" \n \nI" M@  j"/Aݺs -A>srE\r@@ Aj! Aj" K\r  "j"/Aݺs -A>sr\r  !  \nI  \rAj"\rKq@  \rAtj"(! (!  6ô ( !\n ! Aj" \nO\r@ Aj" \nO\r  j-" A0kAÿqA\nIA AßqAÁkAÿqAI E\r@  j-"A0kAÿqA\nI\r AßqAÁkAÿqAI\r A+k"AK\r AF\r Aj" \nF\r@  kA K\r  j-" A:F@  \nO\r@  j-" A k"AMAA tA€€q\r A>G@ A I\r AG\r  j-A-F\rAð! Aj !@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\r\rAõ (,  \r Aj6x (t"\rA! 6t (x"Aj6x  Alj" Ar: A<: B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\r\rAõ (,  \r Aj6x (t"\rA! 6t (x"Aj6x  Alj" Ar: A>: B7  6  Ak6 (t (x"Alj"A k Ak6 Ak Ak6 ! !   6 @@@ (x" (|"N@ AÀ Av j AL"6| (t All"E\r 6t (x"Aj6x  Aj6x (t"E\r  Alj"A¼(; B7  6  6@ (x" (|"N@ AÀ Av j AL"6| (t All"E\r 6t (x"Aj6x  Aj6x (t"E\r ( !  Alj"A¾0; B7  6  6 (t (x"Alj"A k Ak6 Ak Ak6A! !  (M\r@@   jAv"Atj"( K@ E@ !   AtjAk(K@ ! Ak!  ( O@ ! \n Aj!  M\r A!  ($"E\rAõ (,  A!  Aj!  A F\r A F\r A\rkA}K\r AÿG AúKq A!kAI A:kAIr AÛkAIrr "! As AßFq! @  \nF\rA!A!@  \nj" ( \nk"A4"AÿM@ A F\r A F\r A kAO\r  @@@  jAm"AtA j(" Au j"AtA j(Aÿÿÿq K@ Ak!   AvAq j"AtA j(AÿÿÿqM\r Aj!  N\r  AN\r A!Aä!AAA  A4"AÿM@ AÿG AúKq A!kAI A:kAIr AÛkAIrr  @@@  jAm"AtA°9j(" Au j"AtA°9j(Aÿÿÿq K@ Ak!   AvAq j"AtA°9j(AÿÿÿqM\r Aj!  N\r A! AN "Aq As qAq"!A  ! ! \n!  rE\r  kAp!@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" AtA# AG  LqAA  NA AJr" AFrA@k:  : B7  \n6  6 \n \r"M\r@@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"AÄ; B7  \r6  \r6 \rAj"\r \nI\r ! \n! @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x" Aj6x (!  Alj"Aÿ ; B7  6  6A! Aj$@ " \r A (xAÏAJ@ (à"AN@ Ak!\r A€j! AØj! A j!@ (t" Al"j" (! @ -"AÄF\r ( !A! A! AN@   Alj" ( Alj! Al! (! -A q@ A!: Ak"6A!!  j!  I@ ( I\r  I@ ( I\r @  I@ A!G\r  AÛG\r ( A€€ÀqE\r (" (O\r ( j-AÞG\r Aj"6 (t j"(" M\r  ( j  kK"E\r ( "E@ (pAj"6p  6  (Aj6 6 (6 -A”r:  -A˜r: (! (! (  @@@@ -\rA qE\r -AÛG\r E\r (" ("kAG\r -AÛG\r E\r ( ("kAG\r -AÝG\r  AkG\r ( ("kAG\r  (AjG\r@@ Aj" N"@ !  (t! !@@  Alj"\n-"AÄF\r AüF\r \n( kAäK\r \n-AqE\r \n( ! Aj" H\r A!\n \n  \n(" F\r  kAäK\r  I@ (!@@  j-A\nk Aj" G\r A!@   Atj"("L@ (t! @ Alj( " N\r  6  ("L@ (t! @ Alj( " N\r  6 Aj"AG\r \n@@ \n( (I@@  L\r  kAk! (t! A!  AsjAq"@@ AljAÄ; Aj! Aj" G\r AI\r@ Alj"AÄ;L AÄ;8 AÄ;$ AÄ; Aj" G\r \n \n-Ar: \n(6  @ \r  kAk! (t! A!  AsjAq"@@ AljAÄ; Aj! Aj" G\r AI\r@ Alj"AÄ;L AÄ;8 AÄ;$ AÄ; Aj" G\r  \n(6 (6  (6 (t" j" 6 j" 6  -Ar: -Ar: (! (!  Aj L (  (6  (6 (t" j" 6 j" 6  -Ar: -Ar: (! (! (  \r (!  (" ("G\r ( AjK@ -AÀq\r    ( AjM  -AÀq\r   (  AjM "AH\r E\r  (6 (t Alj(!  @  ("O\r (" j-A(G\r Aj!\nA! \r!@@@   jAv"Atj"( K@ E\r  AtjAk( I\r Ak!   (M\r Aj!  O\r A! @@ Aj"  Atj(" I@@  j-A k"AK\rA tA€€qE\r Aj" G\r !  6@  O\r  j-A\nk Aj" O\r  Atj(!  6 @  O\r  j-A)G\rA! A6 B7  6  6 Aj! A    Atj"(" Aj Aj 5E\r@   k ( Aj A j A,j A(j A$j6@ ( " j! (," F!   j"Atj(! (!   ("6(  6$A!A! @  I@@  j-"A k"AK\rA tA€€qE\r Aj" G\r Aj" O\r   Atj("j-! AÿqA)G\r  Aj" 6 ($" (("M@A! A6 B7A   k! @ A6  6   j"6A  @ j"@  Atj!A! !@   Atj("  K" KA!  "kAq"@@  ( j-: Aj! Aj! Aj" G\r  kA|M@@  ( j-:  ( j-:  ( j-:  ( j-: Aj! Aj" G\r   O\r A\n: Aj!  Aj"Atj(! ($" E\rAä (,   A6  6   k6 (! A ! (x" \nL@  6  (t! \n!@  Alj"(K@@ -AqAG\r  ( "Alj( I\r \r k  Aj" H\r  6  \nL\r  kAk! (t! A!  AsjAq"@@ \nAljAÄ; \nAj!\n Aj" G\r AI\r@ \nAlj"AÄ; AÄ;L AÄ;8 AÄ;$ \nAj"\n G\r  -AÀq\r   ( ( AjM"AH\r E\r -Ar:  -Ar: Aj" (tj (6 (t j ( 6  (tj (6( Aj! (t! (@  Alj (Ð6 6Ð  Alj (6 (! (! -!  Aj L   AÛF"!   !   !   ! ! / AŒqE\r@@ "Aj! -$"AÄG" \r  I\r À! !@@ "Ak! \r  K\r -%" AqE\r E\r ( (G\rA‡ APE\r (  (tkAmG\r Ak( (G\r AÄ:$  Aïq:% AkAÄ: Ak" -Aïq: "AN\r B7à A6¸ @A! A6Ô (x"AL\r (t! A!@@ Alj"-"Aq\r -AüG\r ( (kAG\r A6  Ar:@ (ØAH@ 6Ø  (ÜAlj 6 6Ü Aj"6Ô Aj" G\r A!  A! A (xL  A! A0j$ Þ#Aðk"$ B7h B7` B7X B7P B7H B7@ B78 B70 A6( B7 B7 B7 B7A! @    Aj AÐj9\r  A Aj A0j9\r  6( (,! @  Aj (" E@A!  ($"E\rAÒ (,    Aj (" E@A!  ($"E\rAö (,  (`@ (Pk (Tk (Xk (@@ (0k (4k (8k Aðj$ Ý@ ( " ("I@ (!  ( Av jA "At"l"E@A ($"E\rAõ (, A  6 ( Ajl"E@A ($"E\rAõ (, A  6  6 ( ! ( Atj 6  ( Atj 6  ( Aj6 A Ü Aj!@@@ Aj" O\r j-A#G\r j-A rAøG\r !@@@@ j-"A0kAÿqA\nI\r AÁkAÿqAI\r  kAK\r AákAÿqAI\r   kAK\r Aj" G\r !  F\r  kAI\r  @ Aj" O"\r j-A#G\r !@@  kAK\r j-A0kAÿqA K\r Aj" G\r !  F\r  kAI\r   M\r j-AßqAÁkAÿqAK\r  \r@@ j-"AßqAÁkAÿqAO@   kA0K\r A0kAÿqA\nI\r    kA0M\r   Aj" G\r  " kAkA.K\r  M\r j-A;G\rA!  Aj6  ³#Ak"$@@  O\r (!@  j-A k"AK\rA tA€€qE\r Aj" G\r !   O\r (!@  jAk-A k"AK\rA tA€€qE\r Ak" K\r !  6  6  6@  Aj (, ("@ ($"E\rAš (,   AjA<"\r  Aj (, ("E@A!  ($"E\rA¿ (,  Aj$  à!#A0k"\r$  H@ A¸j! A€j! \rAr! \rAr! (!@@ (t" Al" j" -"Aq@ AqE\r@ E\r ,"E\r  [\r ( !  ,"\nAÿq"E\r  \n[E\r (" I\r@@@@@@@@@@@@@@@@@ A!k^  (! Aq@ AN@  Alj" -AÀr: 6  6  AH\r  Alj"( 6¸  6 6@ (ä" AN@  Alj 6  6à 6ä A6  Aj" (xN\r  Alj"-A;G\r (  (" \rHE\r 6  6 Ar:  -Ar: 6  AqE\r \r A€A˜ \nA*Fj" A j6 AÀq"AÀF@A!   \r Aj6 A€F\rA!  Aj6A! A°A´ ( kAFj!\n@ AqE\rA! \n("AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6 6  -Ar: -Ar: \r AqE\r \n(6 \n 6 @ AqE\rA! (Ä"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6 6  -Ar: -Ar: AqE\r (Ä6 6Ä @ AqE\r (¼"AH\rA!  Alj" ( (k ( kG\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r @ Aj" N\r  kAk!A!  AsjAq"\n@@  AljAÄ; Aj! Aj" \nG\r AI\r@  Alj" AÄ;L AÄ;8 AÄ;$ AÄ; Aj" G\r 6 6 -Ar: -Ar: A6¼ AqE\r\n (¼6 6¼ \n  Aj"Al"j"(! (! \r 6 \r ("6, \r 6( \r 6$ \nAÀF@   \rAAAA¨ \rA(jOAL\r\n \r(!  O\r N"AÿM\rA!A!@@@  jAm" AtA j("Au j"AtA j(Aÿÿÿq K@ Ak!   AvAq j"AtA j(AÿÿÿqM\r Aj!  N\r  AN\r  ( kAG\r@ AqE\rA! (À"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6 6  -Ar: -Ar: AqE\r (À6 6À  ( kAG\r@ AqE\rA! (È"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6 6  -Ar: -Ar:  AqE\r (È6 6È  ( kAG\r@ AqE\rA! (Ì"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6 6  -Ar: -Ar:  AqE\r (Ì6 6Ì  A!A!  A F\r A F\r A kAI\r ( jAk,"@Aó AP\r \r((" I\r Ak!@@@ -AÄF\r ( K\r  (O\r -" Aq\r Ak" O\r  AqE\r   \rA,jAA.AAè \rA$jOAH\r \r(,!@ \nAÀF\r@  O\r (" j-A/G\r Aj  \rA,jAA/A‰A \rA$jOAH\r \r(," O\r  j-A/G\r \r Aj"6, @  O\r ( j-A?G\r Aj  \rA,jAA&AƒA¥ \rA$jOAH\r \r(,!  O\r ( j-A#G\r Aj  \rA,jAAAAë \rA$jOAH\r \r(,! @  O\rA!A!@ ( j" ( kA4"AÿM@ A F\r A F\r A kAO\r  @@@  jAm" AtA j("Au j"AtA j(Aÿÿÿq K@ Ak!   AvAq j"AtA j(AÿÿÿqM\r Aj!  N\r  AN\r ,"@A± A P\r \r($"  (xAlj"O\r@@@ -AÄF\r ( M\r ( K\r -"Aq\r Aj" I\r  AqE\r 6 6  \n:  6  6 (t" j" 6  j" 6  -Ar:  -Ar:  \r Atr 6 A q! Aj! AàqAàG@ \r Atj Aj6 Aj! As AGqE@ \r Atj Aj6 Aj! Aq! Aq!A!A!A! A!@@ \r Atj"(("AH\r  Alj! @ ( (M\r ! ! @ (("AH\r  Alj! @ ( (M\r ! ! Aj! Aj" G\r @ E\r \r Atj((" AH\r  Alj! @ ( (M\r ! ! E\r (! (! (! !\n@@@@@@  Alj"-"A!k\n AÛk A€A˜ A*Fj -"AtAuA qj!\n@ AvAk \nAj!\n  A°A´ ( (kAFj!\n  \nAj!\n @  k"  k" K@  (" (k k j"Al"j" (6  )7  )7   k" 6  6 (t j \n(6 \n 6   O\r  Alj j" (6  )7  )7  k j"6  6 A!\n@   \nAtj"("L@@ AN@  (t Alj( "6  N\r \nAj"\nAG\r (t" Alj" 6  j" 6  -Ar:  -Ar:  AqE\r A€A˜ \nA*Fj AtAuA qj!@@@ AvAk Aj!  Aj! (6  6 -"AqE\r Aq@ (t ( Alj(!  (! Aj" H\r \rA0j$ –#Ak"$@ (E\r  3! (  (pAtj("E@A!  @  ("I\r   ( (ljO\r  (F@ ( (  AE\r A!   6  6  6  Aj6 Aj!@ ("@@   Av"Atj"D"AH@   E\r Aj!  Asj "\r A! E@A!  (! Aj$  ý#A k"$ Aß: A¦Ô;A! ( "A€ q@ A$:A! A€€€q@ Aj rA=: Aj! A€€€q@ Aj rA+: Aj! A€€q@ Aj jAü: Aj! A€€q@ Aj jAÞ: Aj! A€„q@ Aj jAþ: Aj! A! Aj jA:    AjAJ ( "AŒq@ Aq@ AÀ:A! Aq@  rA:: Aj! A€q  rA.: Aj  rA:     AjJ A€jAÿAÐü A j$ ‰ (TE@A Ak! Ak!AA ("\n j-A!F j!A!@@   jAv"Atj" ( K@ E\r  AtjAk( I\r Ak!   (M\r Aj!  @  (M" E@@  kj"@ !@  Atj("  K" KA!  "kAq"\n@@  ( j-: Aj! Aj! Aj" \nG\r  kA|M@@  ( j-:  ( j-:  ( j-:  ( j-: Aj! Aj" G\r   O\r A : Aj! Aj" Atj(! A! ($"E\rAä (, A  k   \nj!  k ! A  Aj60A! Aj !\nA!@@ \n("E\r Aj i"AH\r   kK\r \nAj!\n  j" I\r A=! AH\r A \r  e E@A!  A! (0!\n@ \n(" E\r Aj i" j" K\r Aj a \nAj!\n  K\r A \r  A€Àse \r   \rH!   AHq\r A=! +0 \r   f"AN\r \n -!\n Aj! \r E\rA!@  Atj("\n@  Atj \n cA! Aj"A\nG\r A\nO@A! \n @  Atj(\rA! Aj"A\nG\r A!   :\'A! ! !   k"  H" AÿÿÿÿsJ\rA=! \r  j"  \rH" \nK\r A   e  a A0   A€€se A0  Ae a A   A€Àse ( Ak" O\r B€”ëÜT\r Ak" > @ "I@ Ak"(E\r (¬ k"6¬ ! AJ\r AH@ AjA nAj! AæF!@A A k" A O!\n@  M@AA (!  A€”ëÜ \nv!\rA \ntAs!A! !@  (" \nv j6  q \rl! Aj" I\r AA (! E\r  6 Aj! (¬ \nj"6¬   j" " Atj   kAu J! AH\r A!@  M\r  kAuA l!A\n! ("A\nI\r@ Aj!  A\nl"O\r A AæGk AçF AGqk"  kAuA lA kH@ A„`Aìc AHj A€Èj"A m"\rAtj!\nA\n!  \rA lk"AL@@ A\nl! Aj"AG\r @ \n("  n" lk"\rE \nAj" Fq\r@ AqE@D@C! A€”ëÜG\r \nO\r \nAk-AqE\r D@C! Dà?Dð?Dø?  FDø? \r Av"F \r I!@ \r -A-G\r š! š! \n  \rk"6    a\r \n  j"6 A€”ëÜO@@ \nA6 \nAk"\nK@ Ak" A6 \n \n(Aj"6 Aÿ“ëÜK\r  kAuA l!A\n! ("A\nI\r@ Aj!  A\nl"O\r \nAj"   K! @ " M"E@ Ak"(E\r @ AçG@ Aq!\r  AsA A " J A{Jq"\n j! AA~ \n j! Aq"\r\rAw!@ \r Ak("\nE\rA\n!A! \nA\np\r@ "\rAj! \n A\nl"pE\r \rAs!  kAuA l! A_qAÆF@A!\r  jA k"A AJ"  J!  A!\r  j jA k"A AJ"  J! A!\n AýÿÿÿAþÿÿÿ \rr"J\r AGjAj!@ A_q"AÆF@  AÿÿÿÿsJ\r A AJ!    Au"s k­ d"kAL@@ Ak"A0:  kAH\r Ak" : AkA-A+ AH:  k" AÿÿÿÿsJ\r  j" AÿÿÿÿsJ\r A   j" e  a A0   A€€se@@@ AÆF@ AjA r!  K"! @ 5 d!@  G@  AjM\r@ Ak"A0:  AjK\r   G\r Ak"A0:   ka Aj" M\r @ A‰Aa  M\r AL\r@ 5 d" AjK@@ Ak"A0:  AjK\r A A Na A k! Aj" O\r A J! ! \r  @ AH\r  Aj  K!\n AjA r! !@  5 d"F@ Ak"A0: @  G@  AjM\r@ Ak"A0:  AjK\r  Aa Aj! \rrE\r A‰Aa   k"  Ha k! Aj" \nO\r AN\r A0 AjAAe   ka  ! A0 A jA Ae A   A€Àse    J!\n   AtAuA qj!@ A K\r -!Dð?!@A4 Atk"A€N@Dà! AÿI@ Aÿk!  Dð!Aý  AýOAþk!  AxJ\rD`! A¸pK@ AÉj!  D!Aðh  AðhMA’j!  Aÿj­B4†¿¢! A-F@  š ¡ š!     ¡!  (¬" Au"s k­ d"F@ Ak"A0: (¬! Ar! A q! Ak"\r Aj: AkA-A+ AH: AqE ALq! Aj!@ " ü"AÀÐj- r:  ·¡D0@¢!@ Aj" AjkAG\r Da q\r A.: Aj! Db\r A!\n Aûÿÿÿ   \rk" jkJ\r A  Aj  Ajk" Ak H  " jj" e  a A0   A€€se Aj a A0  kAAe \r a A   A€Àse    J!\n A°j$ \n ¹~  (AjAxq"Aj6 )! )!#A k"$ Bÿÿÿÿÿÿ?ƒ!~ B0ˆBÿÿƒ"§"AøkAýM@ B† B<ˆ„! A€øk­!@ Bÿÿÿÿÿÿÿÿƒ"B€€€€€€€Z@ B|!  B€€€€€€€€R\r Bƒ |! B  BÿÿÿÿÿÿÿV"! ­ |  @  „P\r BÿÿR\r B† B<ˆ„B€€€€€€€„!Bÿ  Aþ‡K@B!Bÿ  A€øAø P"\n" k" AðJ@B!B   B€€€€€€À„ \n!A!\n G@ Aj! ! !@A€ k"AÀq@  A@j­†!B!  E\r  ­"† AÀ k­ˆ„!  †! 7 7 ) )„BR!\n @ AÀq@  A@j­ˆ!B!  E\r AÀ k­†  ­"ˆ„!  ˆ!  7  7 )B† )"B<ˆ„!@ \n­ Bÿÿÿÿÿÿÿÿƒ„"B€€€€€€€Z@ B|!  B€€€€€€€€R\r Bƒ |! B€€€€€€€…  BÿÿÿÿÿÿÿV"! ­ ! A j$ B€€€€€€€€€ƒ B4†„ „¿9 ¦ (T"(! (" ( ("k"  I"@   Q  ( j"6  ( k"6    K"@   Q  ( j"6  ( k6 A: (,"6 6  ¢ E@A @  AÿM\r@AÔÚ((E@ A€qA€¿F\rAèÛA6  AÿM@ A?qA€r: AvAÀr:A  A€@qA€ÀG A€°OqE@ A?qA€r: A vAàr: AvA?qA€r:A  A€€kAÿÿ?M@ A?qA€r: AvAðr: AvA?qA€r: A vA?qA€r:A  AèÛA6 AA  :A Ñ( #Ak"\n$@@@@@@@@@@ AôM@A¤Ü("A A jAøq A I"Av"v"Aq@@ AsAq j"At"AÌÜj" (Ô\\"("F@A¤Ü A~ wq6   6 6 Aj!  Ar6  j" (Ar6 A¬Ü("M\r @@ tA t"A krqh"At"AÌÜj" (Ô\\"("F@A¤Ü A~ wq"6   6  6 Ar6 j"  k"Ar6 j 6 @ AxqAÌÜj!A¸Ü(! A Avt"qE@A¤Ü  r6   ( !  6  6  6  6 Aj!A¸Ü 6A¬Ü 6 A¨Ü(" E\r hAt(Ô^"(Axq k! !@@ ("E@ ("E\r (Axq k"   K"!  ! !  (!  ( "G@ (" 6 6 \n (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6 A! A¿K\r A j"Axq!A¨Ü(" E\rA! AôÿÿM@ A& Avg"kvAq AtkA>j! A k!@@@ At(Ô^"E@A!  A! A AvkA AGt!@@ (Axq k" O\r ! "\rA! !  ("   AvAqj("F ! At! "\r rE@A!A t"A kr q"E\r hAt(Ô^! E\r @ (Axq k" I!   !  ! ("  ( "\r E\r A¬Ü( kO\r (!  ( "G@ (" 6 6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  A¬Ü("M@A¸Ü(!@ k"AO@  j" Ar6 j 6  Ar6   Ar6 j" (Ar6A!A! A¬Ü 6A¸Ü 6 Aj! A°Ü("I@A°Ü  k"6A¼ÜA¼Ü(" j"6  Ar6 Ar6 Aj! A! A/j"Aüß(@A„à(  AˆàB7A€àB€ €€€€7Aüß \nA jApqAتժs6AàA6AàßA6A€ "j"A k"q" M\rAÜß("@AÔß(" j" M\r  I\r @Aàß-AqE@@@@@A¼Ü("@Aäß!@ (" M@   (jI\r ("\r An"AF\r !A€à("Ak" q@  k  jA kqj!  O\rAÜß("@AÔß(" j" M\r I\r n" G\r   k q"n" ( (jF\r ! AF\r A0j M@ !  A„à("  kjA kq"nAF\r  j! !  AG\r AàßAàß(Ar6 n!An! AF\r AF\r M\r k" A(jM\r AÔßAÔß( j"6AØß( I@AØß 6 @A¼Ü("@Aäß!@  (" ("jF\r ("\r  A´Ü("A ME@A´Ü 6 A!Aèß 6Aäß 6AÄÜA6AÈÜAüß(6AðßA6@ At" AÌÜj"6Ô\\  6Ø\\ Aj"A G\r A°Ü A(k"Ax kAq"k"6A¼Ü  j"6  Ar6 jA(6AÀÜAŒà(6   O\r  I\r ( Aq\r  j6A¼Ü Ax kAq"j"6A°ÜA°Ü( j" k"6  Ar6  jA(6AÀÜAŒà(6  A!  A!  A´Ü( K@A´Ü 6  j!Aäß!@@  ("G@ ("\r  - AqE\r Aäß!@@ (" M@   (j"I\r (!  A°Ü A(k"Ax kAq"k"6A¼Ü  j"6  Ar6 jA(6AÀÜAŒà(6  A\' kAqjA/k" AjI"A6 Aìß)7 Aäß)7Aìß Aj6Aèß 6Aäß 6AðßA6 Aj!@ A6 Aj! Aj!  I\r  F\r  (A~q6   k"Ar6  6 AÿM@ AøqAÌÜj!A¤Ü("A Avt"qE@A¤Ü  r6  ( ! 6  6 A !A  A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÔÞj!@@A¨Ü("A t"qE@A¨Ü  r6  6  6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6  6 A! ! !A  (" 6  6  6A!A!A j 6  j 6 A°Ü(" M\rA°Ü k"6A¼ÜA¼Ü(" j"6  Ar6 Ar6 Aj!  AèÛA06A!  6 ( j6 Ax kAqj" Ar6 Ax kAqj"  j"k!@A¼Ü( F@A¼Ü 6A°ÜA°Ü( j"6  Ar6  A¸Ü( F@A¸Ü 6A¬ÜA¬Ü( j"6  Ar6  j 6  ("AqAF@ Axq! ( !@ AÿM@ (" F@A¤ÜA¤Ü(A~ Avwq6  6  6  (!@  G@ (" 6  6  @ (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  A! E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨ÜA¨Ü(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6  j"(!  j!  A~q6  Ar6  j 6 AÿM@ AøqAÌÜj!A¤Ü("A Avt"qE@A¤Ü  r6   ( !  6  6  6  6  A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÔÞj!@@A¨Ü("A t"qE@A¨Ü r6  6  6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6  6  6  6  (" 6  6 A6  6  6 Aj!  @ E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨Ü A~ wq" 6  @  (F@  6   6 E\r 6 ("@ 6  6 ("E\r 6  6 @ AM@   j"Ar6 j" (Ar6   Ar6  j" Ar6  j 6 AÿM@ AøqAÌÜj!A¤Ü("A Avt"qE@A¤Ü  r6  ( ! 6  6  6  6  A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÔÞj!@@ A t"qE@A¨Ü  r6  6  6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6  6  6  6  (" 6  6 A6  6  6 Aj!  @ E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨Ü A~ wq6  @  (F@ 6  6 E\r 6 ("@ 6  6 ("E\r 6  6 @ AM@   j"Ar6 j" (Ar6   Ar6  j" Ar6  j 6 @ AxqAÌÜj!A¸Ü(!A Avt" qE@A¤Ü  r6   ( !  6  6 6 6 A¸Ü 6A¬Ü 6 Aj! \nAj$ ‚ @ E\r Ak" Ak("Axq"j!@ Aq\r AqE\r  ("k"A´Ü(I\r j!@@@A¸Ü( G@ ( ! AÿM@  ("G\rA¤ÜA¤Ü(A~ Avwq6  (!  G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  ("AqAG\rA¬Ü 6  A~q6  Ar6  6  6  6  A! E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨ÜA¨Ü(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6  O\r ("AqE\r@@@@ AqE@A¼Ü( F@A¼Ü 6A°ÜA°Ü( j"6  Ar6 A¸Ü(G\rA¬ÜA6A¸ÜA6 A¸Ü(" F@A¸Ü 6A¬ÜA¬Ü( j"6  Ar6 j 6 Axq j! ( ! AÿM@ (" F@A¤ÜA¤Ü(A~ Avwq6   6  6  (!  G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6   A~q6  Ar6 j 6  A! E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨ÜA¨Ü(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6  Ar6 j 6  G\rA¬Ü 6 AÿM@ AøqAÌÜj!A¤Ü("A Avt"qE@A¤Ü r6   ( !  6 6  6  6 A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÔÞj!@A¨Ü("A t"qE@A¨Ü  r6  6A!A  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6A! !A ! !   (" 6  6A!A!A !  j 6  6 j 6AÄÜAÄÜ(Ak"A 6 ‰  E@ j A@O@AèÛA06A A A jAxq A I! Ak"("Axq!@ AqE@ A€I\r Aj M@ !  kA„à(AtM\r A   j!@  O@  k"AI\r   AqrAr6  j" Ar6  (Ar6  m  A¼Ü( F@A°Ü( j" M\r   AqrAr6  j"  k"Ar6A°Ü 6A¼Ü 6  A¸Ü( F@A¬Ü( j" I\r@  k"AO@   AqrAr6  j" Ar6  j" 6  (A~q6   Aq rAr6  j" (Ar6A!A! A¸Ü 6A¬Ü 6  ("Aq\r Axq j"\n I\r \n k! ( !@ AÿM@ (" F@A¤ÜA¤Ü(A~ Avwq6   6  6  (! @  G@ (" 6  6  @ (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  A! E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨ÜA¨Ü(A~ wq6  @  (F@ 6  6 E\r  6 ("@  6  6 ("E\r  6  6 AM@  Aq \nrAr6  \nj" (Ar6    AqrAr6  j" Ar6  \nj" (Ar6  m !  "@ Aj j"E@A  A|Ax Ak("Aq Axqj"   KQ k  «  j!@@ ("Aq\r AqE\r (" j!@@@ k"A¸Ü(G@ ( ! AÿM@  ("G\rA¤ÜA¤Ü(A~ Avwq6  (! G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  ("AqAG\rA¬Ü 6  A~q6 Ar6  6  6  6  A! E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨ÜA¨Ü(A~ wq6  @ (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6 @@@@ ("AqE@A¼Ü( F@A¼Ü 6A°ÜA°Ü( j"6 Ar6 A¸Ü(G\rA¬ÜA6A¸ÜA6 A¸Ü(" F@A¸Ü 6A¬ÜA¬Ü( j"6 Ar6 j 6 Axq j! ( ! AÿM@ (" F@A¤ÜA¤Ü(A~ Avwq6   6  6  (!  G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6   A~q6 Ar6 j 6  A! E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨ÜA¨Ü(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6 Ar6 j 6 G\rA¬Ü 6 AÿM@ AøqAÌÜj!A¤Ü("A Avt"qE@A¤Ü  r6   ( !  6  6 6 6 A! AÿÿÿM@ A& Avg"kvAq AtrA>s! 6 B7 AtAÔÞj!@@A¨Ü("A t"qE@A¨Ü  r6  6 6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6 6 6 6 (" 6  6 A6 6 6 W~@ ­B|BøÿÿÿƒAØÚ("­|"BÿÿÿÿX@ §"?AtM\r \r AèÛA06A AØÚ 6 ß  #Ak"$  6 @ AÓM@AÐÐAÒ A jp(!  A|O@]  AÒn"AÒl"k6AÒAÐÓ Ajp"( j! AÒkAu!@A! !@@@ ! A/F@AÓ!@ n" I\r  lF\r A\nj"n" I\r  lF\r A j"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r A$j"n" I\r  lF\r A(j"n" I\r  lF\r A*j"n" I\r  lF\r A.j"n" I\r  lF\r A4j"n" I\r  lF\r A:j"n" I\r  lF\r AA AM!@@ j"\rA”à("E\r \n  "E@u ]  \\w ¬@  (AÿÿÿÿqAkA\n , "AH"" (  "kM@ E\r ( AH! @  j  ü\n  j!@ , AH@ 6  Aÿq:  jA:   j k  A  x …Aöÿÿÿ k O@A÷ÿÿÿ! ( , AH! AòÿÿÿM@A  j" At"  K"ArAj A I! t!@ E\r E\r  ü\n @ E\r E\r  j  ü\n   j"k!\n@  F\r \nE\r  j j  j j \nü\n A\nG@ k 6 A€€€€xr6  j \nj"6  jA: u Å@@@ , "AN@A\n! A\nF\r AjAÿq:  (" (AÿÿÿÿqAk"G\r !@ AöÿÿÿG@A÷ÿÿÿ! ( , AH! AòÿÿÿM@A Aj" At"  K"ArAj A I! t!@ E\r E\r   ü\n @  F\r  k"E\r  j  j ü\n A\nG@ k 6 A€€€€xr6  u 6 ! Aj6 (! j"A: : « A÷ÿÿÿI@ (AÿÿÿÿqAkA\n , "AH" I@A ArAj A I"t! (!@ (  "Aj"E\r E\r   AH ü\n AH@ k 6 6 A€€€€xr6  u h (Aÿÿÿÿq" K@ 6 (!@ E\r E\r   ü\n  jA: Ak  kAj ("A   x Q A\nM@ : @ E\r E\r  ü\n jA: A\n A\nk - Aÿq"A  x #A k"$#Ak"$ Aj" A j" Aj"k"A L@A= A ArgkAÑ lA v"  At(°TIkAjH\r  A¿„=M@ AÎM@ AãM@ A M@  A0r: Aj   At/àT; Aj  AçM@  AÿÿqAän"A0r:   AälkAÿÿqAt/àT; Aj   ~  AŸM@  AÎn"A0j: Aj  AÎlk~     AÿÁ×/M@ Aÿ¬âM@  AÀ„=n"A0j: Aj  AÀ„=lk   €  Aÿ“ëÜM@  A€Â×/n"A0j: Aj  A€Â×/lk€   A€Â×/n"At/àT; Aj  A€Â×/lk€ !A 6  6 A j" (6  ( 6 Aj$@ ( " Aj"k"A÷ÿÿÿI@@ A\nM@ :  Ar"Ajt! Aÿÿÿÿk6 6 6 !  k!@  F\r E\r  ü\n jA:  u A j$ 2 Aän"At/àT;  AälkAt/àT; Aj * AÎn"At/àT; Aj  AÎlk~ * AÀ„=n"At/àT; Aj  AÀ„=lk  AÐjjAÐj  t E@ ( (F F@A ("-!@ ("-"E\r G\r@ -! -"E\r Aj! Aj! F\r F Ú#AÐk"$@A ( (F\r#A@j"$  ("Ak("j!@ Ak("(AÜÖ(F@A  !   "L@ B7 A6 AØÖ6  6  6 B7 B7$ B7, A6< B€€€€€€€74  Aj  AA (( (\r B7 A6 A¨Ö6  6 AØÖ6 B7 B7$ B7, B73 A6< A:;  Aj AA (( A!@@ (( (A ($AFA ( AFA (,AF!  (AG@ (,\r ( AG\r ($AG\r (! A@k$A "E\r ("E\r AjAA8ü A:K A6  6  6 A6D  Aj A (( (,"AF@  ($6 AF ! AÐj$  A6 Aç6 AŽ 6] v ($"E@ 6 6 A6$ (86 @@ ( (8G\r ( G\r (AG\r 6 A:6 A6 Aj6$  ( ((F@   … 5 ( ((F@   … ("    (( { ("Aq! -7AF@ Au" E\r ( j(  Au E\r  ((68A!A ! ("   j A Aq (( r ( ((F@   … ( ! Aj"   ˆ@ AI\r  Atj! Aj!@   ˆ -6\r Aj" I\r š A:5@  (G\r A:4@ ("E@ A6$ 6 6 AG\r (0AF\r   F@ ("AF@ 6 ! (0AG\r AF\r  ($Aj6$ A:6 Â@ ( ƒ@  (G\r (AF\r  6 @ ( ƒ@@ ( G@  (G\r AG\r A6   6 (,AF\r Aj" ( Atj!@@@ @  M\r A;4    A Œ -6\r -5AG\r -4AF@ (AF\rA!A! -AqE\r  A! -Aq\rA  AA Aq 6, \r  A6,  Aj! ( ! Aj"     AI\r  Atj! Aj!@ ("AqE@ ($AG\r @ -6\r      Aj" I\r  AqE@@ -6\r ($AF\r      Aj" I\r  @ -6\r ($AF@ (AF\r      Aj" I\r   6  ((Aj6( ($AG\r (AG\r A:6 K ("Au! ("   Aq ( j(  j A Aq  (( I ("Au! ("  Aq ( j(  j A Aq  (( Š@ ( ƒ@  (G\r (AF\r  6 ( ƒ@@ ( G@  (G\r AG\r A6   6 @ (,AF\r A;4 ("   A  (( -5AF@ A6, -4E\r  A6,  6  ((Aj6( ($AG\r (AG\r A:6 ("     (( ¦@ ( ƒ@  (G\r (AF\r  6 ( ƒE\r@ ( G@  (G\r AG\r A6   6  6  ((Aj6(@ ($AG\r (AG\r A:6 A6, › ( ƒ@    Š -5! ( ! A:5 -4! A:4 Aj"     Œ  -4"\nr!  -5" r!@ AI\r Atj! Aj!@ -6\r@ \nAq@ (AF\r -Aq\r  AqE\r -AqE\r A;4      Œ -5" rAq! -4"\n rAq! Aj" I\r  Aq:5  Aq:4 9 ( ƒ@    Š ("      ((  ( ƒ@    Š A§ 7 AàÙ6 ("Ak" (Ak"6 AH@ A kk ” k  (  $ # kApq"$ # ‘P(A€ Þ{"type":"Document"},"attributes":{summarytbodyTableBodyLatexMathDisplay\\u%04x-+ 0X0x-0X+0X 0X-0x+0x 0xTableRowdivnavmenuTextUnorderedListOrderedListstartSuperscriptSubscripttfootcountbodyRowCountheadRowCountbasefontDocumentimportantdefaulttightrightHighlightleftframesetfieldsetdt\\taddressdetailsEmphasisnoframes%s:%d: %strvectordirhrfootercenterSpoilerheaderfenceChar\\roptgroupcolgroup/Users/ernest/react-native-enriched-markdown/packages/core/cpp/wasm/ASTSerializer.cpp/emsdk/emscripten/system/lib/libcxxabi/src/private_typeinfo.cpptipcautionoptionfigcaptionAdmonitionsectionmainalignnan\\nformmenuitemListItemparamulurlcolhtmlTableHeaderCellTableCellleveldlisTasklinkLinkCodeBlocktrackSoftBreakLineBreakThematicBreaklibad_array_new_lengthParagraphStrikethroughsearchdialogStrongbasic_stringnodeTypeToStringwarningHeadinginf\\ftrueblockquoteBlockquotenotefalsebasefigureadmonitionTypeUnderlineLatexMathInlineBlankLineiframetitlearticletableTableImageCodeasidetdlegendtaskCheckedddtheadTableHead\\b"\'=<>`-_.-+_\\\\({[,"children":[NANINFcatching a class without an object?)}].!?,;mailto:,"content":h6h5h4h3h2h1http://Too many link reference definition instantiations.Unsupported abi_version.Aborted from text() callback.Aborted from enter_span() callback.Aborted from leave_span() callback.Aborted from enter_block() callback.Aborted from leave_block() callback.malloc() failed.realloc() failed.@:..!#$%&\'*+/=?^_`{|}~-_.:-+-.-_+-_*(null)Suppressing too sparse table (see https://github.com/mity/md4c/issues/345)._=().+-_~%false && "unhandled NodeType in nodeTypeToString"\\"{"type":"[]!xX \nñ<‡í bOFß*$®ŒšŸòT2q¾Îé<Ь¸ùlAè ~€ ¸+Œ ô)NSt3__220__shared_ptr_emplaceIN8Markdown15MarkdownASTNodeENS_9allocatorIS2_EEEE"ƒA€ ¹Nú‡RA  f ° à \rˆ P\rˆ \rð\rˆ ˆ  @`€ˆ ˆ  Àˆ ˆ ˆ ˆ ˆ A ?/A° Äè£\n-Aà dƒµ¯A 5KG[uzÞ9AÐ 50a\nÉ|‘\'A MÝÚ×ÔÑÎO’y¹Að A€ 8$èA  ‚–AÀ \r~XAà ¶¦ZA€ \rP¨A  Tw$AÀ E,5Â9N\nküA ®A  —  € @\n €/ _ 0A@Z€µÀ@րØ@ހ@.€2@6€9@G€J@v€xy@}€‚„†‡‰Š‹Ž‘“”–—˜œŸ @¤€¦§©¬®¯±²³µ·¸¼ÄÅÇÈÊË@Û€Þ@î€ñòôö÷ø@€ "@2€:;=>ACDEF@N€Eprv†ˆ@Š€ŒŽ‘@¡€£@«€ÂÏÐÑÕÖØ@î€ðñôõ÷ùúý@ÿ€@€@/€`@€€Š@¾€ÀÁ@Í€Ð@.€1@V€ @Å€ÇÍø@ý€€‚ƒ„…†‡ˆ‰@º€½@¿€@”€› @þ€@€@€(@/€8@?€H@M€Y[]_h@o€¸¹º»¾È@Ë€ØÙÚÛèéêëìøùúû&!*!+!2!`!@o!€ƒ!¶$@Ï$€,@/,€`,b,c,d,g,@k,€m,n,o,p,r,u,~,,€,@â,€ë,í,ò,@¦@l¦€€¦@š¦€"§@.§€2§@n§€y§{§}§~§@†§€‹§§§’§–§@¨§€ª§«§¬§­§®§°§±§²§³§´§@§€Ä§Å§Æ§Ç§É§Ë§Ì§@Ú§€Ü§Ý§â§õ§l«m«p«@¿«€!ÿ@:ÿ€@\'€°@Ó€p@z€|@Š€Œ@’€”•€ @² €P\r@e\r€ @¿€@n@_n€ n@¸n€@ßHßJßMßQßhß@n߀rß@~߀é@!é€az¼àöøþ/37:HKwÿz~sSƒ…TˆVWŒÝY[’`cih™oru¡¥€¨ƒ­ˆ°Š‹´¶’¹½ÆÆÉÉÌÌÜßïóóõ•¿ùž#3e,<šf,B€‰ŒGO¹qswó¬­¯ÌÍαÁÃËÃײ¸ÆÀÙïºÁ¸µøòû{}P_0Oa‹¿ÏÂÎÑ/a†-%-\'---ðõ24>ABBJcK¦ŠÐúýÿ•a¡ÿ \'07@EQSUW`g°±pq¹ruÐÑvwàáz{åxy|}ÉkåN!p!!„!Ð$é$0,_,a,k}}h,l,QqPRs,v,?@,ã,ì,î,ó,A¦m¦¦›¦#§/§3§o§z§|§y§‡§Œ§e‘§“§—§©§f\\aljž‡S«µ§Ã§”§‚Žȧʧdͧۧ›w|ö§K«L« ïAÿZÿ(OØû—¡£±³¹»¼À ò p\r…\rÀß`nn»nÓnAßIßKßNßRßißoßsßß"éCéAÀ1 Óß0Ið‡–—˜™šžP€@‡€ˆ@€@—€˜@Ÿ€ @§€¨@¯€²³´¶¼ÂÃÄÆÌÖäæòóôöüûûûûûûûûûû•ßA 3 ¥ssi¼nj e‚h1tw\ny\na¾ssʹ¹¹ ¹\'¹ ¹\'¹`¹g¹`¹g¹p¹±¹¬¹±B±¹t¹·¹®¹·B·¹¹BÁÅB|¹É¹Î¹ÉBɹfffiflststtvtetk~vtmssAÐ6 À°RTV·ÇÒÓ×âãç÷ûû¹ÅÅÅÅB±B¹·B¹¹¹¹BÅÅÅBÉB¹ffiffl@.À 5P A¤9 ¬!@/€:@@€[@`€{@~€¡@©€«@¬€®@±€´¶@¸€»¿×÷Â@Å€Ò@߀å@ë€íï@ÿ€u~„@…€‡ö‚Z@_€‰@Š€@€¾ÀÃÆó@ô€@€@€j@m€ÔÞéý@þ€@\r€ö@ù€þ@ÿ€0@>€^ˆd @e €p ò @ó €ú @û €ý v\nð\n@ñ\n€p ó @ú €w  „ O\ry\rô\r?OZ@[€@€@€468:@=€…¾@Å€Ç@Ì€Î@Ú€J@O€ž@Ÿ€û`@h€@™€m@n€›@œ€ë@í€5@6€Ô@Ö€Ø@Û€@\n€@D@E€Þ@ÿ€@€ @¦€¨@­€N@O€Z@j€t@€ü@ÿ€;@?€~@€À@Ç€Ó½¿@Á€Í@Ï€Ý@߀í@ï€ý@þ€ @\' €0 @^ €z @~ €Š @Ž €  @Ä €!@!€!@!€!@ !€!!@!€!@#!€%!\'!)!.!:!@;!€@!@D!€J!@M!€O!Š!@‹!€!@)$€@$@J$€œ$@é$€%@u\'€”\'@s+€v+@ÿ+€å,@ê,€ù,@ü,€þ,@ÿ,€p-.@..€0.@].€`.@c.€€.@™.€›.@ó.€/@Õ/€ð/@ÿ/€0@0€0@ 0€0060@70€=0@?0€›0@œ0€ 0û01@‘1€–1@Ÿ1€À1@å1€ï12@2€*2@G2€P2`2@2€Š2@°2€À2@ÿ3€ÀM@ÿM€¤@Ƥ€þ¤@ÿ¤€\r¦@¦€s¦~¦ò¦@÷¦€§@§€ §@!§€‰§@Ч€(¨@+¨€6¨@9¨€t¨@w¨€Î¨@Ϩ€ø¨@ú¨€ü¨.©@/©€_©Á©@Í©€Þ©@ß©€\\ª@_ª€wª@yª€Þª@ߪ€ðª@ñª€[«j«@k«€ë«)û²û@Òû€>ý@Oý€ý@‘ý€Èý@Ïý€üý@ÿý€þ@þ€0þ@Rþ€Tþ@fþ€hþ@kþ€ÿ@ÿ€ÿ@ ÿ€;ÿ@@ÿ€[ÿ@eÿ€àÿ@æÿ€èÿ@îÿ€üÿ@ýÿ€@€7@?€y@‰€Œ@Ž€@œ€ Ð@ü€ŸÐoWw@x€ ? P\n@X\n€\nÈ\nð\n@ö\n€9 @? €™ @œ €n\rŽ\r@\r€­É@Ê€Ð@Ø€U@Y€†@‰€G@M€»@¼€¾@Á€@@C€t@u€Å@È€ÍÛÝ@߀8@=€©Ô@Õ€×@Ø€K@O€Z@[€]ÆÁ@×€A@C€`@l€¹<@?€;D@F€â?@F€š@œ€ž@¢€@ €áA@E€p@q€÷@ø€C@O€Õ@ñ€ÿp$@t$€ñ/@ò/€nj@oj€õj7k@?k€Dk@Ek€mm@om€—n@šn€âoœ¼Ÿ¼Ì@ïÌ€úÌ@üÌ€Í@³Î€ºÎ@Ð΀ÒÎ@Ô΀ÝÎ@ý΀PÏ@ÃÏ€Ð@õЀÑ@&Ñ€)Ñ@dÑ€jÑ@lÑ€ƒÑ@„Ñ€ŒÑ@©Ñ€®Ñ@AÒ€EÒSÒ@ZÒ€]Ò@^Ò€`Ò@Ò€Ó@VÓ€ÁÖÛÖûÖ×5×O×o׉ש×Ãר@ÿÙ€7Ú@:Ú€mÚ@tÚ€vÚ@ƒÚ€…Ú@‹Ú€Û@Û€Oáÿâÿå^é@_逬ì°ì.íðî@ñî€ð@+ð€0ð@“ð€ ð@®ð€±ð@¿ð€Áð@Ïð€Ñð@õð€\rñ@®ñ€æñ@ò€ò@;ò€@ò@Hò€Pò@Qò€`ò@eò€ó@Ùö€Üö@ìö€ðö@üö€÷@Û÷€à÷@ë÷€ð÷@ ø€ø@Gø€Pø@Yø€`ø@‡ø€ø@­ø€°ø@»ø€Àø@Áø€Ðø@Øø€ù@Wú€`ú@mú€pú@|ú€€ú@Æú€ÈúÌú@Ýú€ßú@ëú€ïú@úú€û@’û€”û@ïû€úû AãÌ  AðÌ A  \n\n\n   AÁÍ ! \r\r  AûÍ  A‡Î  AµÎ AÁÎ  AïÎ AûÎ  A²Ï  AãÏ AïÏ  AÐ A©Ð € 0123456789ABCDEF \r%)+/5;=CGIOSYaegkmqƒ‰‹•—£§­³µ¿ÁÅÇÓ \r%)+/5;=CGIOSYaegkmqyƒ‰‹•—£§©­³µ»¿ÁÅÇѐ+Ø)NSt3__214__shared_countE, *Ð)NSt3__219__shared_weak_countEA´Ô œ\ndè\' †@B€–˜áõʚ;00010203040506070809101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899¸+4+<-N10__cxxabiv116__shim_type_infoE¸+d+(+N10__cxxabiv117__class_type_infoEX+ !"#$%&Ø+\'!"#()*¸+ä+X+N10__cxxabiv120__si_class_type_infoE4,+!"#,-.¸+@,X+N10__cxxabiv121__vmi_class_type_infoE°,/0+„,St9exception¸+ ,|,St9bad_alloc¸+¼,”,St20bad_array_new_lengthì,12¸+ø,|,St11logic_error-32¸+(-ì,St12length_error+D-St9type_infoAÔÚ  . 0€'); ++ return binaryDecode('asm«````````````` `````||`~`|``Tenv __cxa_throwenv\r__assert_failenvemscripten_resize_heapenv _abort_js ¾¼   \r\r    \r\n     \n\n\n\np99€€@ Aã€ ¢memory__wasm_call_ctors\rparseMarkdown__indirect_function_table_emscripten_stack_restore½_emscripten_stack_alloc¾emscripten_stack_get_current¿ PA 8º \r{ !"#$%&\'()*+,~-E@G_mno{-cc¦¶³¨-µ²©-´¯¬-¸-¹-·»¼»» (\n¼è¼ AÈÜB7AÐÜA6AßÜA:AÔÜAµ(6AØÜA¹-:AÙÜA:AëÜA:AàÜAŽ/;AâÜA-:AãÜA:A÷ÜA:AìÜAç(6AðÜAë-:AñÜA:AƒÝA :AøÜA¯ )7A€ÝA· -:AÝA:AˆÝB칻֮سå7A“ÝA:AÝA:AŸÝA:A”ÝA¾(6A˜ÝAÂ/;AšÝA:A ÝA"6A¤ÝB‹€€€€‚€€€7 A£(6 Aœ)7 A: A·ÝA:A¬ÝA¼ (6A°ÝAÀ -:A±ÝA:AÃÝA:A¸ÝAÞ (6A¼ÝAâ -:A½ÝA:AÄÝA"6AÈÝBŽ€€€€‚€€€7 A³)7 A­)7 A:AìÞAôÝ6AÄÞA€€€6AÀÞAã€6A¤ÞA*6AÈÞAÀÜ(6 AÓÜ,AH@AÐÜ(AÈÜ(r ó‹!~#A0k"$@ E@AÈÜ„!AÈ܃!@ AO@AÈÜ…"A€A‰#Ak"$AÈ܃AÈÜAŠ A: Aj Aj‹ Aj$  AÈÜ A k A AA€† AÈÜAÈÜ(AÓÜ,AN  A:,  AG:/  AG:.  AG:-  AG:+  AG:*  AG:)  AG:(  AG:\'A4"A60 B7( B7 B7 B7 B7 B7 A j" 6 ! a"AøÿÿÿO\r@@ A O@ Ar"Aj!  Aÿÿÿÿk6  6  6   : A j! E\r E\r  ü\n  jA: Aj! A\'j!#AÐk"$@ A j"( , " AH"E@A<"B7 Aà6 B7 B7 B7 B7$ B74 B€€€ü7,  6  A j6  (! AéOA4 Aèn" A4OA jA !A<"\nB7 \nAà6 \nB7 \nB7 \nB7 \nB7$ \nB74 \nB€€€ü7,  \nA j6 (!  \n6@ E\r  ("\nAk6 \n\r  (( | Aj! ("! ( "\n G@@@ \nAk("E\r  ("Ak6 \r  (( |  \nAk"\nG\r (!  6 @A€  A€O"\n ( kAuM\r \nAt"!\n  k"@ \n  ü\n   \nj6  \n j6  \n6 E\r r !@@@@ ("\n ("I@ \n (6 \n ("6 @  (Aj6 \nAj!\n  \n ("k"\nAu"Aj"A€€€€O\rAÿÿÿÿ  k"Au"   K AøÿÿÿO"A€€€€O\r At"" \nj" (6  ("6  j!    (Aj6 ( ( ("k"\nAu  Atk! \n@   \nü\n  6  Aj"\n6  6 E\r r  \n6    Aj!\n@ ,AH@ (A: A6  A: A: \nA€” A:$@ ,3AH@ ((A: A6,  A:3 A:( (" (  , AH6 -! -! -! -! -! -! -!\n -! -! B7( A6$ A6 A6 A6 A6 A6  A tA̞AÀ– r \nAtr Atr Atr Atr Atr Atr Atr6 (  , "AH"! ( ! !\n#A€k" $@ A j"(@A! ("E\rA‹    A4jAAüü 6 6 )7 )7 )7 )7$ ( 6, \n60 AA ("Aq6œ A„j"AA€ü A;ã A:® A;à A:ß A: A;¿ A:ª A:¥ A:„ A€„q@ A:‚ A€€q@ A:â A€ q@ A:¨ A€€€q@ A:Á A€€€q@ A:¯ Aq@ A:Ä Aq@ A:¾ A€q@ A:² A€Âq@ A:€ Aq@A!@ A G A GqE@ jA: Ar"AýqA F@  jA: Aj"A€G\r A E\rA  jAk-"A\rF\r A\nF ! A6` A 6D 64 A€€  A€€OAt6X A„jAÿAÔü B7ä B7Ü@AA \n ("E@ AÌj!% A°jAr!& Aüj!\' AèjAr!( A°j!AÜ!A!@@ !@@ (" K@ B7ø B7ð B7èA! A°jAA  A°jF""j   F"#! (¨!) (!@@ j-"A G A Gq\r A|qAj Aj A F! Aj" G\r ! A°jA,A "j Aj #" 6 A°jA$A "j A j #" 6A! A°jA A "j Aj #"$A6@ (”"AL\r (œ! (! !\n@@  Alj"-A>F@ \n O\r O\r j-A>G\r Aj"!@ Aj" O\r@ j-"\nA G \nA Gq\r A|qAj Aj \nA F! Aj" G\r !   kAkA  G"\n6  6 !  \n ( "I\r  \n k"\n6 ! Aj" G\r ! ! 6ä@@ O\r j-A\nk  N\r (!@  Alj-A>F\r Aj" G\r ! % & " Aj #!A!\nA!A! !@A k!A k! („" (üjAk! (¬E! (! ( !\r (œ!! (¤!@@ ("AG@ !   6@ ! ("M@ !  @ " O\r  ( j-!@  j- AÿqG\r Aj" G\r ! @  k \rI\r@  O\r@@  j-A k Aj" G\r ! 6ä A6 A6¨  6ä  F@   (k"A M6 A6 !  (! @@@@ AG\r AL\r  N\rA! A6¤  O\r  j" -A\nk @ Aj  Aäj.AL\r A6¤ AþÿÿÿqAG\r A6 A6  A6 !  @ AG\r  (”G\r A6  (" (œk"A O6 A6¨  A6@@ AL"\r  G\r ( AljAk-"A>G6¨  A6¨ \r ( AljAk-! AÿqA>F\r  G\r (€@ !  („"A H@ !  (ü jAk-AG@ !  A6¬ !  @ Aq@ !    AL\r  (” G\r   Alj"Ak-A>F\r   G\r  (€\r  A H\r  -AG\r  6A AF\r   A$k(k"A M6 Ak ! A6¬ A6¨@ (" !O\r@ AG\r@ -"A-k !  G\r@@@ Aj" O\r j- F\r @@ j-A k Aj" G\r ! 6ä A6 AA A=F6 !   \nI\r -"" E\rA ÀATE\rA! Aj"\n O\r@@@  \nj-" F"\r A F\r A G\r j! \nAj"\n G\r !\n AI\r  AI\r A\nk @@@  N\r  G\r@ Aj  Aàj Aèj/E\r -è"A>F\r  Alj"-G\r (ð" ( K\r (ô" k j"!@ (à" O\r@ j-"A G A Gq\r A|qAj Aj A F! Aj" G\r 6ä   k6  6 !  6ä   k"6  6 O\r@ j-A\nk  !M@  j"6ô A6  Aj"6ô  Ak6  ! @ !O@ (AF@ !  A6  !k6 A6 !  Aj  Aäj Aèj/! (!@@ @ (ä!@ AG\r  G\r@@ O\r j-"A\rF\r -è! A\nG\r AÿqA>F\r   A>! -èA>F\r   Aÿq"A.G A)Gq\r (ìAF\r   (ô" (ðk j"!@ O\r@ j-"A G A Gq\r A|qAj Aj A F! Aj" G\r ! 6ä   k"6  6  ,è6@@@ O\r j-A\nk Aj6ô   !M@  j6ô A6  Aj6ô  Ak6 E@ Aj  j0\r (”! @ (˜" J@ (!  A Av j AL"6˜ ( Als"E@ (("E\rAå (0   6 (”! AÜ   F! Aj! Aj6”  Alj" )ø7  )ð7  )è7 (”! (! (! ! A F  Fq\r (ä!  ! O@ !  @ j"\n-"A#G@ ! AþF\r AàF\r !  Ak"\nF\rA  Aj"j-A#G\r  \nF\rA  Aj"j-A#G\r  \nF\rA  Aj"j-A#G\r  \nF\rA  Aj"j-A#G\r  \nF\rA  Aj"j-A#G\r  \nF\rA  Aj"j-A#G\r  \nF\r Aj!A  @@  j- G\r Aj" G\r !  k"AI@ !  6 @@@  M\r@  j-A G\r Aj" G\r   M\r@@  j-"A\nk @ AàG\r \n-AàG\r !  Aj" G\r ! 6ä A6 A6 $A6 \n A 6 ! " k "AJ@ !  @  M"\n\r (Aq\r  j-A k"AK@ !  A tA“€€q\r !  @ \n\r@  j-"A G A Gq\r Aj" G\r !  6 6ä A6  6  @@@@@  O\r  j-AF\r Aj" O\r A/G\r  j-A>F\r AjM@A!  A! \rAA  A jAE\rA! ( " O\r \r(!@@  j-"A k" AK\rA tA€€qE\r Aj" G\r  AAA A\nF A\rF! Aj$ "6¤@  (AG\r A6¤ ("A€qE\r (AG\r  (O\r ( j-"A-k Aj  Aäj. F@ A6¤ $A6 A6 AüG\r  (”G\r (€"E\r (AG\r Aäj!\n Aàj!A!A!\r@ Aj"(" M\r (" j-AüG\rA! Aj" O\r@  j-A k" AK\rA tA€€qE\r Aj" G\r ! @@@@  I@  ( j-A:Fj!  O\r (" j-A-G\r@@@  j-"A-G\r Aj" G\r \rAj!\r  A! \rAj!\r@  A:Fj" O\r@@  j-"A k"AK\rA tA€€qE\r  Aj"G\r  AüG\rA!A! Aj" O\r@  j-A k"AK\rA tA€€qE\r Aj" I\r !   O\r@  j-A\nk E\r  ! E\r \n 6  \r6A  A E\r  (à6 A\n6  A6 (”"   F  (AF! A€qE\r  jAL\r ( Alj"Ak-"E\r A?K\rB ­†B€€€€ÀƒP\r@ (ä" ("\n Aj"  \nK"O\r (!@ j-"A G A Gq\r Aj" G\r ! Aj" \nO\r (" j-AÛG\r  Aj"j,"E\rAà ATE\r  j-AÝG\r \n Aj"G@ j-A k"AK\rA tA“€€qE\r Ak ( " -Ar: Ak \'  6@ ("O\r (!@ j-A k"AK\rA tA€€qE\r Aj" G\r ! 6ä  6  Aj"6ô  6  6AÜ!A! ! ! Aj1A! Aj A@k2\r -Aq@ Aj AÜj2\r AjA0\r Aj3\rA!#Ak"\r$ \rA6$ Aj" A6@ (€AJ@ \rAøj!@@@@@@ (ø j"("AÿqAk \r AsA\nvAq6 \r /:  \r (6 \r (AsA\nvAq6 \r /:  \r AÿÿK6 \r /: \r (6  \rA6 \r /At(€a"6 /! \r 6 \r At(€6 \r \rAj6 \r \rA$j6 @@ ("A€q@@ A€qE\r Aÿq \rA j (, ("@ ($"E\rA¯ (,   ("Aÿq"AK\rA tAŽ€qE\r (Ak6 (! A€qE\r Aÿq \rA j (, ("@ ($"E\rAŠ (,   @ ("AÿqAk (Œ (Alj" -Ar: (Aj6  \rB7` \rB7X \rB7P \rB7H \rB7@ \rB78 \rB70 \rB7(@ ("AÿqA\nG\r A€€„I\r ("A€ Av"nM\r Aq! Aj!A!\nA!@ "AO@ A|q!A!@  Atj"( j ( j (j (j ( ( ( (jjjk! Aj! Aj" G\r E\r @  Atj"( j (k! Aj! \nAj"\n G\r  n AvO\r  ($"A¨ (,  (  A€~qA r"6 A! \rA6ˆ \rB7€ \rB7x \rB7p \rB7h ("@ (Œ AljAk-AqE! A!@@@@@@@ AÿqAk \r Av6h  A€€I\r ("\n ("j-! ( !@@@  ("O\r@  \nj- AÿqG\r Aj" G\r  M\r@  \nj-A G\r Aj" G\r ! @  M\r@  \njAk-A G\r Ak" K\r ! A!  \nj  kA \rAèj \rAÈj<\r (! !\n@  M\r@  \nj-A k"AMAA tA€€q\r \nAj"\n G\r !\n  j \n kA  \rA(j<\r \r :ˆ (!A!  \rA6l \r Av6h \r (Ak6p  \r Av6h @@@@@@@  Aÿq"A FqE@  \rAèj (, ("\r (! AÿqAk ($"E\rAŠ (,   (!@ AÿÿM@ E\r Aj!@ ( (F@ A j! Ak"\r  @  A lj"A k( Ak(G\r Ak"\r  Ak"E\r Aj! A  ="E\r  A Aj (="E\r  (!@@ Av"Atq"E@A! ($"E\rAÔ (,  r  @ (! ("! ! !@ !@ M\r (!@  j-A-F\r Aj" G\r ! A!  I@ ( jAk-A:F! @ M\r (!@@  j-"\nA-G\r Aj" G\r !  Ar  \nA:F!  At(°:6 Ak! Aj! AJ\r A A (, ("@ ($"E\rAŠ (,  r  A ( (  >"\rA A (, ("@ ($"E\rA¯ (,  r  AI\r@A A (, ("E@ Aj!A!  ($"E\rAŠ (,  r  @ A  Atj"( (  >"\r Aj" G\r A A (, ("E\r ($"E\rA¯ (,  r  r  Aj (?"\r A!  (Aÿq"A Fq\r  \rAèj (, ("E\r ($"@A¯ (,  ! \r \r(X@ \r(Hr \r(Lr \r(Pr \r(8E\r \r((r \r(,r \r(0r \r (! -AkAM@ A l j!  At j!  (Œ (Alj" -Aþq A\nvAqr: (Aj6 Aj" (€H\r A6€A! \rAj$ "\r -Aq@A!A!#A@j"$@ Aj"(`"E\r (pE\r (X AAW@@AA (, ("E@ (`E\r A0j!\n  ($"E\rAŠ (,   @ (X Alj"( E\r B7 B7 B7 B7 \nB7 \nB7  ( 6(  (6,@  ( (A \n Aj<@A!  A A(j (, ("@ ($"E\rAŠ (,    ( (?"\rA A(j (, ("E@A!  ($"E\rA¯ (,  (@ (r ( r (r \r Aj" (`I\r A!AA (, ("E\r ($"@A¯ (,  ! A@k$ \r A!AA (0 ("E\r (("@A¯ (0  !  \n6ä A6 ! @ (ä"Aj" ("O\r (!@@ j"-A\nk @ -A\nk @ -A\nk @  j-A\nk Aj! Aj!  I\r @ I@ (!@A!@ j-A\nk Aj" G\r ! A! A°jA(A "j Aj #"\n 6 " ("AG\r@@ ("M\r (!@  jAk-"A G A Gq\r Ak" K\r  @  O\r (!@  jAk-A#G\r Ak" K\r   F\r ( jAk-"A F@ !  A F@ !  ! -AqE\r \n 6  !@ AkAI\r  ("M\r (!@  jAk-"A G A Gq\r \n Ak"6  I\r @ ( j-A\rFj! I@ ( j-A\nFj! @ )E\r E\r  j"AL\r ( Alj"Ak-A>F\r (ü Ak(j" (A€r6 @ \r  j" (”N\r Aj 0\r AJ@ AjA Al" (j"(" -Aq ( j,A A4\r AjA (ü" -éAq" ( j,A A4\r ( j" -Aýq r: ( j 6 @ (”!@ -AqE\r ( Alj"Ak-A>G\r (AG\r \n(" ("k"AkA K\r (" j"-AÛG\r -A!G\r  jAk-AÝG\r Ak! Aj!A!@@@@ Ak @  j-"A k  AákAÿqAIAÿq -–"A k  AákAÿqAIAÿqG\r Aj" G\r A  @  j-"A k  AákAÿqAIAÿq -\r"A k  AákAÿqAIAÿqG@ A\nk Aj" G\r A  A!@  j-"A k  AákAÿqAIAÿq -\n"A k  AákAÿqAIAÿqG@ A\nG\r  Aj" G\r A  A!@  j-"A k  AákAÿqAIAÿq -ä"A k  AákAÿqAIAÿqG@ A\nG\r -"A k  AákAÿqAIAÿqAÃG\r -"A k  AákAÿqAIAÿqAÁG\r -"A k  AákAÿqAIAÿqAÕG\r -"A k  AákAÿqAIAÿqAÔG\r -"A k  AákAÿqAIAÿqAÉG\r -"A k  AákAÿqAIAÿqAÏG\r -"A k  AákAÿqAIAÿqAÎG\rA  Aj" G\r A ! Ak" -Ar: ( (”AljAk" -AÇq r: A6 (”!  k!@A!@@A ( Alj"-A)kt"AqE@ A!qE\rA! Aj1  („6 Aj  ( ,A4\r AjA (" -Aq ( j,A A4\r  AjAA -"AqA AvAqA4\r Aj" (”H\r @ (E@ Aj1@A!  A!AÜ! -AÀqE\r ( \n(I\r (ŒAj6Œ  (Œ@@ („"Aj" (ˆ"J@ A€ Av j AL"6ˆ (ü s"E@A! (("E\rAå (0   (€"@   (ükj6€ 6ü („"Aj6„  6„ (ü"\rA!   j"A; (Œ! A6  AtAr6 A6Œ @ $(E\r Aj1E\rA!  @@@@ ("Ak\n A! Aj1\r („"Aj" (ˆ"J@ A€ Av j AL"6ˆ (ü s"E@ (("E\r\nAå (0  \n (€"@   (ükj6€ („"Aj6„ 6ü  j  6„ (ü"E\r  j " (AtAÐÍj(" /Atr6 (! A6   Atr6 6€ Aj 5\r Aj1\rA!AÜ!  (€A: (€ /; (€" (A€r6A! Aj 5\r Aj1\rA! (€E@AÜ!  A6 !  (€A\n: (€ /; A 6AA Aj 5!  @  (F\r Aj1E\rA!  @ (€\r !@ Aj"\n(€"Aj" \n(„"J@ \nA€ Av j AL"6„ \n(ø s"E@A \n($"E\rAå \n(, A  \n(ü"@ \n   \n(økj6ü \n 6ø \n \n(€"Aj6€  j  \n 6€ \n(ø"E\r  j " (AtAÐÍj(" /Atr6 (! A6   Atr6 \n 6üA  A E\rA!  Aj 5! A! E\r  A!  (("E\rAŠ \n  (P"@ (T"A!@@ (P Atj("E\r (@" M@   (D (HljI\r r (T! Aj" I\r (P r (H@A!@ (@ Atj"-"Aq (r -  Aq@ ( r Aj" (HI\r (@r (l"@ (p"A!@@ (l Atj("E\r (\\" M@   (` (dljI\r r (p! Aj" I\r (l r (d@A!@ (\\ Alj(r Aj" (dI\r (\\r (8r (xr (ür (r A€j$ @A<"B7 Aà6 B7 B7 B7 B7$ B74 B€€€ü7,  6  A j6  (@ (("E\r ((" ($"G A$j!A!@@@  At"j("(AF@ ($" (("G\r Aj!   k"Au!@@ ((AF\rA! AG@@ Aj!  Atj"((AF@  O\r Ak(!  Atj(! \r \r !  G\r A! A68 B70A!  A! A68 B70 A6L B7D  A0j6@  AÄj6<@@@@@@  At"j"("(AG\r  Ak(A !  Aj"K@  Atj( rAG\r AF\r -A/F\r Aj" G\r ! F\r Aj  k” I@@ Aj ((  ,3AH j-bÀš Aj" G\r  (," ,3" AH"O\r Av! Aj!\n@ (  Aq!@  j"-S@ Aj" I\r  -A>F\r -A/F\r@@ K@ !@@  j"-A=F\r -S\r -A>F\r -A/F\r Aj" I\r !  G\r Aj!  A6P B7H AÈj  k”  I@@ AÈj ((  ,3AH j-bÀš Aj" I\r @@  (, ,3" AH""O\r (  !@@  j"-SE\r Aj" G\r  -A=G\r@@@@@@ "Aj"M\r  j"-S\r B78 A6@@ -A"F\r -A\'F\r !@  j" -S\r -A>F\r -A/F\r Aj" G\r !  Aj"!@ M\r -!@  j- F\r Aj" I\r ! k"  k" I"AøÿÿÿO\r@@ A O@ Ar" Aj!  Aÿÿÿÿk64  6,  60   :7 A,j! E\r E\r   j ü\n jA: ,CAH@ (@ (8r  (46@  ),78   (, ,3" AHIj!  A6@ B78 ! M\r k"  k" I"AøÿÿÿO\r@@ A O@ Ar" Aj!  Aÿÿÿÿk64  6,  60   :7 A,j! E\r E\r   j ü\n jA: ,CAH@ (@ (8r  (46@  ),78  AÈj6\\ A,j \n AÈj AÜj@ (,"Aj" A8jF\r ,C! ,AN@ AN@ (@6 )87  (8 (<™  (8 A8j AH" (<  ˜ ,CAN\r (@ (8r   A¤AƁ"AüÛ6 AˆÜA  !  AÈj6, A8j \n AÈj A,j (8",AH@ A6 (  A: Aj A: ,SAH@ (P (Hr (,! -3!   Aÿq ÀAH""I\r A!A!@ ( ," AHAG\r ( Aj AH"(Aöґ«s -Aïsr\r A:S A:K A»/;H A½-:J Aj!\nA! AÈj"( , " AH""! (  " !@ "AI\r Ak"Aq@ !  (A•ÓÇÞl"Av sA•ÓÇÞl A•ÓÇÞls! ! Aj ! AI\r !@ (A•ÓÇÞl"Av sA•ÓÇÞl (A•ÓÇÞl"Av sA•ÓÇÞl A•ÓÇÞlsA•ÓÇÞls! Aj! Ak"AK\r @@@@ Ak -At s! -At s!  -sA•ÓÇÞl! @ \n("E\r \n( A\rv sA•ÓÇÞl"Av s" Akq i"AM\r   K\r  p " Atj("E\r ("E\r Ak! AK!\n@@@  ("G@@ \nE@  q!   K\r  p!  G\r  ( ," AH" G\r ( Aj  U\r !  ("\r A  ! ,SAH@ (P (Hr E\r ( ," AHE\rA<"B7 A!6 Aà6 B7 B7 B7 A6( B74 B€€€ü7, A:S AŽ/;H A-:J A:K  AÈj6, A8j Aj AÈj A,j@ (8" F\r Aj! Aj! ,! ,AN@ AN@  (6  )7   ( (™   (  AH" (  ˜ ,SAH@ (P (Hr A j! Aj ,AH@ ( (r @ ,3AH@ ((A: A6,  A:3 A:( @ E\r ( ( "F\r Ak(!  6  6 A$j Aj@ ( "E\r ("Ak6 \r (( | A!  A! E\r  ("Ak6 \r  (( |  A! ( ( F\r  ( " (kA I\r@ Ak("E\r  ("Ak6 \r  (( |  Ak6 Aàj$ à%#A k"$@@ E@A!  @@@@@@@@@@@@@@@@ Ak\n \r  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6Œ  A j6ˆ  )ˆ7    A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,@ E\r (! A:• AA  AL" ANA0r:” A”ja"AøÿÿÿO\r@@ A O@ Ar"Aj!  Aÿÿÿÿk6€  6ø  6ü   :ƒ Aøj! E\r E\r  A”j ü\n  jA: AÔÜ6˜ A¤j AjAÔÜ A˜j@ (¤"Aj" AøjF\r ,ƒ! ,AN@ AN@  (€6  )ø7   (ø (ü™   (ø Aøj AH" (ü  ˜ ,ƒAN\r (€ (ør  6ô  A j6ð (Aj6  )ð7  Aj ("Ak6 \r (( |  A<"B7 A 6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6ì  A j6è  )è7  Aj \r A<"B7 A 6 Aà6 B7 B7 B7 A6( B74 B€€€ü7, A j!@ E\r ("E\r ("E\r AøÿÿÿO\r@ A O@ Ar"Aj!  Aÿÿÿÿk6€  6ø  6ü   :ƒ Aøj! @   ü\n  jA: (ü -ƒ" ÀAH@ AÄÝ Aøj ,ƒAN\r (€ (ør  6ä  6à (Aj6  )à7  Aj ("Ak6 \r (( | A<"B7 A\r6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6Ü  A j6Ø  )Ø7  A j A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7, A j!@ E\r ("AF\r Aøj œ A¬Ý Aøj ,ƒAN\r (€ (ør  6Ô  6Ð (Aj6  )Ð7(  A(j ("Ak6 \r\n (( | \n A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7, A j!@ E\r (E\r A”Ý AøjAû" , AH@ ( (r A Ý AøjAûA› -AßqAØF" , AN\r ( (r  6Ì  6È (Aj6  )È70  A0j ("Ak6 \r (( | A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7, A j!@ E\r@ - "E\r A:¥  :¤ AøÜ Aøj A¤j" , AN\r ( (r ("E\r ("E\r AøÿÿÿO\r\n@ A O@ Ar"Aj!  Aÿÿÿÿk6€  6ø  6ü   :ƒ Aøj! @   ü\n  jA: (ü -ƒ" ÀAH@ AˆÝ Aøj ,ƒAN\r (€ (ør  6Ä  6À (Aj6  )À78  A8j ("Ak6 \r (( |  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6¼  A j6¸  )¸7@  A@k  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7, A j!@ E\r Aøj (œ A¸Ý6˜ A¤j AjA¸Ý A˜j@ (¤"Aj" AøjF\r ,ƒ! ,AN@ AN@  (€6  )ø7   (ø (ü™   (ø Aøj AH" (ü  ˜ ,ƒAN\r (€ (ør  6´  6° (Aj6  )°7H  AÈj ("Ak6 \r (( |  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7, A j!@ E\r A:€ BãÞ±›ô­·ô7ø A:ƒ A¤j (œ  Aøj6” A˜j Aj" Aøj A”j@ (˜"Aj" A¤jF\r ,¯! ,AN@ AN@  (¬6  )¤7   (¤ (¨™   (¤ A¤j AH" (¨  ˜ ,¯AH@ (¬ (¤r ,ƒAH@ (€ (ør A"6ø BŒ€€€€‚€€€7ü Aù (6 Añ )7 A: A¤j (œ  Aøj6” A˜j  Aøj A”j@ (˜"Aj" A¤jF\r ,¯! ,AN@ AN@  (¬6  )¤7   (¤ (¨™   (¤ A¤j AH" (¨  ˜ ,¯AH@ (¬ (¤r ,ƒAH@ (€ (ør A"6ø BŒ€€€€‚€€€7ü Aì (6 Aä )7 A: A¤j (œ  Aøj6” A˜j  Aøj A”j@ (˜"Aj" A¤jF\r ,¯! ,AN@ AN@  (¬6  )¤7   (¤ (¨™   (¤ A¤j AH" (¨  ˜ ,¯AH@ (¬ (¤r ,ƒAN\r (€ (ør  6   6œ (Aj6  )œ7P  AÐj ("Ak6 \r (( |  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6˜  A j6”  )”7X  AØj  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6  A j6Œ  )Œ7`  Aàj  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6ˆ  A j6„  )„7h  Aèj  A<"B7 Aà6 B7 B7 B7 A6( B74 B€€€ü7, AA AF6 @ E\rAš\n! (Ak"AM@ At(Ð! A:ƒ A:ý Aá\r(6ø Aå\r-:ü a"AøÿÿÿO\r@@ A O@ Ar"Aj!  Aÿÿÿÿk6¬  6¤  6¨   :¯ A¤j! E\r E\r   ü\n jA:  Aøj6” A˜j Aj Aøj A”j@ (˜"Aj" A¤jF\r ,¯! ,AN@ AN@ (¬6 )¤7  (¤ (¨™  (¤ A¤j AH" (¨  ˜ ,¯AH@ (¬ (¤r ,ƒAN\r (€ (ør  6€  A j6|  (Aj6  )|7p  Aðj  ("Ak6 \r  (( |  A:$ ,3AH@ ((A: A6,  A:3 A:( A j$    #Ak"$@ ( ," AHE\r ( " (F\rA<"B74 B€€€ü7, A6 Aà6 B7 B7 B7$  Aj")7  (6 A: A: Ak(!  6  A j6 A$j Aj@ ( "E\r  ("Ak6 \r  (( | ,AH@ (A: A6  A: A: Aj$ ª~A!@@@ (Ak ( ," AH""E\r  ( Aj "j! !@@ 1"B?V\rB †B€Ì€€ƒP\r Aj" G\r  kAF A!  ¹  #Ak"$ ("(" ("kAu!@  F\r@  Atj(E\r Aj" G\r ! @@ " M\r  Ak"Atj(\r A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6  A j6 A0j!  At"j!  At"j!@  kAu" (" ("kAuM@ (" k"Au I@  j!  G@@ (! ("@  (Aj6  6 (!  6@ E\r  ("Ak6 \r  (( | Aj! Aj" G\r (! !  G@@  (6  ("6 @  (Aj6 Aj! Aj" G\r    kj6   G@@ (! ("@  (Aj6  6 (!  6@ E\r  ("Ak6 \r  (( | Aj! Aj" G\r (!  G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r  6  @ ! (" G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r ( (!  6 r A6 B7A! @ A€€€€O\rAÿÿÿÿ Au"  K AøÿÿÿO"A€€€€O\r  At""6  6   j6  G@@  (6  ("6 @  (Aj6 Aj! Aj" G\r  6   ( Aj@ ( "E\r  ("Ak6 \r  (( | ("(! (!  G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r  6 Aj$ A„  )A¤€"A€Ú6 A”Ú6 AèÚA  ~ AL@ (" ("kAu N@  k"Au"\n H@@   j" F@ !  ! !@  (6  ("6 @  (Aj6 Aj! Aj" G\r 6 \nAL@  At"j! !   k"K@ !@  (6  (6 B7 Aj! Aj" I\r 6  G@@ Ak")! B7 Ak(! Ak" 7@ E\r  ("Ak6 \r  (( |  G\r  F@ !@ (! ("@  (Aj6  6 (!  6@ E\r  ("Ak6 \r  (( | Aj! Aj" G\r   At"j! !   k"K@ !@  (6  (6 B7 Aj! Aj" I\r 6  G@@ Ak")! B7 Ak(! Ak" 7@ E\r  ("Ak6 \r  (( |  G\r  Atj! !@ (! ("@  (Aj6  6 (!  6@ E\r  ("Ak6 \r  (( | Aj! Aj" G\r  @  ("kAu j"A€€€€I@Aÿÿÿÿ  k"Au"   I AøÿÿÿO" A€€€€O\r AtA "  kj! At"Ak"Aq   (6  ("6 @  (Aj6 Aj! Aj !  j! @@  (6  ("6 @  (Aj6  (6  ( "6 @  (Aj6 Aj! Aj" G\r ( k"@   ü\n 6   ("k"k! @   ü\n  j6 6 ( Atj6 @ r    Ý#A k"$ ($" (("G@@ ( Aj" G\r @ (AG\r ($" (("F\r !@@@@@ (("  k"AH\r  "6  6   j6 A6 B7  Aj6  Aj6@ Aj!@@ (("  Aj!    Aj"G\r  Aj" G\r    ($"@ " (("G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r ($! 6( (, r (6$ (6( (6, A6 B7 ("E\r " ( "G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r (!  6 ( r ("E\r " ("G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r (!  6 ( r A j$ ¶ ~#A k"$@@@ ($" (("F\r@ ("("A G A GqE@  Aj" G\r ((" ($"F\r A$j! @  \nAt"\rj("(" A\rkAO \nAjA!@ ($" (("F\r !@@@ ("(AG\r (( ($"kAG\r ((A!F\r Aj" G\r  A6 B7 A6 B7@@@ ("(AG\r (( ($"kAG\r ((A!G\r (" ( "G@A<"B7  6 Aà6 B7 B7 B7 B€€€€€€€À?7(  6  A j6  (68  64  60 A6 B7 Aj @ ("E\r  ("Ak6 \r  (( | (($! (" ("I@  (6  ("6 @  (Aj6  Aj6   ("k"Au"Aj"A€€€€O\rAÿÿÿÿ  k"Au"   I AøÿÿÿO"A€€€€O\r At"" j" (6  ("6  j!    (Aj6 ( ( k"Au  Atk! @   ü\n  6  Aj"6  6 @ r  6  Aj  Aj" G\r @ (" ( "F\rA<"B7  6 Aà6 B7 B7 B7 B€€€€€€€À?7(  6  A j6  (68  64  60 A6 B7 Aj  ("E\r  ("Ak6 \r  (( | ($ \rj" ! Aj" (("G@@ )! B7 (!  7@ E\r  ("Ak6 \r  (( | Aj! Aj" G\r ((!  G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r 6( (" ("  kAu (" ! ( k! ("@ " ( "G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r (!  6 ( r (! @ " ("G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r (!  6 ( r Au! ($! ((!  \nj !\n \n  kAuI\r A j$   Þ#Ak"$ @ ("E\r ( " (F\r Ak(! ("@  (Aj6 Aj!  6  6 A$j Aj@ ( "E\r ("Ak6 \r (( |   @ ("E\r  ("Ak6 \r  (( | Aj$ ¢  } ( , " AH""! (  " !@ "AI\r Ak"Aq@ !  (A•ÓÇÞl"Av sA•ÓÇÞl A•ÓÇÞls! ! Aj ! AI\r !@ (A•ÓÇÞl"Av sA•ÓÇÞl (A•ÓÇÞl"Av sA•ÓÇÞl A•ÓÇÞlsA•ÓÇÞls! Aj! Ak"AK\r @@@@ Ak -At s! -At s!  -sA•ÓÇÞl! A\rv sA•ÓÇÞl"Av s! @ ("E\r (  Akq i"AM\r   K\r  p "Atj("E\r ("E\r Ak!\n AK! @@  ("G@@ E@  \nq!   K\r  p!  F\r  ( ," AH G\rA ( Aj AH UE\r ("\r A " 6 A6 Aj!@ (", AN@  (6  )7  (! (!\n#Ak" $@@@ \n‘@ " \n’  \nA÷ÿÿÿK\r Aj \nŒAj“ (   ("6  (   \n6   \nAj‰ Aj$   A6 B7 *!\r ( Aj³!@ @ \r ³” ]E\r @A  AkqAG AIr Atr"  \r•ü"  K"AF\r   AkqE\r w " ("M@  M\r ( ³ *•ü! @ AI\r iAK\r AA Akgkt AI  w "  K" O\r !@@ "@ A€€€€O\r At"! (!  6 @ ( r (!  6 @ A ü ("E\r Aj! (!@ i" AM@  Akq!   I\r  p! Atj 6 ("E\r Ak!\n AK! @ (!@ E@  \nq!   I\r  p! @  F@ !  Atj" (E@ 6 ! !   (6  ((6 ( 6 ("\r  (! A6 @ ( r A6   (" Ak"qE@  q!   K@ !   p! @ (" Atj"("E@  (6  6  Aj6 ("E\r (!@  Ak"qE@  q!   K\r  p!  Atj 6   (6  6  ( Aj6 A : 6 ¡#Ak"$  6 Aj Aj  Aj@ ("Aj" F\r , ! ,AN@ AN@  (6  )7   ( (™   (  AH" (  ˜ Aj$ } a"AøÿÿÿI@@@ A O@ Ar"Aj! Aÿÿÿÿk6 6 6  : ! E\r E\r   ü\n  jA:   o ("@@ (! ,AH@ ( (r ,AH@ ( (r r "\r (! A6 @ ( r  (" ("I@  (6  (6 B7 Aj6 @  ("k"Au"Aj"A€€€€I@Aÿÿÿÿ  k"Au"   I AøÿÿÿO" A€€€€O\r AtA " j" (6  (6 B7  Atk! @   ü\n  Atj6 Aj"6 6 @ r 6   Î#Ak"$ @ ("E\r ( ( "F\r Ak(! ("@ (Aj6  6  6 A$j Aj ( "E\r ("Ak6 \r (( | @ ("E\r  ("Ak6 \r  (( | Aj$ Ý#A0k"$ ("(" ("G@ !@@@ (@  Aj"G\r  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6  A j6 A: A¢\n(6 A¦\n-: A: A: AôäÕ«6 A:  A j6$ A(j Aj A j A$j@ (("Aj" F\r ,AN@  (6  )7   A˜ , AN\r ( (r ,AH@ ( ( r (! (0"@ " (4"G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r (0!  64 (8 r A68 B70  (60  (64  (68 A6 B7 ( Aj ("E\r  ("Ak6 \r  (( |  @ (  Aj" G\r ("(" ("G@@@ Ak("E\r  ("Ak6 \r  (( | Ak"G\r  6 A0j$ AßÜ,AH@AÜÜ(AÔÜ(r AëÜ,AH@AèÜ(AàÜ(r A÷Ü,AH@AôÜ(AìÜ(r AƒÝ,AH@A€Ý(AøÜ(r A“Ý,AH@AÝ(AˆÝ(r AŸÝ,AH@AœÝ(A”Ý(r A«Ý,AH@A¨Ý(A Ý(r A·Ý,AH@A´Ý(A¬Ý(r AÃÝ,AH@AÀÝ(A¸Ý(r AÏÝ,AH@AÌÝ(AÄÝ(r Aà6  Aà6 r ¬#Ak"$ A j"($"@ " (("G@@@ Ak("E\r ("Ak6 \r (( |  Ak"G\r ($!  6( (, r Aj ,AH@ (  (r Aj$  r µ  (!@@@@@@@@@@@@ ( Ak  Aj"M@  6A Ak! (!@@ ! !@ j"-"AF\r @ Aj" O\r -"A k  AákAÿqAIAÿqAÓG\r -"A k  AákAÿqAIAÿqAÃG\r -"A k  AákAÿqAIAÿqAÒG\r -"A k  AákAÿqAIAÿqAÉG\r -"A k  AákAÿqAIAÿqAÐG\r -"A k  AákAÿqAIAÿqAÔG\r  j-A>F\r @ Aj" O\r -"A k  AákAÿqAIAÿqAÓG\r -"A k  AákAÿqAIAÿqAÔG\r -"A k  AákAÿqAIAÿqAÙG\r -"A k  AákAÿqAIAÿqAÌG\r -"A k  AákAÿqAIAÿqAÅG\r  j-A>F\r A\nj" O\r -"A k AákAÿqAIAÿqAÔG\r -"A k AákAÿqAIAÿqAÅG\r -"A k AákAÿqAIAÿqAØG\r -"A k AákAÿqAIAÿqAÔG\r -"A k AákAÿqAIAÿqAÁG\r -"A k AákAÿqAIAÿqAÒG\r -"A k AákAÿqAIAÿqAÅG\r - "A k AákAÿqAIAÿqAÁG\r  j-A>F\r\r Aj" G\r !  6A Aj" O\r Ak! (!@@  j"-A\nk\n\n /A­Ús -A>srE\r Aj! Aj!  I\r !  Aj" O\r Ak! (!@@  j"-A\nk /A¿üF\r Aj! Aj!  I\r !  Aj" O\r Ak! (!@@@  j-"A\nk  A>F\r Aj! Aj!  I\r !  Aj" O\r Ak! (!@@  j"-A\nk /Aݺs -A>srE\r Aj! Aj!  I\r !  @@  O\rA! ( j-A\nk  6 ( !   6A  6A  6A  6A  6A  Aj6A Å@  ("O\r  (˜O\r @ (" j-"E\r A>F@ A>:  6  -Aüq: Aj! Aj  A?K\rB ­†B€€€€€ ƒP\r  Aj"K@  j-A k"AK\rA tA“€€qE\r  :  6  -Aüq: Aj  A! A6  A j"  I"\nO\rA! !@@ j-" A0kAÿqA K\r  A\nljA0k"6 Aj" \nG\r \n! M\r O\r@ j-" A)k  Aj"K@  j-A k"AK\rA tA“€€qE\r  :  6  -Aüq:  k jAj 6  6A!  ç  ("H@@A!@@@A (Œ Alj"Ak"-A)kt"AqE@ A!qE\rA! Ak(!A! Ak-Aq@ ( j,! A! A  A4\r A ,A4\r  AA Ak-"AqA AvAqA4E\rA!  (Ak"6  H\r A Þ#A0k"$@ (ü"E\r@@@ ("AÿqAk A€qE\r (" (O\r ( j-AÛG\r@ ("E\r Aj! AG\r !  @ \r@ A/G\r Aj" O\r \n j-A>F\r AL\r@ AßqAÁkAÿqAI\r AßF\r A:G\r  Aj"M@A!  @@@ \nj,"A0kAÿqA\nI\r A_qAÁkAÿqAI\r E@A!  A ATE\r A!  Aj"G\r  A!  A! @@ Ak A=G\r Aj!A!  A+!@@@@ A"k E\r AÑ AT\r A\rF\r A)!  A*! Aj! I\r A! E\r \rF\rA  A)FA !  Aj" Atj"(! (" I\r   M@A A!  Aj6 „#Ak"$@  O\r@@@ j"  k A j7"AÿK@A!A!@@@  jAm"AtA°j("\nAu j"AtA°j(Aÿÿÿq K@ Ak!   \nAvAq j"AtA°j(AÿÿÿqM\r Aj!  L\r  AH\r  A F\r A F\r A kAI\r -A\nk ( j" I\r Aj$  à@@@ AÿK@A­!@@@  jAm"AtAÐj("Au j"AtAÐj("Aÿÿÿq K@ Ak!  AvAq j"AtAÐj(AÿÿÿqM\r Aj!  N\r  AH\rAà9!  A6  A r AÁkAI6 A!A4!@@@  jAm"AtAÐ2j("Au j"AtAÐ2j("Aÿÿÿq M@ AvAq j"AtAÐ2j(Aÿÿÿq O@ AH\rAð9!  Aj!  Ak!  N\r A!A!@@@  jAm"AtAà7j("Au j"AtAà7j("Aÿÿÿq M@ AvAq j"AtAà7j(Aÿÿÿq O@ AH\rA€:!  Aj!  Ak!  N\r A6  6  ( ( " lAtj! At"@   ü\n  6 F\r ( Aÿÿÿq"AjF@  sAsAq j6  ( kj6 õ #A k"$ B7 B7 B7 B7 A B! A B! @@@  K@   M\r @ \nE@   AjF!A! AqE@   F! A!  Atj( Aj Atj(k"\r Aj" ( " I! Aj" (" I!\n  K\r  I\r  K\r  I\r A! A j$  iA!@ ("(" ("("I\rA!  K\r ( ( ( (D"\rA! (" ("I\r K!  ‰#Ak"$@@@  O\r j  k A j7! ( j! AÿK@A!@@@  jAm"AtA°j("\nAu j" AtA°j(Aÿÿÿq K@ Ak!   \nAvAq j"AtA°j(AÿÿÿqM\r Aj!  L\r  AN\r  A F\r A F\r A kAO\r A6 A 6  B!   C Aj$  D ("(" ("("I@A  K@A ( ( ( (D ž“#A0k"$ A6x#Ak"$ AjAA€ü @@@ E\r Ak! A€j!\r  AtjAk!@  Atj"(!@ !@ !@ Aj" ("O@ !  (! @ \r  j"-j-@ !  \r -j-@ !  \r -j-@ !  \r  j-j-@ !  Aj! Aj"!  K\r @  K@ (!@ \r  j"-" j-\r Aj" G\r Aj" I\r  @AA@@@@@@ AÜF@A! Aj" (O\r@  j-"A!kAÿqAI\r@ AÿqA\nku Aj I\r Aj! @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"\r  6t (x"Aj6x  Alj"AÜ ; B7  Aj"6  6 A!@@@@@@@@@@@@ A!k\\ \r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\n\r \r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r Aj"!\n@  M\r@  \nj- G\r \nAj"\n I\r !\n A!A!A!  (F\r Q"AÿM\rA! A!@@@@  jAm" AtA°j("Au j"AtA°j(Aÿÿÿq K@ Ak!   AvAq j" AtA°j(AÿÿÿqM\r Aj!  N\r  A! AN\r A! Aä!@@@  jAm" AtAÀ:j("Au j"AtAÀ:j(Aÿÿÿq K@ Ak!   AvAq j" AtAÀ:j(AÿÿÿqM\r Aj!  N\r A! AN   k! !@@  j-" AàG\r Aj" G\r !A  A F ! !  k"A K\r At j( "  AtjAk(O\rA!A!@ E@ ! !\n  !\n  " I\r @@@@ \nO\r@  j-"A G@ AàF\rA! Aj" \nG\r A! \n  ! \nO@A! !  @  j-AàG" @ ! !  Aj" \nG\r ! \n! @  k"F@A!\n  Atj" (" I@  jAk-A F!\n As  Gq\r \r F \nrE\r E\r Aj  @ F\rA! AK\r At jA j" (" K6 \r  Aj"M@A!   Atj"\n(! \n(!\n  ( ! \n@ Ak!  Ak(" ("O\r@  j-"A G A Gq\r Aj" G\r ! @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"\r  6t (x"Aj6x  Alj"Aà; B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"\r  6t (x"Aj6x  Alj"Aà; B7  6  6 (t (x"Alj"Ak" Ak6 A(k" Ak6  -Ar:  -Ar:A! ! ! ! (M\r@@@@   jAv"Atj"( K@ E\r  AtjAk( I\r Ak!  (K\r ! ! !  Aj! A!  M\r ! !  @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"\r  6t (x"Aj6x  Alj"A¦; B7  Aj"6  6  (x"AL\r (t" AljAk-A&G\r (|" L@ AÀ Av j AL"6|  Als"E@A! ($"\rE\rAå (, \r  6t (x! Aj6x  Alj"A»; B7  Aj6  6  ("\n - AÀq\r\n   k"  \n A jA\r (" AjM\r -A!G\r  Aj"j-A-G\r -A-G\r A! (è" K  \nAkOq\r @ Aj" \n \nI"M@  j" /A­Ús -A>srE\r\r@@ Aj! Aj" K\r " j" /A­Ús -A>sr\r  !  \nI Aj" Kq@  Atj"(! (!  6è (! A! -AqE\r Aj" O\r  j-AüG\r@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"Aü; B7  Aj"6  6  ("Aj K\r  Ak"j"/A÷îs -A÷sr\r @  F\r Q"AÿK@A!A!@@@@  jAm" AtA°j("\nAu j"AtA°j(Aÿÿÿq K@ Ak!   \nAvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r  AN\r A!Aä!@@@  jAm" AtAÀ:j("\nAu j"AtAÀ:j(Aÿÿÿq K@ Ak!   \nAvAq j"AtAÀ:j(AÿÿÿqM\r Aj!  N\r  AH\r\r  A F\r A F\r A\rkA}K\r A!kAI\r A:kAI\r AÛkAI\r AÿG AúKqE\r @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"A®; B7  Aj"6  6@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" (6 (! AÄ; B7  6  Aj!@@ (" Aj K\r  Ak"\nj(AèèуG\r  M\r /A¯ÞF\r @ Aj K\r  Ak"\nj"(Aèèуs -Aósr\r  M\r /A¯ÞF\r Aj K\r  Ak"\nj"/Aæès -Aðsr\r  M\r /A¯ÞG\r @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"Aº; B7  6  \n6@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" (6 (! AÄ; B7  6 Aj!  (Aj K\r @ Ak-"AßqAÁkAÿqAO@ Aj O\r A0kAÿqA\nO\r  Aj O\r\n  Aj"j-"A0kAÿqA\nO@ A_qAÁkAÿqAK\r\n @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"AÀ; B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" (6 (! AÄ; B7  6 Aj!  @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"AÝ; B7  Aj"6  6 \r @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" 6A!A  Aj" O\r  j-AÛG\r@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" 6A!!A !  :  : B7   j"6@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"AÄ; B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"AÄ; B7  6  6 \n @@ E@  -\rA qAvq\r  E\r @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"A:  : B7  Aj"6  6 \n !@@@@@@@@@AA@@AA@@@@@@@@@@ > AþF\r AÞG\r -AqE\r Aj" !@  M\r@@  j-AÞG\r Aj" I\r ! !  kAG\r A! A!  j ( kA7"AÿM@ A F\r A F\r A kAM\rA  @@@  jAm"AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r A  \nAN\rA  A !  (F\r Q"AÿM\rA!A!@@@  jAm" AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r !  \nAN\r !  -A qE\r@ Aj" O\r  j" -A=F\r !  kAG\rA!A!AA ( kA7"AÿM@A! A F\r A F\r A kAI  @@@  jAm" AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r A!\n \nAN !  -A€qE\r@ Aj" O\r  j" -A+F\r !  kAG\rA!A!AA ( kA7"AÿM@A! A F\r A F\r A kAI  @@@  jAm" AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r A!\n \nAN !  A! Aj"\n!@  \nM"\r@  j-AþG" \r Aj" I\r ! @  k"AF@ ( "A€€qE\r@ \rA! A!  \nj ( \nkA7"AÿM@ A F\r A F\r A kAM\rA  @@@  jAm"AtA°j(" Au j"AtA°j(Aÿÿÿq K@ Ak!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r A  AN\rA  A !  (F\r Q"AÿM\r\rA!A!@@@  jAm" AtA°j(" Au j"AtA°j(Aÿÿÿq K@ Ak!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r !  AN\r !  ! AK\r ( ! ! A€qE\rA!  (M\r\n Q"AÿM\rA!\nA!@@@@  \njAm"AtA°j(" Au j" AtA°j(Aÿÿÿq K@ Ak!   AvAq j"\nAtA°j(AÿÿÿqM\r \nAj!\n  \nN\r  AN\r A!\nAä!@@@  \njAm"AtAÀ:j(" Au j" AtAÀ:j(Aÿÿÿq K@ Ak!   AvAq j"\nAtAÀ:j(AÿÿÿqM\r \nAj!\n  \nN\r A! AN @ Aj" O" E@  j-A$F\r !  kAK\rA!  (M\r Q"AÿM\rA!\nA!@@@@  \njAm"AtA°j(" Au j" AtA°j(Aÿÿÿq K@ Ak!   AvAq j"\nAtA°j(AÿÿÿqM\r \nAj!\n  \nN\r  AN\r A!\nAä!@@@  \njAm"AtAÀ:j(" Au j" AtAÀ:j(Aÿÿÿq K@ Ak!   AvAq j"\nAtAÀ:j(AÿÿÿqM\r \nAj!\n  \nN\r A! AN  @ Aj" O\r@  j-A k"AK\rA tA€€qE\r Aj" I\r ! A F@ !  kAI\r @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\r!Aå (, \r ! Aj6x (t"E\r  6t (x"Aj6x  Alj"A:  : B7  6  6  @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\r Aå (, \r Aj6x (t"E\r  6t (x"Aj6x  Alj"A€ ; B7  Aj"6  6  A! !  kAG\r A! !  kAG\r A F\r A F\r A\rkA}K\r AÿG AúKq A!kAI A:kAIr AÛkAIrr ! @ \rA! A!@  j" ( k" A7"AÿM@ A F\r A F\r A kAO\r  @@@  jAm"AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r  \nAN\r A!Aä! A7"AÿM@ AÿG AúKq A!kAI A:kAIr AÛkAIrr  @@@  jAm" AtAÀ:j("Au j"\nAtAÀ:j(Aÿÿÿq K@ \nAk!   AvAq j"AtAÀ:j(AÿÿÿqM\r Aj!  N\r A!\n \nAN ! !  Aq "E\r @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" : A$: B7  6  6  A F\r A F\r A\rkA}K\r AÿG AúKq A!kAI A:kAIr AÛkAIrr ! @ E\rA! A!@  j" ( k" A7"AÿM@ A F\r A F\r A kAO\r  @@@  jAm"AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r  \nAN\r A!Aä! A7"AÿM@ AÿG AúKq A!kAI A:kAIr AÛkAIrr  @@@  jAm" AtAÀ:j("Au j"\nAtAÀ:j(Aÿÿÿq K@ \nAk!   AvAq j"AtAÀ:j(AÿÿÿqM\r Aj!  N\r A!\n \nAN ! !  Aq "E\r @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" : Aþ: B7  6  6  A F\r A F\r A kAM\r !  A! ! AqE\r @@ (x" (|" N@ AÀ Av j AL"6| (t Als" \rA! ($"\rE\rAå (, \r  Aj6x (t" E\r  6t (x"Aj6x Alj" : Aþ: B7  \n6  6  @@  (F\r Q"AÿK@A!A!@@@  jAm" AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r !  \nAN\r !  A F\r A F\r A kAM\r !  A! ! AqE\r\r @@ (x" (|" N@ AÀ Av j AL"6| (t Als" \rA! ($"\rE\rAå (, \r  Aj6x (t" E\r  6t (x"Aj6x Alj" : A+: B7  6  6 \r @@  (F\r Q"AÿK@A!A!@@@  jAm" AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r !  \nAN\r !  A F\r A F\r A kAM\r !  A! ! AqE\r @@ (x" (|" N@ AÀ Av j AL"6| (t Als" \rA! ($"\rE\rAå (, \r  Aj6x (t" E\r  6t (x"Aj6x Alj" : A=: B7  6  6 A F\r A F\r A kAM\r !  A! ! AqE\r @@ (x" (|" N@ AÀ Av j AL"6| (t Als" \rA! ($"\rE\rAå (, \r  Aj6x (t" E\r  6t (x"Aj6x Alj" : AÞ: B7  6  6 @ Aj" O\r -A?G\rA!@ (ì" M@ !  !  \nAkO\r  Aj" \n \nI"M@  j/A¿üF\r@@ Aj! Aj" K\r " j/A¿üG\r  !  \nI Aj"Kq  Atj"(! (!  6ì ( ! @ M\r -A!G\r  j-AßqAÁkAÿqAK\r@ Aj" O\r@  j-AßqAÁkAÿqAK\r Aj" G\r ! A! (ð" K  \nAkOq\r Aj" \n \nI"M@  j-A>F\r@@ Aj! Aj" K\r " j-A>G\r  !  \nI Aj" Kq  Atj"(! (!  6ð ( ! @ A j" O\r )B¼ÂìšÄ¨ªÁ… 1Bۅ„BR\rA! (ô" K  \nAkOq\r@ Aj" \n \nI" M@  j"/Aݺs -A>srE\r@@ Aj! Aj" K\r  "j"/Aݺs -A>sr\r  !  \nI Aj"Kq@  Atj"(! (!  6ô ( !\n ! Aj" \nO\r@ Aj" \nO\r  j-" A0kAÿqA\nIA AßqAÁkAÿqAI E\r@  j-"A0kAÿqA\nI\r AßqAÁkAÿqAI\r A+k"AK\r AF\r Aj" \nF\r@  kA K\r  j-" A:F@  \nO\r@  j-" A k"AMAA tA€€q\r A>G@ A I\r AG\r  j-A-F\rAð! Aj !@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" Ar: A<: B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" Ar: A>: B7  6  Ak6 (t (x"Alj"A k Ak6 Ak Ak6 ! !   6 @@ (x" (|"N@ AÀ Av j AL"6| (t Als"E\r 6t (x"Aj6x  Aj6x (t"E\r\n  Alj"A¼(; B7  6  6@ (x" (|"N@ AÀ Av j AL"6| (t Als"E\r 6t (x"Aj6x  Aj6x (t"E\r\n ( !  Alj"A¾0; B7  6  6 (t (x"Alj"A k Ak6 Ak Ak6A! !  (M\r@@   jAv"Atj"( K@ E@ ! \n   AtjAk(K@ ! \n Ak!  ( O@ ! Aj!  M\r A!  ($"E\rAå (,   Aj!  A F\r A F\r A\rkA}K\r AÿG AúKq A!kAI A:kAIr AÛkAIrr "! As! A! @  \nF\rA!A!@@  \nj" ( \nk"A7"AÿM@ A G\r  @@@  jAm"AtA°j(" Au j" AtA°j(Aÿÿÿq K@ Ak!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r  A! AH\r  A F\r A kAI\r A!Aä!AA  A7"AÿM@ AÿG AúKq A!kAI A:kAIr AÛkAIrr  @@@  jAm"AtAÀ:j(" Au j" AtAÀ:j(Aÿÿÿq K@ Ak!   AvAq j"AtAÀ:j(AÿÿÿqM\r Aj!  N\r A! AN "! As! ! \n!A   q AßFq""A " rE\r  kAp!@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"\r  6t (x"Aj6x  Alj" AtA# AG  MqAA  OA r" AFrA@k:  : B7  \n6  6 ! \n "M\r@@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\r Aå (, \r Aj6x (t"\r  6t (x"Aj6x  Alj"AÄ; B7  6  6 Aj" \nI\r ! \n! @@ (x"\r (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  \rAj6x (t"E\r  6t (x"\rAj6x (!  \rAlj"Aÿ ; B7  6  6A!  A! Aj$@ " \r A (xA¿AM@ (à"AN@ Ak! A€j! AØj! A j!@ (t" Al"j" (!\r@ -"AÄF\r ( !\nA! A! \rAN@   \rAlj" ( Alj! \nAl! (! -A q@ A!: Ak"6A!!  j!  I@ ( I\r  I@ ( I\r @  I@ A!G\r  AÛG\r ( A€€ÀqE\r (" (O\r ( j-AÞG\r Aj"6 (t j"(" M\r  ( j  kN"E\r ( "E@ (pAj"6p  6  (Aj6 6 (6 -A”r:  -A˜r: (! (! (  @@@@ -\rA qE\r -AÛG\r E\r (" ("kAG\r -AÛG\r E\r ( ("kAG\r -AÝG\r  AkG\r ( ("kAG\r  (AjG\r@@ Aj" \nN"@ !  (t! !@@  Alj" -"AÄF\r AüF\r ( kAäK\r -AqE\r ( ! Aj" \nH\r A!  (" F\r  kAäK\r  I@ (!@@  j-A\nk Aj" G\r A!@   Atj"("L@ (t!\r@ \r Alj( " N\r  6  ("L@ (t!\r@ \r Alj( " N\r  6 Aj"AG\r @@ ( (I@@  L\r  kAk! (t!\rA!  AsjAq"@@ \r AljAÄ; Aj! Aj" G\r AI\r@ \r Alj"AÄ;L AÄ;8 AÄ;$ AÄ; Aj" G\r -Ar: (6  @ \r \n kAk! (t!\rA! \n AsjAq"@@ \r AljAÄ; Aj! Aj" G\r AI\r@ \r Alj"AÄ;L AÄ;8 AÄ;$ AÄ; Aj" \nG\r  (6 (6  (6 (t"\r j" \n6 \r j"\r 6  -Ar: \r \r-Ar: (! (!  Aj \nO (  (6  (6 (t"\r j" \n6 \r j"\r 6  -Ar: \r \r-Ar: (! (! (  \r (!  (" ("G\r ( AjK@ -AÀq\r    ( AjP  -AÀq\r   (  AjP "AH\r E\r  (6 (t \rAlj(!\r  @  ("O\r (" j-A(G\r \nAj! A! !@@@   jAv"Atj"( K@ E\r  AtjAk( I\r Ak!   (M\r Aj!  O\r A! @@ Aj"  Atj(" I@@  j-A k"AK\rA tA€€qE\r Aj" G\r !  6@  O\r  j-A\nk Aj" O\r  Atj(!  6 @  O\r  j-A)G\rA! A6 B7  6  6 Aj! A    Atj"(" Aj Aj 8E\r@   k ( Aj A j A,j A(j A$j9@ ( " j! (," F!   j"Atj(! (!   ("6(  6$A!A! @  I@@  j-"A k"AK\rA tA€€qE\r Aj" G\r Aj" O\r   Atj("j-! AÿqA)G\r  Aj" 6 ($" (("M@A! A6 B7A   k! @ A6  6   j"6A  @ q"@  Atj!A! !@   Atj("  K" KA!  "kAq"@@  ( j-: Aj! Aj! Aj" G\r  kA|M@@  ( j-:  ( j-:  ( j-:  ( j-: Aj! Aj" G\r   O\r A\n: Aj!  Aj"Atj(! ($" E\rAÔ (,   A6  6   k6 (! A ! (x" L@  6  (t! !@  Alj"(K@@ -AqAG\r  ( "Alj( I\r \r r  Aj" H\r  6  L\r  \nkAk! (t! A!  \nAsjAq"@@ AljAÄ; Aj! Aj" G\r AI\r@ Alj"AÄ; AÄ;L AÄ;8 AÄ;$ Aj" G\r  -AÀq\r   ( ( AjP"AH\r E\r -Ar:  -Ar: Aj" (tj (6 (t j ( 6  (tj (6( Aj! (t! (@  Alj (Ð6 6Ð  Alj (6 (! (! -!  Aj \nO   AÛF"!   !   !   ! ! / AŒqE\r@@ "Aj! -$"AÄG" \r  I\r À! !@@ "\nAk! \r  K\r -%" AqE\r E\r ( (G\rA÷ ATE\r (  (tkAmG\r \nAk( (G\r AÄ:$  Aïq:% \nAkAÄ: \nAk" -Aïq: \r "AN\r B7à A6¸ @A! A6Ô (x"AL\r (t!\rA!@@ \r Alj"-"Aq\r -AüG\r ( (kAG\r A6  Ar:@ (ØAH@ 6Ø  \r (ÜAlj 6 6Ü Aj"6Ô Aj" G\r A!  A! A (xO  A! A0j$ Þ#Aðk"$ B7h B7` B7X B7P B7H B7@ B78 B70 A6( B7 B7 B7 B7A! @    Aj AÐj<\r  A Aj A0j<\r  6( (,! @  Aj (" E@A!  ($"E\rA (,    Aj (" E@A!  ($"E\rAæ (,  (`@ (Pr (Tr (Xr (@@ (0r (4r (8r Aðj$ Ý@ ( " ("I@ (!  ( Av jA "At"s"E@A ($"E\rAå (, A  6 ( Ajs"E@A ($"E\rAå (, A  6  6 ( ! ( Atj 6  ( Atj 6  ( Aj6 A Ü Aj!@@@ Aj" O\r j-A#G\r j-A rAøG\r !@@@@ j-"A0kAÿqA\nI\r AÁkAÿqAI\r  kAK\r AákAÿqAI\r   kAK\r Aj" G\r !  F\r  kAI\r  @ Aj" O"\r j-A#G\r !@@  kAK\r j-A0kAÿqA K\r Aj" G\r !  F\r  kAI\r   M\r j-AßqAÁkAÿqAK\r  \r@@ j-"AßqAÁkAÿqAO@   kA0K\r A0kAÿqA\nI\r    kA0M\r   Aj" G\r  " kAkA.K\r  M\r j-A;G\rA!  Aj6  ³#Ak"$@@  O\r (!@  j-A k"AK\rA tA€€qE\r Aj" G\r !   O\r (!@  jAk-A k"AK\rA tA€€qE\r Ak" K\r !  6  6  6@  Aj (, ("@ ($"E\rAŠ (,   AjA?"\r  Aj (, ("E@A!  ($"E\rA¯ (,  Aj$  Ê"#A0k"\r$  H@ A¸j! A€j! \rAr! \rAr! (!@@ (t" Al" j"-"Aq@ AqE\r@ E\r ,"E\r  `\r ( !  ,"\nAÿq"E\r  \n`E\r (" I\r@@@@@@@@@@@@@@@@@ A!k^  (! Aq@ AN@  Alj" -AÀr:  6  6  AH\r  Alj"( 6¸  6  6@ (ä" AN@  Alj 6  6à 6ä A6  Aj" (xN\r  Alj"-A;G\r (  (" \rKE\r  6  6  Ar:  -Ar:  6  AqE\r \r A€A˜ \nA*Fj"A j6 AÀq" AÀF@ ! A  \r Aj6 A€F\r ! A ! Aj6A! A°A´ ( kAFj!\n@ AqE\rA! \n("AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6  6  -Ar:  -Ar: \r AqE\r  \n(6 \n 6 @ AqE\rA! (Ä"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6  6  -Ar:  -Ar: AqE\r  (Ä6 6Ä @ AqE\r (¼"AH\rA!  Alj" ( (k ( kG\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r @ Aj" N\r  kAk!A!  AsjAq"\n@@  AljAÄ; Aj! Aj" \nG\r AI\r@  Alj" AÄ;L AÄ;8 AÄ;$ AÄ; Aj" G\r 6  6 -Ar:  -Ar: A6¼ AqE\r\n  (¼6 6¼ \n  Aj"Al"j"(! (! \r 6 \r ("6, \r 6( \r 6$ \nAÀF@   \rAAAA˜ \rA(jRAL\r\n \r(!  O\r Q" AÿM\rA!A!@@@  jAm" AtA°j("Au j"AtA°j(Aÿÿÿq K@ Ak!  AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r  AN\r  ( kAG\r@ AqE\rA! (À"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6  6  -Ar:  -Ar: AqE\r  (À6 6À  ( kAG\r@ AqE\rA! (È"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6  6  -Ar:  -Ar:  AqE\r  (È6 6È  ( kAG\r@ AqE\rA! (Ì"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6  6  -Ar:  -Ar:  AqE\r  (Ì6 6Ì  A! A!  A F\r A F\r A kAI\r ( jAk,"@Aã AT\r \r((" I\r Ak!@@@ -AÄF\r ( K\r  (O\r -" Aq\r Ak" O\r  AqE\r   \rA,jAA.AAØ \rA$jRAH\r \r(,! @ \nAÀF\r@ O\r (" j-A/G\r Aj  \rA,jAA/AùA \rA$jRAH\r \r(," O\r  j-A/G\r \r Aj" 6, @ O\r ( j-A?G\r Aj  \rA,jAA&AóA• \rA$jRAH\r \r(,! O\r ( j-A#G\r Aj  \rA,jAAAAÛ \rA$jRAH\r \r(,! @ O\rA!A!@ ( j" ( kA7"AÿM@ A F\r A F\r A kAO\r  @@@  jAm" AtA°j("Au j"AtA°j(Aÿÿÿq K@ Ak!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r  AN\r ,"@A¡ A T\r \r($"  (xAlj"O\r@@@ -AÄF\r ( M\r ( K\r -"Aq\r Aj" I\r  AqE\r  6  6  \n:  6  6 (t" j" 6  j" 6  -Ar:  -Ar:  \r Atr 6 A q! Aj! AàqAàG@ \r Atj Aj6 Aj! As AGqE@ \r Atj Aj6 Aj!  \r(("AljA AN"! A !@ \r(("AH\r  Alj! @ ( (M\r ! ! @ AF\r@ \r(("AH\r  Alj! @ ( (M\r ! ! AF\r@ \r( ("AH\r  Alj! @ ( (M\r ! ! AF\r@ \r(("AH\r  Alj! @ ( (M\r ! ! AF\r \r((" AH\r  Alj! @ ( (M\r ! ! E\r (! (! (! !\n@@@@@@  Alj"-"A!k\n AÛk A€A˜ A*Fj -"AtAuA qj!\n@ AvAk \nAj!\n  A°A´ ( (kAFj!\n  \nAj!\n @ k" k"K@  (" (k k j"Al" j" (6  )7  )7  k"6  6 (t j \n(6 \n 6  O\r  Alj j" (6  )7  )7  k j"6  6 A!\n@   \nAtj"("L@@ AN@  (t Alj( "6  N\r \nAj"\nAG\r (t" Alj" 6  j" 6  -Ar:  -Ar:  AqE\r A€A˜ \nA*Fj AtAuA qj!@@@ AvAk Aj!  Aj!  (6  6 -"AqE\r Aq@ (t ( Alj(!  (! Aj" H\r \rA0j$ –#Ak"$@ (E\r  6! (  (pAtj("E@A!  @  ("I\r   ( (ljO\r  (F@ ( (  DE\r A!   6  6  6  Aj6 Aj!@ ("@@   Av"Atj"G"AH@   E\r Aj!  Asj "\r A! E@A!  (! Aj$  ý#A k"$ Aß: A¦Ô;A! ( "A€ q@ A$:A! A€€€q@ Aj rA=: Aj! A€€€q@ Aj rA+: Aj! A€€q@ Aj jAü: Aj! A€€q@ Aj jAÞ: Aj! A€„q@ Aj jAþ: Aj! A! Aj jA:    AjAM ( "AŒq@ Aq@ AÀ:A! Aq@  rA:: Aj! A€q  rA.: Aj  rA:     AjM A€jAÿAÐü A j$ • (TE@A Ak! Ak!AA ("\n j-A!F j!A!@@@   jAv"Atj" ( K@ E\r  AtjAk( I\r Ak!   (M\r Aj!  O\r A! @  ("M" E@@  kq"@ !@    K" KA!  "kAq"\n@@  ( j-: Aj! Aj! Aj" \nG\r  kA|M@@  ( j-:  ( j-:  ( j-:  ( j-: Aj! Aj" G\r   O\r A : Aj! Aj" Atj"(! (! A! ($"E\rAÔ (, A  k   \nj!  k ! A  Aj60A! Aj !\nA!@@ \n("E\r Aj p"AH\r   kK\r \nAj!\n  j" I\r A=! AH\r A \r  l E@A!  A! (0!\n@ \n(" E\r Aj p" j" K\r Aj h \nAj!\n  K\r A \r  A€Àsl \r   \rH!   AHq\r A=! +0 \r  m"AN\r \n -!\n Aj! \r E\rA!@  Atj("\n@  Atj \n jA! Aj"A\nG\r A\nO@A! \n @  Atj(\rA! Aj"A\nG\r A!   :\'A! ! !   k"  H" AÿÿÿÿsJ\rA=! \r  j"  \rH" \nK\r A   l  h A0   A€€sl A0  Al h A   A€Àsl ( Ak" O\r B€”ëÜT\r Ak" > @ "I@ Ak"(E\r (, k"6, ! AJ\r AH@ AjA nAj! AæF!@A A k" A O!\n@  M@AA (!  A€”ëÜ \nv!\rA \ntAs!A! !@  (" \nv j6  q \rl! Aj" I\r AA (! E\r  6 Aj! (, \nj"6,   j" " Atj   kAu J! AH\r A!@  M\r  kAuA l!A\n! ("A\nI\r@ Aj!  A\nl"O\r A AæGk AçF AGqk"  kAuA lA kH@ A0jA„`A¤b AHj A€Èj"A m"\rAtj!\nA\n!  \rA lk"AL@@ A\nl! Aj"AG\r @ \n("  n" lk"\rE \nAj" Fq\r@ AqE@D@C! A€”ëÜG\r \nO\r \nAk-AqE\r D@C! Dà?Dð?Dø?  FDø? \r Av"F \r I!@ \r -A-G\r š! š! \n  \rk"6    a\r \n  j"6 A€”ëÜO@@ \nA6 \nAk"\nK@ Ak" A6 \n \n(Aj"6 Aÿ“ëÜK\r  kAuA l!A\n! ("A\nI\r@ Aj!  A\nl"O\r \nAj"   K! @ " M"E@ Ak"(E\r @ AçG@ Aq!\r  AsA A " J A{Jq"\n j! AA~ \n j! Aq"\r\rAw!@ \r Ak("\nE\rA\n!A! \nA\np\r@ "\rAj! \n A\nl"pE\r \rAs!  kAuA l! A_qAÆF@A!\r  jA k"A AJ"  J!  A!\r  j jA k"A AJ"  J! A!\n AýÿÿÿAþÿÿÿ \rr"J\r AGjAj!@ A_q"AÆF@  AÿÿÿÿsJ\r A AJ!    Au"s k­ k"kAL@@ Ak"A0:  kAH\r Ak" : AkA-A+ AH:  k" AÿÿÿÿsJ\r  j" AÿÿÿÿsJ\r A   j" l  h A0   A€€sl@@@ AÆF@ AjA r!  K"! @ 5 k!@  G@  AjM\r@ Ak"A0:  AjK\r   G\r Ak"A0:   kh Aj" M\r @ AùAh  M\r AL\r@ 5 k" AjK@@ Ak"A0:  AjK\r A A Nh A k! Aj" O\r A J! ! \r  @ AH\r  Aj  K!\n AjA r! !@  5 k"F@ Ak"A0: @  G@  AjM\r@ Ak"A0:  AjK\r  Ah Aj! \rrE\r AùAh   k"  Hh k! Aj" \nO\r AN\r A0 AjAAl   kh  ! A0 A jA Al A   A€Àsl    J!\n   AtAuA qj!@ A K\rA k!D0@!@ D0@¢! Ak"\r -A-F@  š ¡ š!     ¡!  (," Au"s k­ k"F@ Ak"A0: (,! Ar!\r A q! Ak" Aj: AkA-A+ AH: AqE ALq! Aj!@ " ü"AÐÑj- r:  ·¡D0@¢!@ Aj" AjkAG\r Da q\r A.: Aj! Db\r A!\n Aýÿÿÿ \r  k" j"kJ\r A   Aj  Ajk" Ak H  "j" l  \rh A0   A€€sl Aj h A0  kAAl  h A   A€Àsl    J!\n A°j$ \n ¹~  (AjAxq"Aj6 )! )!#A k"$ Bÿÿÿÿÿÿ?ƒ!~ B0ˆBÿÿƒ"§"AøkAýM@ B† B<ˆ„! A€øk­!@ Bÿÿÿÿÿÿÿÿƒ"B€€€€€€€Z@ B|!  B€€€€€€€€R\r Bƒ |! B  BÿÿÿÿÿÿÿV"! ­ |  @  „P\r BÿÿR\r B† B<ˆ„B€€€€€€€„!Bÿ  Aþ‡K@B!Bÿ  A€øAø P"\n" k" AðJ@B!B   B€€€€€€À„ \n!A!\n G@ Aj! ! !@A€ k"AÀq@  A@j­†!B!  E\r  ­"† AÀ k­ˆ„!  †! 7 7 ) )„BR!\n @ AÀq@  A@j­ˆ!B!  E\r AÀ k­†  ­"ˆ„!  ˆ!  7  7 )B† )"B<ˆ„!@ \n­ Bÿÿÿÿÿÿÿÿƒ„"B€€€€€€€Z@ B|!  B€€€€€€€€R\r Bƒ |! B€€€€€€€…  BÿÿÿÿÿÿÿV"! ­ ! A j$ B€€€€€€€€€ƒ B4†„ „¿9 ¦ (T"(! (" ( ("k"  I"@   V  ( j"6  ( k"6    K"@   V  ( j"6  ( k6 A: (,"6 6  ¢ E@A @  AÿM\r@AìÞ((E@ A€qA€¿F\rAÐÝA6  AÿM@ A?qA€r: AvAÀr:A  A€@qA€ÀG A€°OqE@ A?qA€r: A vAàr: AvA?qA€r:A  A€€kAÿÿ?M@ A?qA€r: AvAðr: AvA?qA€r: A vA?qA€r:A  AÐÝA6 AA  :A Ñ( #Ak"\n$@@@@@@@@@@ AôM@Aß("A A jAøq A I"Av"v"Aq@@ AsAq j"At"A¸ßj" (À_"("F@Aß A~ wq6   6 6 Aj!  Ar6  j" (Ar6 A˜ß("M\r @@ tA t"A krqh"At"A¸ßj" (À_"("F@Aß A~ wq"6   6  6 Ar6 j"  k"Ar6 j 6 @ AxqA¸ßj!A¤ß(! A Avt"qE@Aß  r6   ( !  6  6  6  6 Aj!A¤ß 6A˜ß 6 A”ß(" E\r hAt(Àa"(Axq k! !@@ ("E@ ("E\r (Axq k"   K"!  ! !  (!  ( "G@ (" 6 6 \n (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6 A! A¿K\r A j"Axq!A”ß(" E\rA! AôÿÿM@ A& Avg"kvAq AtkA>j! A k!@@@ At(Àa"E@A!  A! A AvkA AGt!@@ (Axq k" O\r ! "\rA! !  ("   AvAqj("F ! At! "\r rE@A!A t"A kr q"E\r hAt(Àa! E\r @ (Axq k" I!   !  ! ("  ( "\r E\r A˜ß( kO\r (!  ( "G@ (" 6 6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  A˜ß("M@A¤ß(!@ k"AO@  j" Ar6 j 6  Ar6   Ar6 j" (Ar6A!A! A˜ß 6A¤ß 6 Aj! Aœß("I@Aœß  k"6A¨ßA¨ß(" j"6  Ar6 Ar6 Aj! A! A/j"Aèâ(@Aðâ(  AôâB7AìâB€ €€€€7Aèâ \nA jApqAتժs6AüâA6AÌâA6A€ "j"A k"q" M\rAÈâ("@AÀâ(" j" M\r  I\r @AÌâ-AqE@@@@@A¨ß("@AÐâ!@ (" M@   (jI\r ("\r Au"AF\r !Aìâ("Ak" q@  k  jA kqj!  O\rAÈâ("@AÀâ(" j" M\r I\r u" G\r   k q"u" ( (jF\r ! AF\r A0j M@ !  Aðâ("  kjA kq"uAF\r  j! !  AG\r AÌâAÌâ(Ar6 u!Au! AF\r AF\r M\r k" A(jM\r AÀâAÀâ( j"6AÄâ( I@AÄâ 6 @A¨ß("@AÐâ!@  (" ("jF\r ("\r  A ß("A ME@A ß 6 A!AÔâ 6AÐâ 6A°ßA6A´ßAèâ(6AÜâA6@ At" A¸ßj"6À_  6Ä_ Aj"A G\r Aœß A(k"Ax kAq"k"6A¨ß  j"6  Ar6 jA(6A¬ßAøâ(6   O\r  I\r ( Aq\r  j6A¨ß Ax kAq"j"6AœßAœß( j" k"6  Ar6  jA(6A¬ßAøâ(6  A!  A!  A ß( K@A ß 6  j!AÐâ!@@  ("G@ ("\r  - AqE\r AÐâ!@@ (" M@   (j"I\r (!  Aœß A(k"Ax kAq"k"6A¨ß  j"6  Ar6 jA(6A¬ßAøâ(6  A\' kAqjA/k" AjI"A6 AØâ)7 AÐâ)7AØâ Aj6AÔâ 6AÐâ 6AÜâA6 Aj!@ A6 Aj! Aj!  I\r  F\r  (A~q6   k"Ar6  6 AÿM@ AøqA¸ßj!Aß("A Avt"qE@Aß  r6  ( ! 6  6 A !A  A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÀáj!@@A”ß("A t"qE@A”ß  r6  6  6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6  6 A! ! !A  (" 6  6  6A!A!A j 6  j 6 Aœß(" M\rAœß k"6A¨ßA¨ß(" j"6  Ar6 Ar6 Aj!  AÐÝA06A!  6 ( j6 Ax kAqj" Ar6 Ax kAqj"  j"k!@A¨ß( F@A¨ß 6AœßAœß( j"6  Ar6  A¤ß( F@A¤ß 6A˜ßA˜ß( j"6  Ar6  j 6  ("AqAF@ Axq! ( !@ AÿM@ (" F@AßAß(A~ Avwq6  6  6  (!@  G@ (" 6  6  @ (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  A! E\r@ ("At"(Àa F@ AÀáj 6 \rA”ßA”ß(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6  j"(!  j!  A~q6  Ar6  j 6 AÿM@ AøqA¸ßj!Aß("A Avt"qE@Aß  r6   ( !  6  6  6  6  A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÀáj!@@A”ß("A t"qE@A”ß r6  6  6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6  6  6  6  (" 6  6 A6  6  6 Aj!  @ E\r@ ("At"(Àa F@ AÀáj 6 \rA”ß A~ wq" 6  @  (F@  6   6 E\r 6 ("@ 6  6 ("E\r 6  6 @ AM@   j"Ar6 j" (Ar6   Ar6  j" Ar6  j 6 AÿM@ AøqA¸ßj!Aß("A Avt"qE@Aß  r6  ( ! 6  6  6  6  A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÀáj!@@ A t"qE@A”ß  r6  6  6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6  6  6  6  (" 6  6 A6  6  6 Aj!  @ E\r@ ("At"(Àa F@ AÀáj 6 \rA”ß A~ wq6  @  (F@ 6  6 E\r 6 ("@ 6  6 ("E\r 6  6 @ AM@   j"Ar6 j" (Ar6   Ar6  j" Ar6  j 6 @ AxqA¸ßj!A¤ß(!A Avt" qE@Aß  r6   ( !  6  6 6 6 A¤ß 6A˜ß 6 Aj! \nAj$ ‚ @ E\r Ak" Ak("Axq"j!@ Aq\r AqE\r  ("k"A ß(I\r j!@@@A¤ß( G@ ( ! AÿM@  ("G\rAßAß(A~ Avwq6  (!  G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  ("AqAG\rA˜ß 6  A~q6  Ar6  6  6  6  A! E\r@ ("At"(Àa F@ AÀáj 6 \rA”ßA”ß(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6  O\r ("AqE\r@@@@ AqE@A¨ß( F@A¨ß 6AœßAœß( j"6  Ar6 A¤ß(G\rA˜ßA6A¤ßA6 A¤ß(" F@A¤ß 6A˜ßA˜ß( j"6  Ar6 j 6 Axq j! ( ! AÿM@ (" F@AßAß(A~ Avwq6   6  6  (!  G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6   A~q6  Ar6 j 6  A! E\r@ ("At"(Àa F@ AÀáj 6 \rA”ßA”ß(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6  Ar6 j 6  G\rA˜ß 6 AÿM@ AøqA¸ßj!Aß("A Avt"qE@Aß r6   ( !  6 6  6  6 A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÀáj!@A”ß("A t"qE@A”ß  r6  6A!A  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6A! !A ! !   (" 6  6A!A!A !  j 6  6 j 6A°ßA°ß(Ak"A 6 ‰  E@ q A@O@AÐÝA06A A A jAxq A I! Ak"("Axq!@ AqE@ A€I\r Aj M@ !  kAðâ(AtM\r A   j!@  O@  k"AI\r   AqrAr6  j" Ar6  (Ar6  t  A¨ß( F@Aœß( j" M\r   AqrAr6  j"  k"Ar6Aœß 6A¨ß 6  A¤ß( F@A˜ß( j" I\r@  k"AO@   AqrAr6  j" Ar6  j" 6  (A~q6   Aq rAr6  j" (Ar6A!A! A¤ß 6A˜ß 6  ("Aq\r Axq j"\n I\r \n k! ( !@ AÿM@ (" F@AßAß(A~ Avwq6   6  6  (! @  G@ (" 6  6  @ (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  A! E\r@ ("At"(Àa F@ AÀáj 6 \rA”ßA”ß(A~ wq6  @  (F@ 6  6 E\r  6 ("@  6  6 ("E\r  6  6 AM@  Aq \nrAr6  \nj" (Ar6    AqrAr6  j" Ar6  \nj" (Ar6  t !  "@ Aj q"E@A  A|Ax Ak("Aq Axqj"   KV r  «  j!@@ ("Aq\r AqE\r (" j!@@@ k"A¤ß(G@ ( ! AÿM@  ("G\rAßAß(A~ Avwq6  (! G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  ("AqAG\rA˜ß 6  A~q6 Ar6  6  6  6  A! E\r@ ("At"(Àa F@ AÀáj 6 \rA”ßA”ß(A~ wq6  @ (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6 @@@@ ("AqE@A¨ß( F@A¨ß 6AœßAœß( j"6 Ar6 A¤ß(G\rA˜ßA6A¤ßA6 A¤ß(" F@A¤ß 6A˜ßA˜ß( j"6 Ar6 j 6 Axq j! ( ! AÿM@ (" F@AßAß(A~ Avwq6   6  6  (!  G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6   A~q6 Ar6 j 6  A! E\r@ ("At"(Àa F@ AÀáj 6 \rA”ßA”ß(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6 Ar6 j 6 G\rA˜ß 6 AÿM@ AøqA¸ßj!Aß("A Avt"qE@Aß  r6   ( !  6  6 6 6 A! AÿÿÿM@ A& Avg"kvAq AtrA>s! 6 B7 AtAÀáj!@@A”ß("A t"qE@A”ß  r6  6 6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6 6 6 6 (" 6  6 A6 6 6 W~@ ­B|BøÿÿÿƒAÄÜ("­|"BÿÿÿÿX@ §"?AtM\r \r AÐÝA06A AÄÜ 6  ² #Ak"$  6 @ AÓM@AàÑA Ó A jx(!  A|O@y  AÒn"AÒl"k6A ÓAàÔ Ajx"( j! A ÓkAu!@A!@@@ "A/F\r At(àQ"n" I\r Aj!  lG\r A/I\r AÓ!@ n" I\r  lF\r A\nj"n" I\r  lF\r A j"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r A$j"n" I\r  lF\r A(j"n" I\r  lF\r A*j"n" I\r  lF\r A.j"n" I\r  lF\r A4j"n" I\r  lF\r A:j"n" I\r  lF\r A`-_.-+_\\\\({[,"children":[NANINFcatching a class without an object?)}].!?,;mailto:,"content":h6h5h4h3h2h1http://Too many link reference definition instantiations.Unsupported abi_version.Aborted from text() callback.Aborted from enter_span() callback.Aborted from leave_span() callback.Aborted from enter_block() callback.Aborted from leave_block() callback.malloc() failed.realloc() failed.@:..!#$%&\'*+/=?^_`{|}~-_.:-+-.-_+-_*(null)Suppressing too sparse table (see https://github.com/mity/md4c/issues/345)._=().+-_~%false && "unhandled NodeType in nodeTypeToString"\\"{"type":"[]!xX \n™Jìi¿e£<‡ ¢°üOs{±2‡+F< ÂÎ._VÉšAÜ ~ô H, „*NSt3__220__shared_ptr_emplaceIN8Markdown15MarkdownASTNodeENS_9allocatorIS2_EEEE8™(A€ ä A  f\r0\r`\r\r\rÐ\r\rp\r\r€ Àà\r\r @ \r\r\r\rA UmŒA° !þ\n-Aà Á™ÅA 5¨]¸‹;OAÐ 5F¾\n&’îa=A MM J G D A > ¬¨Að `A€ •EA  ܘóAÀ \r”nAà ¼¨A€ \r’A  ±Ô$AÀ Eu,’Ø–«gYA  A  ”A° —  € @\n €/ _ 0A@Z€µÀ@րØ@ހ@.€2@6€9@G€J@v€xy@}€‚„†‡‰Š‹Ž‘“”–—˜œŸ @¤€¦§©¬®¯±²³µ·¸¼ÄÅÇÈÊË@Û€Þ@î€ñòôö÷ø@€ "@2€:;=>ACDEF@N€Eprv†ˆ@Š€ŒŽ‘@¡€£@«€ÂÏÐÑÕÖØ@î€ðñôõ÷ùúý@ÿ€@€@/€`@€€Š@¾€ÀÁ@Í€Ð@.€1@V€ @Å€ÇÍø@ý€€‚ƒ„…†‡ˆ‰@º€½@¿€@”€› @þ€@€@€(@/€8@?€H@M€Y[]_h@o€¸¹º»¾È@Ë€ØÙÚÛèéêëìøùúû&!*!+!2!`!@o!€ƒ!¶$@Ï$€,@/,€`,b,c,d,g,@k,€m,n,o,p,r,u,~,,€,@â,€ë,í,ò,@¦@l¦€€¦@š¦€"§@.§€2§@n§€y§{§}§~§@†§€‹§§§’§–§@¨§€ª§«§¬§­§®§°§±§²§³§´§@§€Ä§Å§Æ§Ç§É§Ë§Ì§@Ú§€Ü§Ý§â§õ§l«m«p«@¿«€!ÿ@:ÿ€@\'€°@Ó€p@z€|@Š€Œ@’€”•€ @² €P\r@e\r€ @¿€@n@_n€ n@¸n€@ßHßJßMßQßhß@n߀rß@~߀é@!é€az¼àöøþ/37:HKwÿz~sSƒ…TˆVWŒÝY[’`cih™oru¡¥€¨ƒ­ˆ°Š‹´¶’¹½ÆÆÉÉÌÌÜßïóóõ•¿ùž#3e,<šf,B€‰ŒGO¹qswó¬­¯ÌÍαÁÃËÃײ¸ÆÀÙïºÁ¸µøòû{}P_0Oa‹¿ÏÂÎÑ/a†-%-\'---ðõ24>ABBJcK¦ŠÐúýÿ•a¡ÿ \'07@EQSUW`g°±pq¹ruÐÑvwàáz{åxy|}ÉkåN!p!!„!Ð$é$0,_,a,k}}h,l,QqPRs,v,?@,ã,ì,î,ó,A¦m¦¦›¦#§/§3§o§z§|§y§‡§Œ§e‘§“§—§©§f\\aljž‡S«µ§Ã§”§‚Žȧʧdͧۧ›w|ö§K«L« ïAÿZÿ(OØû—¡£±³¹»¼À ò p\r…\rÀß`nn»nÓnAßIßKßNßRßißoßsßß"éCéAÐ2 Óß0Ið‡–—˜™šžP€@‡€ˆ@€@—€˜@Ÿ€ @§€¨@¯€²³´¶¼ÂÃÄÆÌÖäæòóôöüûûûûûûûûûû•ßA°4 ¥ssi¼nj e‚h1tw\ny\na¾ssʹ¹¹ ¹\'¹ ¹\'¹`¹g¹`¹g¹p¹±¹¬¹±B±¹t¹·¹®¹·B·¹¹BÁÅB|¹É¹Î¹ÉBɹfffiflststtvtetk~vtmssAà7 À°RTV·ÇÒÓ×âãç÷ûû¹ÅÅÅÅB±B¹·B¹¹¹¹BÅÅÅBÉB¹ffifflА.P05à  A´: ½!@/€:@@€[@`€{@~€¡@©€«@¬€®@±€´¶@¸€»¿×÷Â@Å€Ò@߀å@ë€íï@ÿ€u~„@…€‡ö‚Z@_€‰@Š€@€¾ÀÃÆó@ô€@€@€j@m€ÔÞéý@þ€@\r€ö@ù€þ@ÿ€0@>€^ˆd @e €p ò @ó €ú @û €ý v\nð\n@ñ\n€p ó @ú €w  „ O\ry\rô\r?OZ@[€@€@€468:@=€…¾@Å€Ç@Ì€Î@Ú€J@O€ž@Ÿ€û`@h€@™€m@n€›@œ€ë@í€5@6€Ô@Ö€Ø@Û€@\n€@D@E€Þ@ÿ€@€ @¦€¨@­€N@O€Z@j€t@€ü@ÿ€;@?€~@€À@Ç€Ó½¿@Á€Í@Ï€Ý@߀í@ï€ý@þ€ @\' €0 @^ €z @~ €Š @Ž €  @Ä €!@!€!@!€!@ !€!!@!€!@#!€%!\'!)!.!:!@;!€@!@D!€J!@M!€O!Š!@‹!€!@)$€@$@J$€œ$@é$€%@u\'€”\'@s+€v+@ÿ+€å,@ê,€ù,@ü,€þ,@ÿ,€p-.@..€0.@].€`.@c.€€.@™.€›.@ó.€/@Õ/€ð/@ÿ/€0@0€0@ 0€0060@70€=0@?0€›0@œ0€ 0û01@‘1€–1@Ÿ1€À1@å1€ï12@2€*2@G2€P2`2@2€Š2@°2€À2@ÿ3€ÀM@ÿM€¤@Ƥ€þ¤@ÿ¤€\r¦@¦€s¦~¦ò¦@÷¦€§@§€ §@!§€‰§@Ч€(¨@+¨€6¨@9¨€t¨@w¨€Î¨@Ϩ€ø¨@ú¨€ü¨.©@/©€_©Á©@Í©€Þ©@ß©€\\ª@_ª€wª@yª€Þª@ߪ€ðª@ñª€[«j«@k«€ë«)û²û@Òû€>ý@Oý€ý@‘ý€Èý@Ïý€üý@ÿý€þ@þ€0þ@Rþ€Tþ@fþ€hþ@kþ€ÿ@ÿ€ÿ@ ÿ€;ÿ@@ÿ€[ÿ@eÿ€àÿ@æÿ€èÿ@îÿ€üÿ@ýÿ€@€7@?€y@‰€Œ@Ž€@œ€ Ð@ü€ŸÐoWw@x€ ? P\n@X\n€\nÈ\nð\n@ö\n€9 @? €™ @œ €n\rŽ\r@\r€­É@Ê€Ð@Ø€U@Y€†@‰€G@M€»@¼€¾@Á€@@C€t@u€Å@È€ÍÛÝ@߀8@=€©Ô@Õ€×@Ø€K@O€Z@[€]ÆÁ@×€A@C€`@l€¹<@?€;D@F€â?@F€š@œ€ž@¢€@ €áA@E€p@q€÷@ø€C@O€Õ@ñ€ÿp$@t$€ñ/@ò/€nj@oj€õj7k@?k€Dk@Ek€mm@om€—n@šn€âoœ¼Ÿ¼Ì@ïÌ€úÌ@üÌ€Í@³Î€ºÎ@Ð΀ÒÎ@Ô΀ÝÎ@ý΀PÏ@ÃÏ€Ð@õЀÑ@&Ñ€)Ñ@dÑ€jÑ@lÑ€ƒÑ@„Ñ€ŒÑ@©Ñ€®Ñ@AÒ€EÒSÒ@ZÒ€]Ò@^Ò€`Ò@Ò€Ó@VÓ€ÁÖÛÖûÖ×5×O×o׉ש×Ãר@ÿÙ€7Ú@:Ú€mÚ@tÚ€vÚ@ƒÚ€…Ú@‹Ú€Û@Û€Oáÿâÿå^é@_逬ì°ì.íðî@ñî€ð@+ð€0ð@“ð€ ð@®ð€±ð@¿ð€Áð@Ïð€Ñð@õð€\rñ@®ñ€æñ@ò€ò@;ò€@ò@Hò€Pò@Qò€`ò@eò€ó@Ùö€Üö@ìö€ðö@üö€÷@Û÷€à÷@ë÷€ð÷@ ø€ø@Gø€Pø@Yø€`ø@‡ø€ø@­ø€°ø@»ø€Àø@Áø€Ðø@Øø€ù@Wú€`ú@mú€pú@|ú€€ú@Æú€ÈúÌú@Ýú€ßú@ëú€ïú@úú€û@’û€”û@ïû€úû A€Î A  \n\n\n   AÑÎ ! \r\r  A‹Ï  A—Ï  AÅÏ AÑÏ  AÿÏ A‹Ð  AÂÐ  AóÐ AÿÐ  A­Ñ A¹Ñ € 0123456789ABCDEF \r%)+/5;=CGIOSYaegkmqƒ‰‹•—£§­³µ¿ÁÅÇÓ \r%)+/5;=CGIOSYaegkmqyƒ‰‹•—£§©­³µ»¿ÁÅÇÑ ,h*NSt3__214__shared_countE¤,œ*`*NSt3__219__shared_weak_countEAÄÕ ø\ndè\' †@B€–˜áõʚ;00010203040506070809101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899H,Ä+(.N10__cxxabiv116__shim_type_infoEH,ô+¸+N10__cxxabiv117__class_type_infoEè+ !"#$%&h,\'!"#()*H,t,è+N10__cxxabiv120__si_class_type_infoEÄ,+!"#,-.H,Ð,è+N10__cxxabiv121__vmi_class_type_infoEL-/0h-124-34 ,<-St9exceptionH,X-4-St9bad_allocH,t-L-St20bad_array_new_length¤-56H,°-4-St11logic_errorÔ-76H,à-¤-St12length_error.86H,.¤-St12out_of_range ,0.St9type_infoAÁÜ  1€'); + } + function getBinarySync(file) { + return file; +@@ -117,7 +118,7 @@ var createMd4cModule = (() => { + return imports; + } + async function createWasm() { +- function receiveInstance(instance) { ++ function receiveInstance(instance, module) { + wasmExports = instance.exports; + assignWasmExports(wasmExports); + updateMemoryViews(); +@@ -127,10 +128,11 @@ var createMd4cModule = (() => { + return receiveInstance(result["instance"]); + } + var info = getWasmImports(); +- var instantiateWasm = Module["instantiateWasm"]; +- if (instantiateWasm) { +- return new Promise(resolve => { +- instantiateWasm(info, inst => resolve(receiveInstance(inst))); ++ if (Module["instantiateWasm"]) { ++ return new Promise((resolve, reject) => { ++ Module["instantiateWasm"](info, (inst, mod) => { ++ resolve(receiveInstance(inst, mod)); ++ }); + }); + } + wasmBinaryFile ??= findWasmBinary(); +@@ -145,23 +147,15 @@ var createMd4cModule = (() => { + this.status = status; + } + } +- var HEAP16; +- var HEAP32; +- var HEAP64; +- var HEAP8; +- var HEAPF32; +- var HEAPF64; +- var HEAPU16; +- var HEAPU32; +- var HEAPU64; +- var HEAPU8; + var callRuntimeCallbacks = callbacks => { + while (callbacks.length > 0) { + callbacks.shift()(Module); + } + }; + var onPostRuns = []; ++ var addOnPostRun = cb => onPostRuns.push(cb); + var onPreRuns = []; ++ var addOnPreRun = cb => onPreRuns.push(cb); + var noExitRuntime = true; + var stackRestore = val => __emscripten_stack_restore(val); + var stackSave = () => _emscripten_stack_get_current(); +@@ -249,12 +243,14 @@ var createMd4cModule = (() => { + return HEAPU32[this.ptr + 16 >> 2]; + } + } ++ var exceptionLast = 0; + var uncaughtExceptionCount = 0; + var ___cxa_throw = (ptr, type, destructor) => { + var info = new ExceptionInfo(ptr); + info.init(type, destructor); ++ exceptionLast = ptr; + uncaughtExceptionCount++; +- abort(); ++ throw exceptionLast; + }; + var __abort_js = () => abort(""); + var getHeapMax = () => 536870912; +@@ -400,17 +396,24 @@ var createMd4cModule = (() => { + } + return (...args) => ccall(ident, returnType, argTypes, args, opts); + }; ++ for (var base64ReverseLookup = new Uint8Array(123), i = 25; i >= 0; --i) { ++ base64ReverseLookup[48 + i] = 52 + i; ++ base64ReverseLookup[65 + i] = i; ++ base64ReverseLookup[97 + i] = 26 + i; ++ } ++ base64ReverseLookup[43] = 62; ++ base64ReverseLookup[47] = 63; + { + if (Module["noExitRuntime"]) noExitRuntime = Module["noExitRuntime"]; + if (Module["print"]) out = Module["print"]; + if (Module["printErr"]) err = Module["printErr"]; +- if (Module["arguments"]) programArgs = Module["arguments"]; ++ if (Module["wasmBinary"]) wasmBinary = Module["wasmBinary"]; ++ if (Module["arguments"]) arguments_ = Module["arguments"]; + if (Module["thisProgram"]) thisProgram = Module["thisProgram"]; +- var preInit = Module["preInit"]; +- if (preInit) { +- if (typeof preInit == "function") Module["preInit"] = preInit = [preInit]; +- while (preInit.length > 0) { +- preInit.shift()(); ++ if (Module["preInit"]) { ++ if (typeof Module["preInit"] == "function") Module["preInit"] = [Module["preInit"]]; ++ while (Module["preInit"].length > 0) { ++ Module["preInit"].shift()(); + } + } + } +@@ -432,24 +435,39 @@ var createMd4cModule = (() => { + _abort_js: __abort_js, + emscripten_resize_heap: _emscripten_resize_heap + }; +- async function run() { ++ function run() { + preRun(); +- var setStatus = Module["setStatus"]; +- if (setStatus) { +- setStatus("Running..."); +- await new Promise(resolve => setTimeout(resolve, 1)); +- setTimeout(setStatus, 1, ""); +- } +- if (ABORT) return; +- initRuntime(); +- Module["onRuntimeInitialized"]?.(); +- postRun(); ++ function doRun() { ++ Module["calledRun"] = true; ++ if (ABORT) return; ++ initRuntime(); ++ readyPromiseResolve?.(Module); ++ Module["onRuntimeInitialized"]?.(); ++ postRun(); ++ } ++ if (Module["setStatus"]) { ++ Module["setStatus"]("Running..."); ++ setTimeout(() => { ++ setTimeout(() => Module["setStatus"](""), 1); ++ doRun(); ++ }, 1); ++ } else { ++ doRun(); ++ } + } + var wasmExports; + wasmExports = await createWasm(); +- await run(); ++ run(); ++ if (runtimeInitialized) { ++ moduleRtn = Module; ++ } else { ++ moduleRtn = new Promise((resolve, reject) => { ++ readyPromiseResolve = resolve; ++ readyPromiseReject = reject; ++ }); ++ } + ; +- return Module; ++ return moduleRtn; + }; + })(); + if (typeof exports === "object" && typeof module === "object") { +diff --git a/node_modules/react-native-enriched-markdown/lib/module/web/wasm/md4c.js.map b/node_modules/react-native-enriched-markdown/lib/module/web/wasm/md4c.js.map +index 75b509c..ecbb5e7 100644 +--- a/node_modules/react-native-enriched-markdown/lib/module/web/wasm/md4c.js.map ++++ b/node_modules/react-native-enriched-markdown/lib/module/web/wasm/md4c.js.map +@@ -1 +1 @@ +-{"version":3,"names":["createMd4cModule","_scriptName","globalThis","document","currentScript","src","moduleArg","Module","ENVIRONMENT_IS_WEB","ENVIRONMENT_IS_WORKER","programArgs","thisProgram","scriptDirectory","readAsync","URL","href","url","response","fetch","credentials","ok","arrayBuffer","Error","status","out","console","log","bind","err","error","wasmBinary","ABORT","EmscriptenEH","EmscriptenSjLj","binaryDecode","bin","i","l","length","o","Uint8Array","c","charCodeAt","runtimeInitialized","getMemoryBuffer","wasmMemory","buffer","updateMemoryViews","HEAP8","resizable","b","Int8Array","HEAP16","Int16Array","HEAPU8","HEAPU16","Uint16Array","HEAP32","Int32Array","HEAPU32","Uint32Array","HEAPF32","Float32Array","HEAPF64","Float64Array","HEAP64","BigInt64Array","HEAPU64","BigUint64Array","preRun","onPreRuns","push","callRuntimeCallbacks","initRuntime","wasmExports","postRun","onPostRuns","abort","what","e","WebAssembly","RuntimeError","wasmBinaryFile","findWasmBinary","getBinarySync","file","getWasmBinary","binaryFile","instantiateArrayBuffer","imports","binary","instance","instantiate","reason","instantiateAsync","getWasmImports","env","wasmImports","wasi_snapshot_preview1","createWasm","receiveInstance","exports","assignWasmExports","receiveInstantiationResult","result","info","instantiateWasm","Promise","resolve","inst","ExitStatus","name","constructor","message","callbacks","shift","noExitRuntime","stackRestore","val","__emscripten_stack_restore","stackSave","_emscripten_stack_get_current","UTF8Decoder","TextDecoder","findStringEnd","heapOrArray","idx","maxBytesToRead","ignoreNul","maxIdx","UTF8ArrayToString","endPtr","decode","subarray","str","u0","String","fromCharCode","u1","u2","ch","UTF8ToString","ptr","___assert_fail","condition","filename","line","func","ExceptionInfo","excPtr","set_type","type","get_type","set_destructor","destructor","get_destructor","set_caught","caught","get_caught","set_rethrown","rethrown","get_rethrown","init","set_adjusted_ptr","adjustedPtr","get_adjusted_ptr","uncaughtExceptionCount","___cxa_throw","__abort_js","getHeapMax","alignMemory","size","alignment","Math","ceil","growMemory","oldHeapSize","byteLength","pages","grow","_emscripten_resize_heap","requestedSize","oldSize","maxHeapSize","cutDown","overGrownHeapSize","min","newSize","max","replacement","getCFunc","ident","writeArrayToMemory","array","set","lengthBytesUTF8","len","stringToUTF8Array","heap","outIdx","maxBytesToWrite","startIdx","endIdx","u","codePointAt","stringToUTF8","outPtr","stackAlloc","sz","__emscripten_stack_alloc","stringToUTF8OnStack","ret","ccall","returnType","argTypes","args","opts","toC","string","undefined","arr","convertReturnValue","Boolean","cArgs","stack","converter","onDone","cwrap","numericArgs","every","numericRet","preInit","_parseMarkdown","memory","__indirect_function_table","__assert_fail","__cxa_throw","_abort_js","emscripten_resize_heap","run","setStatus","setTimeout","module","default","define"],"sourceRoot":"../../../../src","sources":["web/wasm/md4c.js"],"mappings":"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","ignoreList":[]} ++{"version":3,"names":["createMd4cModule","_scriptName","globalThis","document","currentScript","src","moduleArg","moduleRtn","Module","ENVIRONMENT_IS_WEB","ENVIRONMENT_IS_WORKER","arguments_","thisProgram","scriptDirectory","readAsync","URL","href","url","response","fetch","credentials","ok","arrayBuffer","Error","status","out","console","log","bind","err","error","wasmBinary","ABORT","binaryDecode","bin","i","l","length","o","Uint8Array","c","charCodeAt","readyPromiseResolve","readyPromiseReject","HEAP8","HEAPU8","HEAP16","HEAPU16","HEAP32","HEAPU32","HEAPF32","HEAPF64","HEAP64","HEAPU64","runtimeInitialized","updateMemoryViews","b","wasmMemory","buffer","Int8Array","Int16Array","Uint16Array","Int32Array","Uint32Array","Float32Array","Float64Array","BigInt64Array","BigUint64Array","preRun","addOnPreRun","shift","callRuntimeCallbacks","onPreRuns","initRuntime","wasmExports","postRun","addOnPostRun","onPostRuns","abort","what","e","WebAssembly","RuntimeError","wasmBinaryFile","findWasmBinary","getBinarySync","file","getWasmBinary","binaryFile","instantiateArrayBuffer","imports","binary","instance","instantiate","reason","instantiateAsync","getWasmImports","env","wasmImports","wasi_snapshot_preview1","createWasm","receiveInstance","module","exports","assignWasmExports","receiveInstantiationResult","result","info","Promise","resolve","reject","inst","mod","ExitStatus","name","constructor","message","callbacks","cb","push","noExitRuntime","stackRestore","val","__emscripten_stack_restore","stackSave","_emscripten_stack_get_current","UTF8Decoder","TextDecoder","findStringEnd","heapOrArray","idx","maxBytesToRead","ignoreNul","maxIdx","UTF8ArrayToString","endPtr","decode","subarray","str","u0","String","fromCharCode","u1","u2","ch","UTF8ToString","ptr","___assert_fail","condition","filename","line","func","ExceptionInfo","excPtr","set_type","type","get_type","set_destructor","destructor","get_destructor","set_caught","caught","get_caught","set_rethrown","rethrown","get_rethrown","init","set_adjusted_ptr","adjustedPtr","get_adjusted_ptr","exceptionLast","uncaughtExceptionCount","___cxa_throw","__abort_js","getHeapMax","alignMemory","size","alignment","Math","ceil","growMemory","oldHeapSize","byteLength","pages","grow","_emscripten_resize_heap","requestedSize","oldSize","maxHeapSize","cutDown","overGrownHeapSize","min","newSize","max","replacement","getCFunc","ident","writeArrayToMemory","array","set","lengthBytesUTF8","len","stringToUTF8Array","heap","outIdx","maxBytesToWrite","startIdx","endIdx","u","codePointAt","stringToUTF8","outPtr","stackAlloc","sz","__emscripten_stack_alloc","stringToUTF8OnStack","ret","ccall","returnType","argTypes","args","opts","toC","string","undefined","arr","convertReturnValue","Boolean","cArgs","stack","converter","onDone","cwrap","numericArgs","every","numericRet","base64ReverseLookup","_parseMarkdown","memory","__indirect_function_table","__assert_fail","__cxa_throw","_abort_js","emscripten_resize_heap","run","doRun","setTimeout","default","define"],"sourceRoot":"../../../../src","sources":["web/wasm/md4c.js"],"mappings":"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","ignoreList":[]} +diff --git a/node_modules/react-native-enriched-markdown/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts b/node_modules/react-native-enriched-markdown/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +index 1ee4eda..b415d18 100644 +--- a/node_modules/react-native-enriched-markdown/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts ++++ b/node_modules/react-native-enriched-markdown/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +@@ -235,6 +235,7 @@ export interface MarkdownStyleInternal { + } + export interface LinkPressEvent { + url: string; ++ sourceType: string; + } + export interface LinkLongPressEvent { + url: string; +diff --git a/node_modules/react-native-enriched-markdown/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map b/node_modules/react-native-enriched-markdown/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +index 8e380e9..5f342bd 100644 +--- a/node_modules/react-native-enriched-markdown/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map ++++ b/node_modules/react-native-enriched-markdown/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +@@ -1 +1 @@ +-{"version":3,"file":"EnrichedMarkdownNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/EnrichedMarkdownNativeComponent.ts"],"names":[],"mappings":"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"} +\ No newline at end of file ++{"version":3,"file":"EnrichedMarkdownNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/EnrichedMarkdownNativeComponent.ts"],"names":[],"mappings":"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"} +\ No newline at end of file +diff --git a/node_modules/react-native-enriched-markdown/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts b/node_modules/react-native-enriched-markdown/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +index bcde9a7..247d757 100644 +--- a/node_modules/react-native-enriched-markdown/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts ++++ b/node_modules/react-native-enriched-markdown/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +@@ -235,6 +235,7 @@ export interface MarkdownStyleInternal { + } + export interface LinkPressEvent { + url: string; ++ sourceType: string; + } + export interface LinkLongPressEvent { + url: string; +diff --git a/node_modules/react-native-enriched-markdown/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map b/node_modules/react-native-enriched-markdown/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +index b4a4797..29bab7c 100644 +--- a/node_modules/react-native-enriched-markdown/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map ++++ b/node_modules/react-native-enriched-markdown/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +@@ -1 +1 @@ +-{"version":3,"file":"EnrichedMarkdownTextNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/EnrichedMarkdownTextNativeComponent.ts"],"names":[],"mappings":"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"} +\ No newline at end of file ++{"version":3,"file":"EnrichedMarkdownTextNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/EnrichedMarkdownTextNativeComponent.ts"],"names":[],"mappings":"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"} +\ No newline at end of file +diff --git a/node_modules/react-native-enriched-markdown/lib/typescript/src/index.d.ts.map b/node_modules/react-native-enriched-markdown/lib/typescript/src/index.d.ts.map +index aaeab3f..6c2cbe8 100644 +--- a/node_modules/react-native-enriched-markdown/lib/typescript/src/index.d.ts.map ++++ b/node_modules/react-native-enriched-markdown/lib/typescript/src/index.d.ts.map +@@ -1 +1 @@ +-{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC7E,YAAY,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AACjE,YAAY,EACV,mBAAmB,EACnB,eAAe,GAChB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,OAAO,IAAI,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAChF,YAAY,EACV,yBAAyB,EACzB,eAAe,EACf,aAAa,EACb,SAAS,EACT,eAAe,IAAI,mBAAmB,EACtC,mBAAmB,IAAI,uBAAuB,EAC9C,yBAAyB,IAAI,6BAA6B,GAC3D,MAAM,+BAA+B,CAAC;AACvC,YAAY,EACV,cAAc,EACd,kBAAkB,EAClB,eAAe,EACf,sBAAsB,EACtB,cAAc,EACd,eAAe,EACf,mBAAmB,GACpB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,mBAAmB,EACnB,2BAA2B,GAC5B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,4BAA4B,EAC5B,0BAA0B,GAC3B,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAC;AACxE,YAAY,EACV,8BAA8B,EAC9B,iCAAiC,EACjC,sBAAsB,EACtB,UAAU,EACV,eAAe,EACf,wBAAwB,EACxB,gBAAgB,EAChB,uBAAuB,EACvB,eAAe,EACf,cAAc,EACd,mBAAmB,EACnB,oBAAoB,EACpB,iBAAiB,EACjB,SAAS,EACT,YAAY,EACZ,YAAY,GACb,MAAM,6BAA6B,CAAC"} +\ No newline at end of file ++{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA,YAAY,EACV,iBAAiB,EACjB,sBAAsB,GACvB,MAAM,2BAA2B,CAAC;AACnC,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC7E,YAAY,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AACjE,YAAY,EACV,mBAAmB,EACnB,eAAe,GAChB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,OAAO,IAAI,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAChF,YAAY,EACV,yBAAyB,EACzB,eAAe,EACf,aAAa,EACb,SAAS,EACT,eAAe,IAAI,mBAAmB,EACtC,mBAAmB,IAAI,uBAAuB,EAC9C,yBAAyB,IAAI,6BAA6B,GAC3D,MAAM,+BAA+B,CAAC;AACvC,YAAY,EACV,cAAc,EACd,kBAAkB,EAClB,eAAe,EACf,sBAAsB,EACtB,cAAc,EACd,eAAe,EACf,mBAAmB,GACpB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,mBAAmB,EACnB,2BAA2B,GAC5B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,4BAA4B,EAC5B,0BAA0B,GAC3B,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAC;AACxE,YAAY,EACV,8BAA8B,EAC9B,iCAAiC,EACjC,sBAAsB,EACtB,UAAU,EACV,eAAe,EACf,wBAAwB,EACxB,gBAAgB,EAChB,uBAAuB,EACvB,eAAe,EACf,cAAc,EACd,mBAAmB,EACnB,oBAAoB,EACpB,iBAAiB,EACjB,SAAS,EACT,YAAY,EACZ,YAAY,GACb,MAAM,6BAA6B,CAAC"} +\ No newline at end of file +diff --git a/node_modules/react-native-enriched-markdown/lib/typescript/src/index.web.d.ts.map b/node_modules/react-native-enriched-markdown/lib/typescript/src/index.web.d.ts.map +index ad425ad..c3403c8 100644 +--- a/node_modules/react-native-enriched-markdown/lib/typescript/src/index.web.d.ts.map ++++ b/node_modules/react-native-enriched-markdown/lib/typescript/src/index.web.d.ts.map +@@ -1 +1 @@ +-{"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../../../src/index.web.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAC3E,YAAY,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAC;AAC/E,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACtE,YAAY,EACV,cAAc,EACd,kBAAkB,EAClB,eAAe,EACf,sBAAsB,EACtB,cAAc,EACd,eAAe,EACf,mBAAmB,GACpB,MAAM,gBAAgB,CAAC"} +\ No newline at end of file ++{"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../../../src/index.web.tsx"],"names":[],"mappings":"AAAA,YAAY,EACV,iBAAiB,EACjB,sBAAsB,GACvB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,oBAAoB,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAC3E,YAAY,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAC;AAC/E,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACtE,YAAY,EACV,cAAc,EACd,kBAAkB,EAClB,eAAe,EACf,sBAAsB,EACtB,cAAc,EACd,eAAe,EACf,mBAAmB,GACpB,MAAM,gBAAgB,CAAC"} +\ No newline at end of file +diff --git a/node_modules/react-native-enriched-markdown/lib/typescript/src/native/EnrichedMarkdownText.d.ts b/node_modules/react-native-enriched-markdown/lib/typescript/src/native/EnrichedMarkdownText.d.ts +index 3835048..1fcf936 100644 +--- a/node_modules/react-native-enriched-markdown/lib/typescript/src/native/EnrichedMarkdownText.d.ts ++++ b/node_modules/react-native-enriched-markdown/lib/typescript/src/native/EnrichedMarkdownText.d.ts +@@ -4,6 +4,6 @@ import type { LinkPressEvent, LinkLongPressEvent, ImagePressEvent, TaskListItemP + export type { MarkdownStyle, Md4cFlags }; + export type { EnrichedMarkdownTextProps, StreamingConfig, ContextMenuItem, SelectionMenuConfig, SelectionMenuPluralLabels, }; + export type { LinkPressEvent, LinkLongPressEvent, ImagePressEvent, TaskListItemPressEvent, CopyPressEvent, LatexErrorEvent, CodeBlockPressEvent, }; +-export declare const EnrichedMarkdownText: ({ markdown, markdownStyle, containerStyle, onLinkPress, onLinkLongPress, onImagePress, onTaskListItemPress, enableTaskListItemToggle, onCopyPress, onLatexError, onCodeBlockPress, enableBlockContextMenu, enableLinkPreview, selectable, md4cFlags, allowFontScaling, maxFontSizeMultiplier, allowTrailingMargin, flavor, streamingAnimation, streamingConfig, spoilerOverlay, contextMenuItems, linkContextMenuItems, imageRequestHeaders, linkPillContent, linkRecognition, selectionMenuConfig, accessibilityLabels, selectionColor, selectionHandleColor, textBreakStrategy, lineBreakStrategyIOS, writingDirection, numberOfLines, ellipsizeMode, ...rest }: EnrichedMarkdownTextProps) => import("react/jsx-runtime").JSX.Element; ++export declare const EnrichedMarkdownText: ({ markdown, markdownStyle, containerStyle, onLinkPress, onLinkLongPress, onImagePress, onTaskListItemPress, enableTaskListItemToggle, onCopyPress, onLatexError, onCodeBlockPress, enableBlockContextMenu, enableLinkPreview, selectable, md4cFlags, allowFontScaling, maxFontSizeMultiplier, allowTrailingMargin, flavor, streamingAnimation, streamingConfig, spoilerOverlay, contextMenuItems, linkContextMenuItems, imageRequestHeaders, linkPillContent, linkRecognition, selectionMenuConfig, accessibilityLabels, selectionColor, selectionHandleColor, textBreakStrategy, lineBreakStrategyIOS, writingDirection, numberOfLines, ellipsizeMode, renderers: _renderers, ...rest }: EnrichedMarkdownTextProps) => import("react/jsx-runtime").JSX.Element; + export default EnrichedMarkdownText; + //# sourceMappingURL=EnrichedMarkdownText.d.ts.map +\ No newline at end of file +diff --git a/node_modules/react-native-enriched-markdown/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map b/node_modules/react-native-enriched-markdown/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map +index 343249e..5ee0474 100644 +--- a/node_modules/react-native-enriched-markdown/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map ++++ b/node_modules/react-native-enriched-markdown/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map +@@ -1 +1 @@ +-{"version":3,"file":"EnrichedMarkdownText.d.ts","sourceRoot":"","sources":["../../../../src/native/EnrichedMarkdownText.tsx"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACvE,OAAO,KAAK,EACV,yBAAyB,EACzB,eAAe,EACf,eAAe,EACf,mBAAmB,EACnB,yBAAyB,EAC1B,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EACV,cAAc,EACd,kBAAkB,EAClB,eAAe,EACf,sBAAsB,EACtB,cAAc,EACd,eAAe,EACf,mBAAmB,EAGpB,MAAM,iBAAiB,CAAC;AAEzB,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,CAAC;AACzC,YAAY,EACV,yBAAyB,EACzB,eAAe,EACf,eAAe,EACf,mBAAmB,EACnB,yBAAyB,GAC1B,CAAC;AACF,YAAY,EACV,cAAc,EACd,kBAAkB,EAClB,eAAe,EACf,sBAAsB,EACtB,cAAc,EACd,eAAe,EACf,mBAAmB,GACpB,CAAC;AA0EF,eAAO,MAAM,oBAAoB,GAAI,moBAsClC,yBAAyB,4CAgS3B,CAAC;AAEF,eAAe,oBAAoB,CAAC"} +\ No newline at end of file ++{"version":3,"file":"EnrichedMarkdownText.d.ts","sourceRoot":"","sources":["../../../../src/native/EnrichedMarkdownText.tsx"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACvE,OAAO,KAAK,EACV,yBAAyB,EACzB,eAAe,EACf,eAAe,EACf,mBAAmB,EACnB,yBAAyB,EAC1B,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EACV,cAAc,EACd,kBAAkB,EAClB,eAAe,EACf,sBAAsB,EACtB,cAAc,EACd,eAAe,EACf,mBAAmB,EAGpB,MAAM,iBAAiB,CAAC;AAEzB,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,CAAC;AACzC,YAAY,EACV,yBAAyB,EACzB,eAAe,EACf,eAAe,EACf,mBAAmB,EACnB,yBAAyB,GAC1B,CAAC;AACF,YAAY,EACV,cAAc,EACd,kBAAkB,EAClB,eAAe,EACf,sBAAsB,EACtB,cAAc,EACd,eAAe,EACf,mBAAmB,GACpB,CAAC;AA0EF,eAAO,MAAM,oBAAoB,GAAI,0pBAuClC,yBAAyB,4CAmS3B,CAAC;AAEF,eAAe,oBAAoB,CAAC"} +\ No newline at end of file +diff --git a/node_modules/react-native-enriched-markdown/lib/typescript/src/types/MarkdownTextProps.d.ts b/node_modules/react-native-enriched-markdown/lib/typescript/src/types/MarkdownTextProps.d.ts +index afc9af3..f2bae28 100644 +--- a/node_modules/react-native-enriched-markdown/lib/typescript/src/types/MarkdownTextProps.d.ts ++++ b/node_modules/react-native-enriched-markdown/lib/typescript/src/types/MarkdownTextProps.d.ts +@@ -1,3 +1,4 @@ ++import type { MarkdownRenderers } from './MarkdownRenderers'; + import type { ColorValue, ViewProps, ViewStyle, TextStyle } from 'react-native'; + import type { MarkdownStyle, Md4cFlags } from './MarkdownStyle'; + import type { AccessibilityLabels } from './AccessibilityLabels'; +@@ -146,6 +147,8 @@ export interface EnrichedMarkdownTextProps extends Omit { + * @platform ios, android, web + */ + markdown: string; ++ /** Custom block components. Ignored on native. @platform web */ ++ renderers?: MarkdownRenderers; + /** + * Style configuration for markdown elements. + * @platform ios, android, web +diff --git a/node_modules/react-native-enriched-markdown/lib/typescript/src/types/MarkdownTextProps.d.ts.map b/node_modules/react-native-enriched-markdown/lib/typescript/src/types/MarkdownTextProps.d.ts.map +index fb1e2eb..e80d931 100644 +--- a/node_modules/react-native-enriched-markdown/lib/typescript/src/types/MarkdownTextProps.d.ts.map ++++ b/node_modules/react-native-enriched-markdown/lib/typescript/src/types/MarkdownTextProps.d.ts.map +@@ -1 +1 @@ +-{"version":3,"file":"MarkdownTextProps.d.ts","sourceRoot":"","sources":["../../../../src/types/MarkdownTextProps.ts"],"names":[],"mappings":"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"} +\ No newline at end of file ++{"version":3,"file":"MarkdownTextProps.d.ts","sourceRoot":"","sources":["../../../../src/types/MarkdownTextProps.ts"],"names":[],"mappings":"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"} +\ No newline at end of file +diff --git a/node_modules/react-native-enriched-markdown/lib/typescript/src/types/MarkdownTextProps.web.d.ts b/node_modules/react-native-enriched-markdown/lib/typescript/src/types/MarkdownTextProps.web.d.ts +index 19b0d0d..7537e44 100644 +--- a/node_modules/react-native-enriched-markdown/lib/typescript/src/types/MarkdownTextProps.web.d.ts ++++ b/node_modules/react-native-enriched-markdown/lib/typescript/src/types/MarkdownTextProps.web.d.ts +@@ -1,3 +1,4 @@ ++import type { MarkdownRenderers } from './MarkdownRenderers'; + import type { LinkRecognition } from './MarkdownTextProps'; + import type { CSSProperties, HTMLAttributes } from 'react'; + import type { MarkdownStyle, Md4cFlags } from './MarkdownStyle'; +@@ -13,6 +14,8 @@ export interface EnrichedMarkdownTextProps extends Omit import("react/jsx-runtime").JSX.Element | null; ++export declare const EnrichedMarkdownText: ({ markdown, markdownStyle, md4cFlags, linkRecognition, renderers, onLinkPress, onLinkLongPress, onImagePress, onTaskListItemPress, onCodeBlockPress, enableTaskListItemToggle, allowTrailingMargin, containerStyle, selectable, dir, selectionColor, testID, ...rest }: EnrichedMarkdownTextProps) => import("react/jsx-runtime").JSX.Element; + export default EnrichedMarkdownText; + //# sourceMappingURL=EnrichedMarkdownText.d.ts.map +\ No newline at end of file +diff --git a/node_modules/react-native-enriched-markdown/lib/typescript/src/web/EnrichedMarkdownText.d.ts.map b/node_modules/react-native-enriched-markdown/lib/typescript/src/web/EnrichedMarkdownText.d.ts.map +index 365eb97..b357421 100644 +--- a/node_modules/react-native-enriched-markdown/lib/typescript/src/web/EnrichedMarkdownText.d.ts.map ++++ b/node_modules/react-native-enriched-markdown/lib/typescript/src/web/EnrichedMarkdownText.d.ts.map +@@ -1 +1 @@ +-{"version":3,"file":"EnrichedMarkdownText.d.ts","sourceRoot":"","sources":["../../../../src/web/EnrichedMarkdownText.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAiBhF,eAAO,MAAM,oBAAoB,GAAI,6PAkBlC,yBAAyB,mDAqL3B,CAAC;AAEF,eAAe,oBAAoB,CAAC"} +\ No newline at end of file ++{"version":3,"file":"EnrichedMarkdownText.d.ts","sourceRoot":"","sources":["../../../../src/web/EnrichedMarkdownText.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAiBhF,eAAO,MAAM,oBAAoB,GAAI,wQAmBlC,yBAAyB,4CAyO3B,CAAC;AAEF,eAAe,oBAAoB,CAAC"} +\ No newline at end of file +diff --git a/node_modules/react-native-enriched-markdown/lib/typescript/src/web/parseMarkdown.d.ts b/node_modules/react-native-enriched-markdown/lib/typescript/src/web/parseMarkdown.d.ts +index 890724b..ff89213 100644 +--- a/node_modules/react-native-enriched-markdown/lib/typescript/src/web/parseMarkdown.d.ts ++++ b/node_modules/react-native-enriched-markdown/lib/typescript/src/web/parseMarkdown.d.ts +@@ -1,4 +1,5 @@ + import type { ASTNode } from './types'; + import type { Md4cFlags } from '../types/MarkdownStyle'; +-export declare function parseMarkdown(markdown: string, { underline, latexMath, superscript, subscript, highlight, hardSoftBreaks, preserveBlankLines, admonitions, }?: Md4cFlags): Promise; ++export declare function parseMarkdownSync(markdown: string, { underline, latexMath, superscript, subscript, highlight, hardSoftBreaks, preserveBlankLines, admonitions, }?: Md4cFlags): ASTNode | null; ++export declare function parseMarkdown(markdown: string, flags?: Md4cFlags): Promise; + //# sourceMappingURL=parseMarkdown.d.ts.map +\ No newline at end of file +diff --git a/node_modules/react-native-enriched-markdown/lib/typescript/src/web/parseMarkdown.d.ts.map b/node_modules/react-native-enriched-markdown/lib/typescript/src/web/parseMarkdown.d.ts.map +index 877d681..ff2beec 100644 +--- a/node_modules/react-native-enriched-markdown/lib/typescript/src/web/parseMarkdown.d.ts.map ++++ b/node_modules/react-native-enriched-markdown/lib/typescript/src/web/parseMarkdown.d.ts.map +@@ -1 +1 @@ +-{"version":3,"file":"parseMarkdown.d.ts","sourceRoot":"","sources":["../../../../src/web/parseMarkdown.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAsDxD,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,EACE,SAAiB,EACjB,SAAgB,EAChB,WAAmB,EACnB,SAAiB,EACjB,SAAiB,EACjB,cAAsB,EACtB,kBAA0B,EAC1B,WAAkB,GACnB,GAAE,SAAc,GAChB,OAAO,CAAC,OAAO,CAAC,CAsBlB"} +\ No newline at end of file ++{"version":3,"file":"parseMarkdown.d.ts","sourceRoot":"","sources":["../../../../src/web/parseMarkdown.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AA2DxD,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,EACE,SAAiB,EACjB,SAAgB,EAChB,WAAmB,EACnB,SAAiB,EACjB,SAAiB,EACjB,cAAsB,EACtB,kBAA0B,EAC1B,WAAkB,GACnB,GAAE,SAAc,GAChB,OAAO,GAAG,IAAI,CAuBhB;AAED,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,KAAK,GAAE,SAAc,GACpB,OAAO,CAAC,OAAO,CAAC,CAGlB"} +\ No newline at end of file +diff --git a/node_modules/react-native-enriched-markdown/lib/typescript/src/web/renderers/BlockRenderers.d.ts.map b/node_modules/react-native-enriched-markdown/lib/typescript/src/web/renderers/BlockRenderers.d.ts.map +index 2a61d73..b304c4e 100644 +--- a/node_modules/react-native-enriched-markdown/lib/typescript/src/web/renderers/BlockRenderers.d.ts.map ++++ b/node_modules/react-native-enriched-markdown/lib/typescript/src/web/renderers/BlockRenderers.d.ts.map +@@ -1 +1 @@ +-{"version":3,"file":"BlockRenderers.d.ts","sourceRoot":"","sources":["../../../../../src/web/renderers/BlockRenderers.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAiB,WAAW,EAAE,MAAM,UAAU,CAAC;AAqP3D,eAAO,MAAM,cAAc,EAAE,WAW5B,CAAC"} +\ No newline at end of file ++{"version":3,"file":"BlockRenderers.d.ts","sourceRoot":"","sources":["../../../../../src/web/renderers/BlockRenderers.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAiB,WAAW,EAAE,MAAM,UAAU,CAAC;AA4P3D,eAAO,MAAM,cAAc,EAAE,WAW5B,CAAC"} +\ No newline at end of file +diff --git a/node_modules/react-native-enriched-markdown/lib/typescript/src/web/renderers/InlineRenderers.d.ts.map b/node_modules/react-native-enriched-markdown/lib/typescript/src/web/renderers/InlineRenderers.d.ts.map +index 817ce9d..eb672e3 100644 +--- a/node_modules/react-native-enriched-markdown/lib/typescript/src/web/renderers/InlineRenderers.d.ts.map ++++ b/node_modules/react-native-enriched-markdown/lib/typescript/src/web/renderers/InlineRenderers.d.ts.map +@@ -1 +1 @@ +-{"version":3,"file":"InlineRenderers.d.ts","sourceRoot":"","sources":["../../../../../src/web/renderers/InlineRenderers.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAiB,WAAW,EAAE,MAAM,UAAU,CAAC;AA8G3D,eAAO,MAAM,eAAe,EAAE,WAc7B,CAAC"} +\ No newline at end of file ++{"version":3,"file":"InlineRenderers.d.ts","sourceRoot":"","sources":["../../../../../src/web/renderers/InlineRenderers.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAiB,WAAW,EAAE,MAAM,UAAU,CAAC;AAuH3D,eAAO,MAAM,eAAe,EAAE,WAc7B,CAAC"} +\ No newline at end of file +diff --git a/node_modules/react-native-enriched-markdown/lib/typescript/src/web/renderers/ListRenderers.d.ts.map b/node_modules/react-native-enriched-markdown/lib/typescript/src/web/renderers/ListRenderers.d.ts.map +index bdcf7d0..ea540b8 100644 +--- a/node_modules/react-native-enriched-markdown/lib/typescript/src/web/renderers/ListRenderers.d.ts.map ++++ b/node_modules/react-native-enriched-markdown/lib/typescript/src/web/renderers/ListRenderers.d.ts.map +@@ -1 +1 @@ +-{"version":3,"file":"ListRenderers.d.ts","sourceRoot":"","sources":["../../../../../src/web/renderers/ListRenderers.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAiB,WAAW,EAAqB,MAAM,UAAU,CAAC;AAmH9E,eAAO,MAAM,aAAa,EAAE,WAI3B,CAAC"} +\ No newline at end of file ++{"version":3,"file":"ListRenderers.d.ts","sourceRoot":"","sources":["../../../../../src/web/renderers/ListRenderers.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAiB,WAAW,EAAqB,MAAM,UAAU,CAAC;AA0H9E,eAAO,MAAM,aAAa,EAAE,WAI3B,CAAC"} +\ No newline at end of file +diff --git a/node_modules/react-native-enriched-markdown/lib/typescript/src/web/types.d.ts b/node_modules/react-native-enriched-markdown/lib/typescript/src/web/types.d.ts +index c83a25e..c3e28f5 100644 +--- a/node_modules/react-native-enriched-markdown/lib/typescript/src/web/types.d.ts ++++ b/node_modules/react-native-enriched-markdown/lib/typescript/src/web/types.d.ts +@@ -1,3 +1,4 @@ ++import type { MarkdownRenderers } from '../types/MarkdownRenderers'; + import type { ComponentType, ReactNode } from 'react'; + import type { MarkdownStyleInternal } from '../types/MarkdownStyleInternal'; + import type { Styles } from './styles'; +@@ -5,6 +6,7 @@ import type { LinkPressEvent, LinkLongPressEvent, ImagePressEvent, TaskListItemP + import type { KaTeXInstance } from './katex'; + export type NodeType = 'Document' | 'Paragraph' | 'Text' | 'Link' | 'Heading' | 'LineBreak' | 'SoftBreak' | 'Strong' | 'Emphasis' | 'Strikethrough' | 'Underline' | 'Superscript' | 'Subscript' | 'Highlight' | 'Code' | 'Image' | 'Blockquote' | 'UnorderedList' | 'OrderedList' | 'ListItem' | 'CodeBlock' | 'ThematicBreak' | 'Table' | 'TableHead' | 'TableBody' | 'TableRow' | 'TableHeaderCell' | 'TableCell' | 'LatexMathInline' | 'LatexMathDisplay' | 'BlankLine' | 'Admonition' | 'Video'; + export interface NodeAttributes { ++ start?: string; + level?: string; + url?: string; + /** Stamped by recognizeTextLinks() — not present in the raw WASM output. */ +@@ -44,6 +46,7 @@ export interface RendererCallbacks { + onCodeBlockPress?: (event: CodeBlockPressEvent) => void; + } + export interface RenderCapabilities { ++ renderers?: MarkdownRenderers; + katex: KaTeXInstance | null; + /** When false, task list checkboxes render read-only and ignore clicks. */ + enableTaskListItemToggle: boolean; +diff --git a/node_modules/react-native-enriched-markdown/lib/typescript/src/web/types.d.ts.map b/node_modules/react-native-enriched-markdown/lib/typescript/src/web/types.d.ts.map +index a32ca4e..279b944 100644 +--- a/node_modules/react-native-enriched-markdown/lib/typescript/src/web/types.d.ts.map ++++ b/node_modules/react-native-enriched-markdown/lib/typescript/src/web/types.d.ts.map +@@ -1 +1 @@ +-{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/web/types.ts"],"names":[],"mappings":"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"} +\ No newline at end of file ++{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/web/types.ts"],"names":[],"mappings":"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"} +\ No newline at end of file +diff --git a/node_modules/react-native-enriched-markdown/src/EnrichedMarkdownNativeComponent.ts b/node_modules/react-native-enriched-markdown/src/EnrichedMarkdownNativeComponent.ts +index 32ff768..8e89249 100644 +--- a/node_modules/react-native-enriched-markdown/src/EnrichedMarkdownNativeComponent.ts ++++ b/node_modules/react-native-enriched-markdown/src/EnrichedMarkdownNativeComponent.ts +@@ -275,6 +275,7 @@ export interface MarkdownStyleInternal { + + export interface LinkPressEvent { + url: string; ++ sourceType: string; + } + + export interface LinkLongPressEvent { +diff --git a/node_modules/react-native-enriched-markdown/src/EnrichedMarkdownTextNativeComponent.ts b/node_modules/react-native-enriched-markdown/src/EnrichedMarkdownTextNativeComponent.ts +index 6f31ccc..7b08b62 100644 +--- a/node_modules/react-native-enriched-markdown/src/EnrichedMarkdownTextNativeComponent.ts ++++ b/node_modules/react-native-enriched-markdown/src/EnrichedMarkdownTextNativeComponent.ts +@@ -276,6 +276,7 @@ export interface MarkdownStyleInternal { + + export interface LinkPressEvent { + url: string; ++ sourceType: string; + } + + export interface LinkLongPressEvent { +diff --git a/node_modules/react-native-enriched-markdown/src/native/EnrichedMarkdownText.tsx b/node_modules/react-native-enriched-markdown/src/native/EnrichedMarkdownText.tsx +index ca7157e..40581c1 100644 +--- a/node_modules/react-native-enriched-markdown/src/native/EnrichedMarkdownText.tsx ++++ b/node_modules/react-native-enriched-markdown/src/native/EnrichedMarkdownText.tsx +@@ -170,6 +170,7 @@ export const EnrichedMarkdownText = ({ + writingDirection = 'first-strong', + numberOfLines, + ellipsizeMode, ++ renderers: _renderers, + ...rest + }: EnrichedMarkdownTextProps) => { + // An equal config keeps the previous object, so an inline literal does not +@@ -297,9 +298,12 @@ export const EnrichedMarkdownText = ({ + ); + + const handleLinkPress = useCallback( +- (e: NativeSyntheticEvent) => { +- const { url } = e.nativeEvent; +- onLinkPress?.({ url }); ++ (e: NativeSyntheticEvent<{ url: string; sourceType: string }>) => { ++ const { url, sourceType } = e.nativeEvent; ++ onLinkPress?.({ ++ url, ++ sourceType: sourceType as LinkPressEvent['sourceType'], ++ }); + }, + [onLinkPress] + ); +diff --git a/node_modules/react-native-enriched-markdown/src/types/MarkdownTextProps.web.ts b/node_modules/react-native-enriched-markdown/src/types/MarkdownTextProps.web.ts +index cac90b4..c308537 100644 +--- a/node_modules/react-native-enriched-markdown/src/types/MarkdownTextProps.web.ts ++++ b/node_modules/react-native-enriched-markdown/src/types/MarkdownTextProps.web.ts +@@ -1,3 +1,4 @@ ++import type { MarkdownRenderers } from './MarkdownRenderers'; + import type { LinkRecognition } from './MarkdownTextProps'; + import type { CSSProperties, HTMLAttributes } from 'react'; + import type { MarkdownStyle, Md4cFlags } from './MarkdownStyle'; +@@ -23,6 +24,8 @@ export interface EnrichedMarkdownTextProps extends Omit< + * @platform ios, android, web + */ + markdown: string; ++ /** Custom block components. Ignored on native. @platform web */ ++ renderers?: MarkdownRenderers; + /** + * Style configuration for markdown elements. + * @platform ios, android, web +diff --git a/node_modules/react-native-enriched-markdown/src/types/events.ts b/node_modules/react-native-enriched-markdown/src/types/events.ts +index 9df94ed..c726533 100644 +--- a/node_modules/react-native-enriched-markdown/src/types/events.ts ++++ b/node_modules/react-native-enriched-markdown/src/types/events.ts +@@ -1,5 +1,7 @@ + export interface LinkPressEvent { + url: string; ++ /** Syntax of the tapped occurrence, independent of other links with the same URL. */ ++ sourceType?: 'markdown' | 'text' | 'inline-code'; + } + + export interface LinkLongPressEvent { +diff --git a/node_modules/react-native-enriched-markdown/src/web/EnrichedMarkdownText.tsx b/node_modules/react-native-enriched-markdown/src/web/EnrichedMarkdownText.tsx +index 08dc7df..11fbd16 100644 +--- a/node_modules/react-native-enriched-markdown/src/web/EnrichedMarkdownText.tsx ++++ b/node_modules/react-native-enriched-markdown/src/web/EnrichedMarkdownText.tsx +@@ -12,7 +12,7 @@ import { + parseErrorFallbackStyle, + buildStyles, + } from './styles'; +-import { parseMarkdown } from './parseMarkdown'; ++import { parseMarkdown, parseMarkdownSync } from './parseMarkdown'; + import { recognizeTextLinks } from './recognizeTextLinks'; + import { RenderNode } from './renderers'; + import type { ASTNode, RendererCallbacks, RenderCapabilities } from './types'; +@@ -27,6 +27,7 @@ export const EnrichedMarkdownText = ({ + markdownStyle = {}, + md4cFlags = {}, + linkRecognition, ++ renderers, + onLinkPress, + onLinkLongPress, + onImagePress, +@@ -46,7 +47,10 @@ export const EnrichedMarkdownText = ({ + [markdownStyle] + ); + +- const [ast, setAst] = useState(null); ++ const [parsed, setParsed] = useState<{ ++ ast: ASTNode; ++ markdown: string; ++ } | null>(null); + const [katex, setKatex] = useState(null); + const [parseError, setParseError] = useState(false); + +@@ -67,7 +71,53 @@ export const EnrichedMarkdownText = ({ + const textLinkKey = linkRecognition?.text?.toString() ?? null; + const inlineCodeLinkKey = linkRecognition?.inlineCode?.toString() ?? null; + ++ // Once WASM is ready, new/virtualized rows must have their text on the first ++ // commit. An effect-only parse briefly measures an empty row and moves readers. ++ const immediate = useMemo(() => { ++ try { ++ const result = parseMarkdownSync(markdown, { ++ underline, ++ latexMath, ++ superscript, ++ subscript, ++ highlight, ++ hardSoftBreaks, ++ preserveBlankLines, ++ admonitions, ++ }); ++ if (!result) return null; ++ indexTaskItems(result); ++ markInlineImages(result); ++ return { ++ markdown, ++ ast: recognizeTextLinks( ++ result, ++ linkRecognitionRef.current?.text, ++ linkRecognitionRef.current?.inlineCode ++ ), ++ }; ++ } catch { ++ return { markdown, ast: null }; ++ } ++ // Regex keys track ref changes; parsed wakes this memo after WASM initialization. ++ // eslint-disable-next-line react-hooks/exhaustive-deps ++ }, [ ++ markdown, ++ underline, ++ latexMath, ++ superscript, ++ subscript, ++ highlight, ++ hardSoftBreaks, ++ preserveBlankLines, ++ admonitions, ++ textLinkKey, ++ inlineCodeLinkKey, ++ parsed, ++ ]); ++ + useEffect(() => { ++ if (immediate && (!latexMath || katex)) return; + let cancelled = false; + + const katexPromise = latexMath ? loadKaTeX() : Promise.resolve(null); +@@ -92,13 +142,14 @@ export const EnrichedMarkdownText = ({ + + setParseError(false); + setKatex(katexInstance); +- setAst( +- recognizeTextLinks( ++ setParsed({ ++ markdown, ++ ast: recognizeTextLinks( + result, + linkRecognitionRef.current?.text, + linkRecognitionRef.current?.inlineCode +- ) +- ); ++ ), ++ }); + } + }) + .catch((error) => { +@@ -108,7 +159,7 @@ export const EnrichedMarkdownText = ({ + } + + setParseError(true); +- setAst(null); ++ setParsed(null); + setKatex(null); + } + }); +@@ -128,6 +179,8 @@ export const EnrichedMarkdownText = ({ + admonitions, + textLinkKey, + inlineCodeLinkKey, ++ immediate, ++ katex, + ]); + + const callbacks = useMemo( +@@ -148,8 +201,8 @@ export const EnrichedMarkdownText = ({ + ); + + const capabilities = useMemo( +- () => ({ katex, enableTaskListItemToggle }), +- [katex, enableTaskListItemToggle] ++ () => ({ katex, enableTaskListItemToggle, renderers }), ++ [katex, enableTaskListItemToggle, renderers] + ); + + const lastChildStyle = useMemo( +@@ -182,7 +235,8 @@ export const EnrichedMarkdownText = ({ + + const domProps = filterNativeOnlyProps(rest); + +- if (parseError) { ++ const visibleParsed = immediate ?? parsed; ++ if (immediate ? immediate.ast === null : parseError) { + return ( +
| null = null; + + // SINGLE_FILE=1 inlines the WASM binary as base64 inside md4c.js, so no + // network fetch is needed — only a one-time decode + compile on first call. + function initializeParser(): Promise { + if (!parserPromise) { +- parserPromise = import('./wasm/md4c') +- .then((module) => module.default()) ++ parserPromise = Promise.resolve() ++ .then(() => createMd4c()) + .then((wasmModule) => + wasmModule.cwrap('parseMarkdown', 'string', [ + 'string', +@@ -36,6 +38,10 @@ + 'number', + ]) + ) ++ .then((parse) => { ++ readyParser = parse as ParseFn; ++ return parse; ++ }) + .catch((error) => { + parserPromise = null; + throw error; +@@ -53,7 +59,7 @@ + ); + } + +-export async function parseMarkdown( ++export function parseMarkdownSync( + markdown: string, + { + underline = false, +@@ -65,8 +71,9 @@ + preserveBlankLines = false, + admonitions = true, + }: Md4cFlags = {} +-): Promise { +- const parse = await initializeParser(); ++): ASTNode | null { ++ const parse = readyParser; ++ if (!parse) return null; + + const result: unknown = JSON.parse( + parse( +@@ -88,3 +95,11 @@ + + return result; + } ++ ++export async function parseMarkdown( ++ markdown: string, ++ flags: Md4cFlags = {} ++): Promise { ++ await initializeParser(); ++ return parseMarkdownSync(markdown, flags)!; ++} +diff --git a/node_modules/react-native-enriched-markdown/src/web/renderers/ListRenderers.tsx b/node_modules/react-native-enriched-markdown/src/web/renderers/ListRenderers.tsx +index 251009e..da2e1b4 100644 +--- a/node_modules/react-native-enriched-markdown/src/web/renderers/ListRenderers.tsx ++++ b/node_modules/react-native-enriched-markdown/src/web/renderers/ListRenderers.tsx +@@ -25,7 +25,14 @@ function ListRenderer({ + : hasTaskChild + ? styles.listTask + : styles.list; +- return {renderChildren(node)}; ++ return ( ++ ++ {renderChildren(node)} ++ ++ ); + } + + function UnorderedListRenderer(props: RendererProps) { +diff --git a/node_modules/react-native-enriched-markdown/src/web/types.ts b/node_modules/react-native-enriched-markdown/src/web/types.ts +index 781adb1..4d4fa0c 100644 +--- a/node_modules/react-native-enriched-markdown/src/web/types.ts ++++ b/node_modules/react-native-enriched-markdown/src/web/types.ts +@@ -1,3 +1,4 @@ ++import type { MarkdownRenderers } from '../types/MarkdownRenderers'; + import type { ComponentType, ReactNode } from 'react'; + import type { MarkdownStyleInternal } from '../types/MarkdownStyleInternal'; + import type { Styles } from './styles'; +@@ -46,6 +47,7 @@ export type NodeType = + | 'Video'; + + export interface NodeAttributes { ++ start?: string; + level?: string; + url?: string; + /** Stamped by recognizeTextLinks() — not present in the raw WASM output. */ +@@ -88,6 +90,7 @@ export interface RendererCallbacks { + } + + export interface RenderCapabilities { ++ renderers?: MarkdownRenderers; + katex: KaTeXInstance | null; + /** When false, task list checkboxes render read-only and ignore clicks. */ + enableTaskListItemToggle: boolean; +diff --git a/node_modules/react-native-enriched-markdown/src/web/wasm/md4c.js b/node_modules/react-native-enriched-markdown/src/web/wasm/md4c.js +index 26a61d7..4f82a68 100644 +--- a/node_modules/react-native-enriched-markdown/src/web/wasm/md4c.js ++++ b/node_modules/react-native-enriched-markdown/src/web/wasm/md4c.js +@@ -1,2 +1,2 @@ +-var createMd4cModule=(()=>{var _scriptName=globalThis.document?.currentScript?.src;return async function(moduleArg={}){var Module=moduleArg;var ENVIRONMENT_IS_WEB=true;var ENVIRONMENT_IS_WORKER=false;var programArgs=[];var thisProgram="./this.program";var scriptDirectory="";var readAsync;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;class EmscriptenEH{}class EmscriptenSjLj extends EmscriptenEH{}function binaryDecode(bin){for(var i=0,l=bin.length,o=new Uint8Array(l),c;i>8&c}return o}var runtimeInitialized=false;function getMemoryBuffer(){return wasmMemory.buffer}function updateMemoryViews(){if(HEAP8?.buffer?.resizable)return;var b=getMemoryBuffer();HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);HEAPU8=new Uint8Array(b);HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){var preRun=Module["preRun"];if(preRun){if(typeof preRun=="function")preRun=[preRun];onPreRuns.push(...preRun)}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["__wasm_call_ctors"]()}function postRun(){var postRun=Module["postRun"];if(postRun){if(typeof postRun=="function")postRun=[postRun];onPostRuns.push(...postRun)}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what=`Aborted(${what})`;err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);throw e}var wasmBinaryFile;function findWasmBinary(){return binaryDecode('asm¬`````````````` ````||`~`|``Tenv __cxa_throwenv\r__assert_failenv _abort_js\nenvemscripten_resize_heap˜–\n\r\n\n\n   \r     \r \n  \n  p44€€@ A à€ ¢memory__wasm_call_ctors\rparseMarkdown__indirect_function_table_emscripten_stack_restore—_emscripten_stack_alloc˜emscripten_stack_get_current™ JA 3” \rq !"#$%&\'()s*B=DZfghq*‚‚„’†*‘އ*‹‰*“•–• (\nªº–æAàÚB7AèÚA6A÷ÚA:AìÚAØ\r(6AðÚAÜ\r-:AñÚA:AƒÛA:AøÚA±\r/;AúÚA³\r-:AûÚA:AÛA:A„ÛAŠ(6AˆÛAŽ-:A‰ÛA:A›ÛA :AÛA™ )7A˜ÛA¡ -:A™ÛA:A ÛB칻֮سå7A«ÛA:A¨ÛA:A·ÛA:A¬ÛAá\r(6A°ÛAå\r/;A²ÛA:A¸ÛAt"6A¼ÛB‹€€€€‚€€€7 AÆ(6 A¿)7 A: AÏÛA:AÄÛA¦ (6AÈÛAª -:AÉÛA:AÛÛA:AÐÛAÈ (6AÔÛAÌ -:AÕÛA:AÜÛAt"6AàÛBŽ€€€€‚€€€7 AÖ)7 AÐ)7 A: AëÚ,AH@AèÚ(AàÚ(k õ”!~#A0k"$@ E@@A€\\"AèÚ("AÿÿÿÿqAkA\nAëÚ,"AH""M@AàÚ(AàÚ !@  @ A€ ü\n AëÚ- Av ÀAH@AäÚ 6  AëÚ Aÿq:  jA:  AàÚ   kAäÚ( "A  A€x AàÚAàÚ(AëÚ,AN  A:,  AG:/  AG:.  AG:-  AG:+  AG:*  AG:)  AG:(  AG:\'A$t"A6 B7 B7 B7 B7 A j" 6 ! \\"A÷ÿÿÿO\r@@ A O@ Ar"Ajt!  Aÿÿÿÿk6  6  6   : A j! E\r E\r  ü\n  jA: Aj! A\'j!#AÐk"$@ A j"( , " AH"E@A<t"B7 Aì6 B7 B7 B7 B7$ B74 B€€€ü7,  6  A j6  (! AéOA4 Aèn" A4OA jA !A<t" B7 Aì6 B7 B7 B7 B7$ B74 B€€€ü7,  A j6 (!  6@ E\r (" Ak6 \r (( r Aj! ("\n! ( " \nG@@@ Ak(" E\r ("Ak6 \r (( r \n Ak" G\r (!  \n6 @A€  A€O" ( kAuM\r At"t! \n k"\n@ \nü\n   j6  \n j6  6 E\r k !@@@@ (" ("\nI@ (6 ("6 @  (Aj6 Aj!  ("k" Au"Aj" A€€€€O\rAÿÿÿÿ \n k"\nAu"  K \nAøÿÿÿO" A€€€€O\r At"\nt" j" (6 ("6  \nj!\n   (Aj6 ( ( ("k" Au  Atk! @   ü\n  \n6  Aj" 6  6 E\r k  6    Aj! @ ,AH@ (A: A6  A: A: A€z (" (  , AH6 -! -! -! -! -! -! -!\r -! -! B7( A6$ A6 A6 A6 A6 A6  A tAìžAà– r \rAtr Atr Atr Atr Atr Atr Atr6 (  , "AH"! ( ! !\n#A€k" $@ A j"(@A! ("E\rA›    A4jAAüü 6 6 )7 )7 )7 )7$ ( 6, \n60 AA ("Aq6œ A„j"AA€ü A;ã A:® A;à A:ß A: A;¿ A:ª A:¥ A:„ A€„q@ A:‚ A€€q@ A:â A€ q@ A:¨ A€€€q@ A:Á A€€€q@ A:¯ Aq@ A:Ä Aq@ A:¾ A€q@ A:² A€Âq@ A:€ Aq@A!@ A G A GqE@ jA: Ar"AýqA F@  jA: Aj"A€G\r A E\rA  jAk-"A\rF\r A\nF ! A6` A 6D 64 A€€  A€€OAt6X A„jAÿAÔü B7ä B7Ü@AA \n ("E@ AÌj!" A°jAr!# Aüj!$ AèjAr!% A°j!\rAè! A!@@ \r!@@@@ (" K@ B7ø B7ð B7èA! A°jAA \r A°jF"j \r \rF"!\r (¨!& (!@@ j-"A G A Gq\r A|qAj Aj A F! Aj" G\r ! A°jA,A j Aj " 6 A°jA$A j A j " 6A! A°jA A j Aj " A6@ (”"AL\r (œ! (! !\n@@  Alj"-A>F@ \n O\r O\r j-A>G\r Aj"!@ Aj" O\r@ j-"\nA G \nA Gq\r A|qAj Aj \nA F! Aj" G\r !   kAkA  G"\n6  6 !  \n ( "I\r  \n k"\n6 ! Aj" G\r ! ! 6ä@@ O\r j-A\nk  L\r (!@  Alj-A>F\r Aj" G\r ! " #  Aj !A!\nA!A! !@A k!A k! („"\' (üjAk!( (¬E! (!! ( !) (œ! (¤!@@ ("AG@ !   6@  ("M@ !  !@ O\r  ( j-!@  j- AÿqG\r Aj" G\r ! @  k )I\r@  O\r@@  j-A k Aj" G\r ! 6ä \rA6  6ä  F@   (k"A M6 \rA6  (! @@@@ AG\r AL\r  L\rA! A6¤  O\r  j"-A\nk @ Aj  Aäj+AL\r A6¤ AþÿÿÿqAG\r \rA6 A6 ! \rA6  @ AG\r  (”G\r \rA6  (" (œk"A O6  \rA6@@ AL"\r G\r ( AljAk-"A>G6¨  A6¨ @ ! ( AljAk-! AÿqA>F@ !  G@ !  (€\r („"A H\r (ü jAk-AG\r A6¬  @ Aq@ !    AL\r  (” G\r  ! Alj"Ak-A>F\r  G\r  (€\r  \'A H\r  (-AG\r  6A AF\r   A$k(k"A M6 Ak ! A6¬ A6¨@ (" O\r@ AG\r@ -"A-k  G\r@@ Aj" O\r@  j-F@  Aj"K\r  @@ j-A k Aj" G\r ! 6ä \rA6 AA A=F6   \nI\r -"E\rA­ ÀAPE\rA! Aj"\n O\r@@@  \nj-" AÿqF"\r A F\r A G\r j! \nAj"\n G\r !\n AI\r  AI\r A\nk @@@  L\r G\r@ Aj   Aàj Aèj,E\r -è"A>F\r ! Alj"-G\r (ð" ( K\r (ô" k j" !@ (à" O\r@ j-"A G A Gq\r A|qAj Aj A F! Aj" G\r 6ä   k6  6 !  6ä   k" 6  6 O\r@ j-A\nk M@ j"6ô A6  Aj"6ô  Ak6  ! @  O@ (AF@ !  \rA6   k6 A6 ! \n Aj   Aäj Aèj,! (!@@@@@ @ (ä!@ AG\r  G\r@@ O\r j-"A\rF\r -è! A\nG\r AÿqA>F\r  A>! -èA>G\r Aÿq"A.G A)Gq\r (ìAG\r (ô" (ðk j"!@ O\r@ j-"A G A Gq\r A|qAj Aj A F! Aj" G\r ! 6ä   k"6  6  ,è6@@@ O\r j-A\nk Aj6ô   M@  j6ô A6  Aj6ô  Ak6 E@ Aj j-\r (”! @ (˜" J@ (!  A Av j AL"6˜ ( All"E@ (("E\rAõ (0   6 (”! Aè  F! Aj! Aj6”  Alj" )ø7  )ð7  )è7 (”! (! (! ! \n A F  Fq\r (ä! ! O@ !  @ j"\n-"A#G@ ! AþF\r AàF\r !  Ak"\nF\rA!  Aj"j-A#F\r  @@  j- G\r Aj" G\r !  k"AI@ !  6 @@@  O\r@  j-A G\r Aj" G\r   O\r@@  j-"A\nk @ AàG\r \n-AàG\r !  Aj" G\r ! 6ä \rA6 A6 A6 \r \rA 6  \nF\rA!  Aj"j-A#G\r  \nF\rA!  Aj"j-A#G\r  \nF\rA!  Aj"j-A#G\r  \nF\rA!  Aj"j-A#G\r  \nF\rA!  Aj"j-A#G\r  \nF\rA! Aj!   k! ! AJ@ !  @  O"\n\r (Aq\r  j-A k"AK@ !  A tA“€€q\r !  @ \n\r@  j-"A G A Gq\r Aj" G\r !  6 6ä \rA6  6 @@@@@  O\r  j-AF\r Aj"\n O\r A/G\r \n j-A>F\r  AjM@A!  A! AA   A j>E\rA! ( "\n O\r (!@@  \nj-"A k"AK\rA tA€€qE\r \nAj"\n G\r  AAA A\nF A\rF! Aj$ 6¤@  (AG\r A6¤ ("A€qE\r (AG\r  (O\r ( j-"A-k Aj  Aäj+ F@ A6¤ A6 \rA6 AüG\r  (”G\r (€"E\r (AG\r Aäj! Aàj!A!A!@ Aj"(" M\r (" j-AüG\rA! Aj" O\r@  j-A k"\nAK\rA \ntA€€qE\r Aj" G\r ! @@@@  I@  ( j-A:Fj!  O\r ("\n j-A-G\r@@@  \nj-"A-G\r Aj" G\r Aj!  A! Aj!@  A:Fj" O\r@@  \nj-"A k"AK\rA tA€€qE\r  Aj"G\r  AüG\rA!A! Aj" O\r@  \nj-A k"AK\rA tA€€qE\r Aj" I\r !   O\r@  \nj-A\nk E\r  ! E\r  6  6A  A E\r  (à6 \rA\n6 \rA6 (”"  F  (AF! A€qE\r jAL\r ( Alj"Ak-"E\r A?K\rB ­†B€€€€ÀƒP\r@ (ä" ("\n Aj"  \nK"O\r (!@ j-"A G A Gq\r Aj" G\r ! Aj" \nO\r (" j-AÛG\r  Aj"j,"E\rAÓ APE\r  j-AÝG\r \n Aj"G@ j-A k"AK\r A tA“€€qE\r Ak % " -Ar: Ak $  6@ ("O\r (!@ j-A k"AK\rA tA€€qE\r Aj" G\r ! 6ä  6  Aj"6ô  6  6Aè!A! ! ! Aj.A! Aj A@k/\r -Aq@ Aj AÜj/\r AjA-\r Aj0\rA!#Ak" $ A6$ Aj"\rA6@ \r(€AJ@ Aøj!@@@@@@ \r(ø j" ("AÿqAk AsA\nvAq6 /:  (6 (AsA\nvAq6 /:  AÿÿK6 /: (6  A6 /At(€\\"6 /! 6 At(€6 Aj6 A$j6 @@ ("A€q@@ A€qE\r Aÿq A j \r(, \r("@ \r($"E\rA¿ \r(,   ("Aÿq"AK\rA tAŽ€qE\r \r \r(Ak6 (! A€qE\r Aÿq A j \r(, \r("@ \r($"E\rAš \r(,   @ ("AÿqAk \r(Œ \r(Alj" -Ar: \r \r(Aj6  B7` B7X B7P B7H B7@ B78 B70 B7(@ ("AÿqA\nG\r A€€„I\r ("A€ Av"nM\r Aq! Aj!A!\nA!@ "AO@ A|q!A!@  Atj"( j ( j (j (j ( ( ( (jjjk! Aj! Aj" G\r E\r @  Atj"( j (k! Aj! \nAj"\n G\r  n AvO\r \r($"A¸ \r(,  (  A€~qA r"6 A! A6ˆ B7€ B7x B7p B7h \r("@ \r(Œ AljAk-As! A!@@@@@@@ AÿqAk Av6h  A€€I\r \r("\n ("j-! ( !@@@  \r("O\r@  \nj- AÿqG\r Aj" G\r  M\r@  \nj-A G\r Aj" G\r ! @  M\r@  \njAk-A G\r Ak" K\r ! A! \r  \nj  kA Aèj AÈj9\r \r(! !\n@  M\r@  \nj-A k"AMAA tA€€q\r \nAj"\n G\r !\n \r  j \n kA  A(j9\r :ˆ (!A!  A6l Av6h (Ak6p  Av6h @@@@@@@  Aÿq"A FqE@  Aèj \r(, \r("\r (! AÿqAk \r($"E\rAš \r(,   (!@ AÿÿM@ E\r Aj!@ ( (F@ A j! Ak"\r  @  A lj"A k( Ak(G\r Ak"\r  Ak"E\r Aj! \rA  :"E\r  \rA Aj (:"E\r  (!@@ Av"Atj"E@A! \r($"E\rAä \r(,  k  @ (! ("! ! !@ !@ M\r \r(!@  j-A-F\r Aj" G\r ! A!  K@ \r( jAk-A:F! @ M\r \r(!@@  j-"\nA-G\r Aj" G\r !  Ar  \nA:F!  At( 96 Ak! Aj! AJ\r A A \r(, \r("@ \r($"E\rAš \r(,  k  \rA ( (  ;"\rA A \r(, \r("@ \r($"E\rA¿ \r(,  k  AI\r@A A \r(, \r("E@ Aj!A!  \r($"E\rAš \r(,  k  @ \rA  Atj"( (  ;"\r Aj" G\r A A \r(, \r("E\r \r($"E\rA¿ \r(,  k  k  \r Aj (<"\r A!  -"A Fq\r  Aèj \r(, \r("E\r \r($"@A¿ \r(,  ! \r (X@ (Hk (Lk (Pk (8E\r ((k (,k (0k \r (! -AkAM@ A l j!  At j!  \r(Œ \r(Alj" -Aþq A\nvAqr: \r \r(Aj6 Aj" \r(€H\r \rA6€A! Aj$ "\r -Aq@A!A!#A@j"\n$@ Aj"(`"E\r (pE\r (X AAR@@AA (, ("E@ (`E\r \nA0j!  ($"E\rAš (,   @ (X Alj"( E\r \nB7 \nB7 \nB7 \nB7 B7 B7 \n ( 6( \n (6,@ ( (A  \nAj9@A!  A \nA(j (, ("@ ($"E\rAš (,   ( (<"\rA \nA(j (, ("E@A!  ($"E\rA¿ (,  \n(@ \n(k \n( k \n(k \r Aj" (`I\r A!AA (, ("E\r ($"@A¿ (,  ! \nA@k$ "\r A!AA (0 ("E\r (("@A¿ (0  !  \n6ä \rA6 !  ! !  A6¨ ! @ (ä"Aj" ("O\r (!@@ j"-A\nk @ -A\nk @ -A\nk @  j-A\nk Aj! Aj!  I\r @ I@ (!@A!@ j-A\nk Aj" G\r ! A! A°jA(A j Aj "\n 6 " \r("AG\r@@ ("M\r (!@  jAk-"A G A Gq\r Ak" K\r  @  O\r (!@  jAk-A#G\r Ak" K\r   F\r ( jAk-"A F@ !  A F@ !  ! -AqE\r \n 6  !@ AkAI\r  ("M\r (!@  jAk-"A G A Gq\r \n Ak"6  K\r @ ( j-A\rFj! I@ ( j-A\nFj! @ &E\r E\r  j"AL\r ( Alj"Ak-A>F\r (ü Ak(j" (A€r6 @ \r  j" (”N\r Aj -\r AJ@ AjA Al" (j"(" -Aq ( j,A A1\r AjA (ü" -éAq" ( j,A A1\r ( j" -Aýq r: ( j 6 @ (”!@ -AqE\r ( Alj"Ak-A>G\r \r(AG\r \n(" ("k"AkA K\r (" j"-AÛG\r -A!G\r  jAk-AÝG\r Ak! Aj!A!@@@@ Ak @  j-"A k  AákAÿqAIAÿq -¹"A k  AákAÿqAIAÿqG\r Aj" G\r A  @  j-"A k  AákAÿqAIAÿq -Î "A k  AákAÿqAIAÿqG@ A\nk Aj" G\r A  A!@  j-"A k  AákAÿqAIAÿq -ú "A k  AákAÿqAIAÿqG@ A\nG\r  Aj" G\r A  A!@  j-"A k  AákAÿqAIAÿq -‡"A k  AákAÿqAIAÿqG@ A\nG\r -"A k  AákAÿqAIAÿqAÃG\r -"A k  AákAÿqAIAÿqAÁG\r -"A k  AákAÿqAIAÿqAÕG\r -"A k  AákAÿqAIAÿqAÔG\r -"A k  AákAÿqAIAÿqAÉG\r -"A k  AákAÿqAIAÿqAÏG\r -"A k  AákAÿqAIAÿqAÎG\rA  Aj" G\r A ! Ak" -Ar: ( (”AljAk" -AÇq r: \rA6 (”!  k!@A!@@A ( Alj"-A)kt"AqE@ A!qE\rA! Aj.  („6 Aj  ( ,A1\r AjA (" -Aq ( j,A A1\r  AjAA -"AqA AvAqA1\r Aj" (”H\r @ \r(E@ Aj.@A!  A!Aè! -AÀqE\r ( \n(I\r (ŒAj6Œ  (Œ@@ („"Aj" (ˆ"J@ A€ Av j AL"6ˆ (ü l"E@A! (("E\rAõ (0   (€"@   (ükj6€ 6ü („"Aj6„  6„ (ü"\rA!   j"A; (Œ! A6  AtAr6 A6Œ @ (E\r Aj.E\rA!  @@@@ \r("Ak\n A! Aj.\r („"Aj" (ˆ"J@ A€ Av j AL"6ˆ (ü l"E@ (("E\r\nAõ (0  \n (€"@   (ükj6€ („"Aj6„ 6ü  j  6„ (ü"E\r  j " \r(AÃÌj-"; (! A6   Atr6 6€ Aj \r2\r Aj.\rA!Aè!  (€A: (€ /; (€" (A€r6A! Aj \r2\r Aj.\rA! (€E@Aè!  \rA6 \r!  (€A\n: (€ /; A 6AA Aj \r2!  @  (F\r Aj.E\rA!  @ (€\r \r! @ Aj"(€"Aj"\n („"J@ A€ Av j AL"6„ (ø l"E@A ($"E\rAõ (, A  (ü"\n@   \n (økj6ü  6ø  (€"\nAj6€  \nj   \n6€ (ø"E\r  j " \r(AÃÌj-"\n; \r(! A6  \n Atr6  6üA  A E\rA!  Aj \r2! A! E\r  A!  (("E\rAš \n  (P"@ (T"A!@@ (P Atj("E\r (@" M@   (D (HljI\r k (T! Aj" I\r (P k (H@A!@ (@ Atj"-"Aq (k -  Aq@ ( k Aj" (HI\r (@k (l"@ (p"A!@@ (l Atj("E\r (\\" M@   (` (dljI\r k (p! Aj" I\r (l k (d@A!@ (\\ Alj(k Aj" (dI\r (\\k (8k (xk (ük (k A€j$ @A<t"B7 Aì6 B7 B7 B7 B7$ B74 B€€€ü7,  6  A j6  (@ (("E\r ((" ($"G A$j!A!\r@@@  \rAt"\nj("(AF@ ($" (("G\r \rAj!\r   k"Au!@@ ((AF\rA! AG@@ Aj!  Atj"((AF@  O\r Ak(!  Atj(! \r \r  "G\r A! A68 B70A!  A! A68 B70 A6L B7D  A0j6@  AÄj6<@@@@@@  At"j"(" (AG\r  Ak(A !  Aj"K@  Atj( rAG\r AF\r @ Aj" O\r -"A k  AákAÿqAIAÿqAÓG\r -"A k  AákAÿqAIAÿqAÃG\r -"A k  AákAÿqAIAÿqAÒG\r -"A k  AákAÿqAIAÿqAÉG\r -"A k  AákAÿqAIAÿqAÐG\r -"A k  AákAÿqAIAÿqAÔG\r  j-A>F\r @ Aj" O\r -"A k  AákAÿqAIAÿqAÓG\r -"A k  AákAÿqAIAÿqAÔG\r -"A k  AákAÿqAIAÿqAÙG\r -"A k  AákAÿqAIAÿqAÌG\r -"A k  AákAÿqAIAÿqAÅG\r  j-A>F\r A\nj" O\r -"A k AákAÿqAIAÿqAÔG\r -"A k AákAÿqAIAÿqAÅG\r -"A k AákAÿqAIAÿqAØG\r -"A k AákAÿqAIAÿqAÔG\r -"A k AákAÿqAIAÿqAÁG\r -"A k AákAÿqAIAÿqAÒG\r -"A k AákAÿqAIAÿqAÅG\r - "A k AákAÿqAIAÿqAÁG\r  j-A>F\r\r Aj" G\r !  6A Aj" O\r Ak! (!@@  j"-A\nk\n\n /A­Ús -A>srE\r Aj! Aj!  I\r !  Aj" O\r Ak! (!@@  j"-A\nk /A¿üF\r Aj! Aj!  I\r !  Aj" O\r Ak! (!@@@  j-"A\nk  A>F\r Aj! Aj!  I\r !  Aj" O\r Ak! (!@@  j"-A\nk /Aݺs -A>srE\r Aj! Aj!  I\r !  @@  O\rA! ( j-A\nk  6 ( !   6A  6A  6A  6A  6A  Aj6A Å@  ("O\r  (˜O\r @ (" j-"E\r A>F@ A>:  6  -Aüq: Aj! Aj  A?K\rB ­†B€€€€€ ƒP\r  Aj"K@  j-A k"AK\rA tA“€€qE\r  :  6  -Aüq: Aj  A! A6  A j"  I"\nO\rA! !@@ j-" A0kAÿqA K\r  A\nljA0k"6 Aj" \nG\r \n! M\r O\r@ j-" A)k  Aj"K@  j-A k"AK\rA tA“€€qE\r  :  6  -Aüq:  k jAj 6  6A!  ç  ("H@@A!@@@A (Œ Alj"Ak"-A)kt"AqE@ A!qE\rA! Ak(!A! Ak-Aq@ ( j,! A! A  A1\r A ,A1\r  AA Ak-"AqA AvAqA1E\rA!  (Ak"6  H\r A Ú#A0k"$@ (ü"E\r@@@ ("AÿqAk A€qE\r (" (O\r ( j-AÛG\r@ ("E\r Aj! AG\r !  @ \r@ A/G\r Aj" O\r \n j-A>F\r AL\r@ AßqAÁkAÿqAI\r AßF\r A:G\r  Aj"M@A!  @@@ \nj,"A0kAÿqA\nI\r A_qAÁkAÿqAI\r E@A!  A  APE\r A!  Aj"G\r  A!  A! @@ Ak A=G\r Aj!A!  A+!@@@@ A"k E\r Aá AP\r A\rF\r A)!  A*! Aj! I\r A! E\r \rF\rA  A)FA !  Aj" Atj"(! (" I\r   M@A A!  Aj6 „#Ak"$@  O\r@@@ j"  k A j4"AÿK@A!A!@@@  jAm"AtA j("\nAu j"AtA j(Aÿÿÿq K@ Ak!   \nAvAq j"AtA j(AÿÿÿqM\r Aj!  L\r  AH\r  A F\r A F\r A kAI\r -A\nk ( j" I\r Aj$  à@@@ AÿK@A­!@@@  jAm"AtAÀj("Au j"AtAÀj("Aÿÿÿq K@ Ak!  AvAq j"AtAÀj(AÿÿÿqM\r Aj!  N\r  AH\rAÐ8!  A6  A r AÁkAI6 A!A4!@@@  jAm"AtAÀ1j("Au j"AtAÀ1j("Aÿÿÿq M@ AvAq j"AtAÀ1j(Aÿÿÿq O@ AH\rAà8!  Aj!  Ak!  N\r A!A!@@@  jAm"AtAÐ6j("Au j"AtAÐ6j("Aÿÿÿq M@ AvAq j"AtAÐ6j(Aÿÿÿq O@ AH\rAð8!  Aj!  Ak!  N\r A6  6  ( ( " lAtj! At"@   ü\n  6 F\r ( Aÿÿÿq"AjF@  sAsAq j6  ( kj6 õ #A k"$ B7 B7 B7 B7 A ?! A ?! @@@  K@   M\r @ \nE@   AjC!A! AqE@   C! A!  Atj( Aj Atj(k"\r Aj" ( " I! Aj" (" I!\n  K\r  I\r  K\r  I\r A! A j$  iA!@ ("(" ("("I\rA!  K\r ( ( ( (A"\rA! (" ("I\r K!  ‰#Ak"$@@@  O\r j  k A j4! ( j! AÿK@A!@@@  jAm"AtA j("\nAu j" AtA j(Aÿÿÿq K@ Ak!   \nAvAq j"AtA j(AÿÿÿqM\r Aj!  L\r  AN\r  A F\r A F\r A kAO\r A6 A 6  ?!   @ Aj$  D ("(" ("("I@A  K@A ( ( ( (A ›”#A0k"$ A6x#Ak"$ AjAA€ü @@ E\r Ak! A€j!  AtjAk!@  Atj"(!@ !@ !@ Aj" ("O@ !  (! @  j"-j-@ !  -j-@ !  -j-@ !   j-j-@ !  Aj! Aj"!  K\r @  K@ (!@  j"-" j-\r Aj" G\r Aj" I\r  @AA@@@@@@ AÜF@A! Aj" (O\r@  j-"A!kAÿqAI\r@ AÿqA\nku Aj I\r Aj! @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"AÜ ; B7  Aj"6  6 A!@@@@@@@@@@@@ A!k\\ \r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\n\r \r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r Aj"\r!\n@  \rM\r@  \nj- G\r \nAj"\n I\r !\n A!A!A!  (F\r N"AÿM\rA!A!@@@@  jAm" AtA j("Au j"AtA j(Aÿÿÿq K@ Ak!   AvAq j"AtA j(AÿÿÿqM\r Aj!  N\r  A! AN\r A!Aä!@@@  jAm" AtA°9j("Au j"AtA°9j(Aÿÿÿq K@ Ak!   AvAq j"AtA°9j(AÿÿÿqM\r Aj!  N\r A! AN   k! !@@  j-" AàG\r Aj" G\r !A  A F ! !  k"A K\r At j( "\r  AtjAk(O\rA!A!@ E@ ! !\n  !\n \r " I\r @@@@ \nO\r@  j-"\rA G@ \rAàF\rA! Aj" \nG\r A! \n  !\r \nO@A! !  @  \rj-AàG"@ ! \r!  \rAj"\r \nG\r ! \n! @  k"\rF@A!\n  Atj"("\r I@  jAk-A F!\n As  Gq\r \r \rF \nrE\r E\r Aj  @ F\rA! \rAK\r \rAt jA j"\r \r("\r \rK6 \r  Aj"M@A!   Atj"\n(! \n(!\n  ( ! \n@ Ak!  Ak(" ("O\r@  j-"A G A Gq\r Aj" G\r ! @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"Aà; B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"Aà; B7  6  6 (t (x"Alj"Ak" Ak6 A(k" Ak6  -Ar:  -Ar:A! ! ! ! (M\r@@@   jAv"Atj"( K@ E\r  AtjAk( I\r Ak!  (K\r ! !  Aj! @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"A¦; B7  Aj"6  6  (x"AL\r (t" AljAk-A&G\r (|" L@ AÀ Av j AL"6|  All"E@A! ($" E\rAõ (,   6t (x! Aj6x  Alj"A»; B7  Aj6  6  ("\n - AÀq\r\n   k"  \n A j>\r (" AjM\r -A!G\r  Aj"j-A-G\r -A-G\r A! (è" K  \nAkOq\r @ Aj" \n \nI"\rM@  j" /A­Ús -A>srE\r\r@@ Aj! Aj" \rK\r " j" /A­Ús -A>sr\r  !  \nI  Aj" Kq@  Atj"(! (!  6è (! A! -AqE\r Aj" O\r  j-AüG\r@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"Aü; B7  Aj"6  6  ("Aj K\r  Ak"\rj"/A÷îs -A÷sr\r @  \rF\r \rN"AÿK@A!A!@@@@  jAm" AtA j("\nAu j"AtA j(Aÿÿÿq K@ Ak!   \nAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r  AN\r A!Aä!@@@  jAm" AtA°9j("\nAu j"AtA°9j(Aÿÿÿq K@ Ak!   \nAvAq j"AtA°9j(AÿÿÿqM\r Aj!  N\r  AH\r\r  A F\r A F\r A\rkA}K\r A!kAI\r A:kAI\r AÛkAI\r AÿG AúKqE\r @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"A®; B7  Aj"6  \r6@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" (6 (! AÄ; B7  6  Aj!@@ (" Aj K\r  Ak"\nj(AèèуG\r  M\r /A¯ÞF\r @ Aj K\r  Ak"\nj"\r(Aèèуs \r-Aósr\r  M\r /A¯ÞF\r Aj K\r  Ak"\nj"/Aæès -Aðsr\r  M\r /A¯ÞG\r @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"Aº; B7  6  \n6@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" (6 (! AÄ; B7  6 Aj!  (Aj K\r @ Ak-"AßqAÁkAÿqAO@ Aj O\r A0kAÿqA\nO\r  Aj O\r\n  Aj"j-"A0kAÿqA\nO@ A_qAÁkAÿqAK\r\n @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"AÀ; B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" (6 (! AÄ; B7  6 Aj!  @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"AÝ; B7  Aj"6  6 \r @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" 6A!A  Aj" O\r  j-AÛG\r@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" 6A!!A !  :  : B7   j"6@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"AÄ; B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"AÄ; B7  6  6 \n @@ E@  -\rA qAvq\r  E\r @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"A:  : B7  Aj"6  6 \n @@@@@@@@@@@AA@@AA@@@@@@@@@@@@@@ >\r"""""""" " """"""""""""""""""" """""""""""""""""""""""""" AÞF\r AþG\r!A! Aj"\n!@  \nM"\r@  j-AþG" \r Aj" I\r !  k"AG\r ( "A€€qE\r \rA! A!  \nj ( \nkA4"AÿK\r A F\r A F\r A kAM\rA  A! Aj" I\r -AqE\r Aj"!@  M\r@@  j-AÞG\r Aj" I\r ! !  kAG\r%A! A!  j ( kA4"AÿM@ A F\r A F\r A kAM\rA  @@@  jAm"AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r A  \nAN\rA  A !  (F\r N"AÿM\rA!A!@@@  jAm" AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r !  \nAN\r !  -A qE\r Aj" O\r@  j" -A=F@  Aj"K\r  !  kAG\r"A!A!AA ( kA4"AÿM@A! A F\r A F\r A kAI  @@@  jAm" AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r A!\n \nAN !  -A€qE\r Aj" O\r@  j" -A+F@  Aj"K\r  !  kAG\r!A!A!AA ( kA4"AÿM@A! A F\r A F\r A kAI  @@@  jAm" AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r A!\n \nAN !  @@@  jAm"AtA j("Au j"\rAtA j(Aÿÿÿq K@ \rAk!   AvAq j"AtA j(AÿÿÿqM\r Aj!  N\r A  \rAN\rA  A !  (F\r\r N"AÿM\r A!A!@@@  jAm" AtA j("Au j"\rAtA j(Aÿÿÿq K@ \rAk!   AvAq j"AtA j(AÿÿÿqM\r Aj!  N\r !  \rAN\r\r !  ! AK\r ( ! ! A€qE\rA!  (M\r N"AÿM\rA!\nA!@@@@  \njAm"\rAtA j(" Au \rj"AtA j(Aÿÿÿq K@ Ak!   AvAq \rj"\nAtA j(AÿÿÿqM\r \nAj!\n  \nN\r  AN\r\n A!\nAä!@@@  \njAm"\rAtA°9j(" Au \rj"AtA°9j(Aÿÿÿq K@ Ak!   AvAq \rj"\nAtA°9j(AÿÿÿqM\r \nAj!\n  \nN\r A! AN  @  j-A$F@  Aj"K\r  A! !  kAK\rA!  (M\r N"AÿM\rA!A! @@@@  jAm"\nAtA j("Au \nj"\rAtA j(Aÿÿÿq K@ \rAk!   AvAq \nj"AtA j(AÿÿÿqM\r Aj!  L\r  \rAN\r A!Aä! @@@  jAm"\nAtA°9j("Au \nj"\rAtA°9j(Aÿÿÿq K@ \rAk!   AvAq \nj"AtA°9j(AÿÿÿqM\r Aj!  L\r A!\r \rAN  @ Aj" O\r@  j-A k"AK\rA tA€€qE\r Aj" I\r ! A F@ !  kAI\r @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\r Aõ (,  Aj6x (t"\rA!  6t (x"Aj6x  Alj"A:  : B7  6  6  @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"A€ ; B7  Aj"6  6  A F\r A F\r A\rkA}K\r AÿG AúKq A!kAI A:kAIr AÛkAIrr ! @ E\rA! A!@  j" ( k" A4"AÿM@ A F\r A F\r A kAO\r  @@@  jAm"AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r  \nAN\r A!Aä!  A4"AÿM@ AÿG AúKq A!kAI A:kAIr AÛkAIrr  @@@  jAm" AtA°9j("\rAu j"\nAtA°9j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA°9j(AÿÿÿqM\r Aj!  N\r A!\n \nAN ! !  Aq "E\r @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" : A$: B7  6  6  A F\r A F\r A\rkA}K\r AÿG AúKq A!kAI A:kAIr AÛkAIrr ! @ E\rA! A!@  j" ( k" A4"AÿM@ A F\r A F\r A kAO\r  @@@  jAm"AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r  \nAN\r A!Aä!  A4"AÿM@ AÿG AúKq A!kAI A:kAIr AÛkAIrr  @@@  jAm" AtA°9j("\rAu j"\nAtA°9j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA°9j(AÿÿÿqM\r Aj!  N\r A!\n \nAN ! !  Aq "E\r @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" : Aþ: B7  6  6  A F\r A F\r A kAM\r !  A! ! AqE\r @@ (x" (|" N@ AÀ Av j AL"6| (t All" \rA! ($" E\rAõ (,   Aj6x (t" \rA!  6t (x"Aj6x Alj" : Aþ: B7  \n6  6  A! !  kAG\r @@  (F\r N"AÿK@A!A!@@@  jAm" AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r !  \nAN\r !  A F\r A F\r A kAM\r !  A! ! AqE\r @@ (x" (|" N@ AÀ Av j AL"6| (t All" \rA! ($" E\rAõ (,   Aj6x (t" \rA!  6t (x"Aj6x Alj" : A+: B7  6  6  A! !  kAG\r @@  (F\r N"AÿK@A!A!@@@  jAm" AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r !  \nAN\r !  A F\r A F\r A kAM\r !  A! ! AqE\r @@ (x" (|" N@ AÀ Av j AL"6| (t All" \rA! ($" E\rAõ (,   Aj6x (t" \rA!  6t (x"Aj6x Alj" : A=: B7  6  6 A F\r A F\r A kAM\r !  A! ! AqE\r @@ (x" (|" N@ AÀ Av j AL"6| (t All" \rA! ($" E\rAõ (,   Aj6x (t" \rA!  6t (x"Aj6x Alj" : AÞ: B7  6  6 @ Aj" O\r -A?G\rA!@ (ì" M@ !  !  \nAkO\r  Aj" \n \nI"\rM@  j/A¿üF\r@@ Aj! Aj" \rK\r " j/A¿üG\r  !  \nI  Aj"Kq  Atj"(! (!  6ì ( ! @ M\r -A!G\r  j-AßqAÁkAÿqAK\r@ Aj" O\r@  j-AßqAÁkAÿqAK\r Aj" G\r ! A! (ð" K  \nAkOq\r Aj" \n \nI"\rM@  j-A>F\r@@ Aj! Aj" \rK\r " j-A>G\r  !  \nI  Aj" Kq  Atj"(! (!  6ð ( ! @ A j" O\r )B¼ÂìšÄ¨ªÁ… 1Bۅ„BR\rA!\r (ô" K  \nAkOq\r@ Aj" \n \nI" M@  j"/Aݺs -A>srE\r@@ Aj! Aj" K\r  "j"/Aݺs -A>sr\r  !  \nI  \rAj"\rKq@  \rAtj"(! (!  6ô ( !\n ! Aj" \nO\r@ Aj" \nO\r  j-" A0kAÿqA\nIA AßqAÁkAÿqAI E\r@  j-"A0kAÿqA\nI\r AßqAÁkAÿqAI\r A+k"AK\r AF\r Aj" \nF\r@  kA K\r  j-" A:F@  \nO\r@  j-" A k"AMAA tA€€q\r A>G@ A I\r AG\r  j-A-F\rAð! Aj !@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\r\rAõ (,  \r Aj6x (t"\rA! 6t (x"Aj6x  Alj" Ar: A<: B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\r\rAõ (,  \r Aj6x (t"\rA! 6t (x"Aj6x  Alj" Ar: A>: B7  6  Ak6 (t (x"Alj"A k Ak6 Ak Ak6 ! !   6 @@@ (x" (|"N@ AÀ Av j AL"6| (t All"E\r 6t (x"Aj6x  Aj6x (t"E\r  Alj"A¼(; B7  6  6@ (x" (|"N@ AÀ Av j AL"6| (t All"E\r 6t (x"Aj6x  Aj6x (t"E\r ( !  Alj"A¾0; B7  6  6 (t (x"Alj"A k Ak6 Ak Ak6A! !  (M\r@@   jAv"Atj"( K@ E@ !   AtjAk(K@ ! Ak!  ( O@ ! \n Aj!  M\r A!  ($"E\rAõ (,  A!  Aj!  A F\r A F\r A\rkA}K\r AÿG AúKq A!kAI A:kAIr AÛkAIrr "! As AßFq! @  \nF\rA!A!@  \nj" ( \nk"A4"AÿM@ A F\r A F\r A kAO\r  @@@  jAm"AtA j(" Au j"AtA j(Aÿÿÿq K@ Ak!   AvAq j"AtA j(AÿÿÿqM\r Aj!  N\r  AN\r A!Aä!AAA  A4"AÿM@ AÿG AúKq A!kAI A:kAIr AÛkAIrr  @@@  jAm"AtA°9j(" Au j"AtA°9j(Aÿÿÿq K@ Ak!   AvAq j"AtA°9j(AÿÿÿqM\r Aj!  N\r A! AN "Aq As qAq"!A  ! ! \n!  rE\r  kAp!@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" AtA# AG  LqAA  NA AJr" AFrA@k:  : B7  \n6  6 \n \r"M\r@@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"AÄ; B7  \r6  \r6 \rAj"\r \nI\r ! \n! @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x" Aj6x (!  Alj"Aÿ ; B7  6  6A! Aj$@ " \r A (xAÏAJ@ (à"AN@ Ak!\r A€j! AØj! A j!@ (t" Al"j" (! @ -"AÄF\r ( !A! A! AN@   Alj" ( Alj! Al! (! -A q@ A!: Ak"6A!!  j!  I@ ( I\r  I@ ( I\r @  I@ A!G\r  AÛG\r ( A€€ÀqE\r (" (O\r ( j-AÞG\r Aj"6 (t j"(" M\r  ( j  kK"E\r ( "E@ (pAj"6p  6  (Aj6 6 (6 -A”r:  -A˜r: (! (! (  @@@@ -\rA qE\r -AÛG\r E\r (" ("kAG\r -AÛG\r E\r ( ("kAG\r -AÝG\r  AkG\r ( ("kAG\r  (AjG\r@@ Aj" N"@ !  (t! !@@  Alj"\n-"AÄF\r AüF\r \n( kAäK\r \n-AqE\r \n( ! Aj" H\r A!\n \n  \n(" F\r  kAäK\r  I@ (!@@  j-A\nk Aj" G\r A!@   Atj"("L@ (t! @ Alj( " N\r  6  ("L@ (t! @ Alj( " N\r  6 Aj"AG\r \n@@ \n( (I@@  L\r  kAk! (t! A!  AsjAq"@@ AljAÄ; Aj! Aj" G\r AI\r@ Alj"AÄ;L AÄ;8 AÄ;$ AÄ; Aj" G\r \n \n-Ar: \n(6  @ \r  kAk! (t! A!  AsjAq"@@ AljAÄ; Aj! Aj" G\r AI\r@ Alj"AÄ;L AÄ;8 AÄ;$ AÄ; Aj" G\r  \n(6 (6  (6 (t" j" 6 j" 6  -Ar: -Ar: (! (!  Aj L (  (6  (6 (t" j" 6 j" 6  -Ar: -Ar: (! (! (  \r (!  (" ("G\r ( AjK@ -AÀq\r    ( AjM  -AÀq\r   (  AjM "AH\r E\r  (6 (t Alj(!  @  ("O\r (" j-A(G\r Aj!\nA! \r!@@@   jAv"Atj"( K@ E\r  AtjAk( I\r Ak!   (M\r Aj!  O\r A! @@ Aj"  Atj(" I@@  j-A k"AK\rA tA€€qE\r Aj" G\r !  6@  O\r  j-A\nk Aj" O\r  Atj(!  6 @  O\r  j-A)G\rA! A6 B7  6  6 Aj! A    Atj"(" Aj Aj 5E\r@   k ( Aj A j A,j A(j A$j6@ ( " j! (," F!   j"Atj(! (!   ("6(  6$A!A! @  I@@  j-"A k"AK\rA tA€€qE\r Aj" G\r Aj" O\r   Atj("j-! AÿqA)G\r  Aj" 6 ($" (("M@A! A6 B7A   k! @ A6  6   j"6A  @ j"@  Atj!A! !@   Atj("  K" KA!  "kAq"@@  ( j-: Aj! Aj! Aj" G\r  kA|M@@  ( j-:  ( j-:  ( j-:  ( j-: Aj! Aj" G\r   O\r A\n: Aj!  Aj"Atj(! ($" E\rAä (,   A6  6   k6 (! A ! (x" \nL@  6  (t! \n!@  Alj"(K@@ -AqAG\r  ( "Alj( I\r \r k  Aj" H\r  6  \nL\r  kAk! (t! A!  AsjAq"@@ \nAljAÄ; \nAj!\n Aj" G\r AI\r@ \nAlj"AÄ; AÄ;L AÄ;8 AÄ;$ \nAj"\n G\r  -AÀq\r   ( ( AjM"AH\r E\r -Ar:  -Ar: Aj" (tj (6 (t j ( 6  (tj (6( Aj! (t! (@  Alj (Ð6 6Ð  Alj (6 (! (! -!  Aj L   AÛF"!   !   !   ! ! / AŒqE\r@@ "Aj! -$"AÄG" \r  I\r À! !@@ "Ak! \r  K\r -%" AqE\r E\r ( (G\rA‡ APE\r (  (tkAmG\r Ak( (G\r AÄ:$  Aïq:% AkAÄ: Ak" -Aïq: "AN\r B7à A6¸ @A! A6Ô (x"AL\r (t! A!@@ Alj"-"Aq\r -AüG\r ( (kAG\r A6  Ar:@ (ØAH@ 6Ø  (ÜAlj 6 6Ü Aj"6Ô Aj" G\r A!  A! A (xL  A! A0j$ Þ#Aðk"$ B7h B7` B7X B7P B7H B7@ B78 B70 A6( B7 B7 B7 B7A! @    Aj AÐj9\r  A Aj A0j9\r  6( (,! @  Aj (" E@A!  ($"E\rAÒ (,    Aj (" E@A!  ($"E\rAö (,  (`@ (Pk (Tk (Xk (@@ (0k (4k (8k Aðj$ Ý@ ( " ("I@ (!  ( Av jA "At"l"E@A ($"E\rAõ (, A  6 ( Ajl"E@A ($"E\rAõ (, A  6  6 ( ! ( Atj 6  ( Atj 6  ( Aj6 A Ü Aj!@@@ Aj" O\r j-A#G\r j-A rAøG\r !@@@@ j-"A0kAÿqA\nI\r AÁkAÿqAI\r  kAK\r AákAÿqAI\r   kAK\r Aj" G\r !  F\r  kAI\r  @ Aj" O"\r j-A#G\r !@@  kAK\r j-A0kAÿqA K\r Aj" G\r !  F\r  kAI\r   M\r j-AßqAÁkAÿqAK\r  \r@@ j-"AßqAÁkAÿqAO@   kA0K\r A0kAÿqA\nI\r    kA0M\r   Aj" G\r  " kAkA.K\r  M\r j-A;G\rA!  Aj6  ³#Ak"$@@  O\r (!@  j-A k"AK\rA tA€€qE\r Aj" G\r !   O\r (!@  jAk-A k"AK\rA tA€€qE\r Ak" K\r !  6  6  6@  Aj (, ("@ ($"E\rAš (,   AjA<"\r  Aj (, ("E@A!  ($"E\rA¿ (,  Aj$  à!#A0k"\r$  H@ A¸j! A€j! \rAr! \rAr! (!@@ (t" Al" j" -"Aq@ AqE\r@ E\r ,"E\r  [\r ( !  ,"\nAÿq"E\r  \n[E\r (" I\r@@@@@@@@@@@@@@@@@ A!k^  (! Aq@ AN@  Alj" -AÀr: 6  6  AH\r  Alj"( 6¸  6 6@ (ä" AN@  Alj 6  6à 6ä A6  Aj" (xN\r  Alj"-A;G\r (  (" \rHE\r 6  6 Ar:  -Ar: 6  AqE\r \r A€A˜ \nA*Fj" A j6 AÀq"AÀF@A!   \r Aj6 A€F\rA!  Aj6A! A°A´ ( kAFj!\n@ AqE\rA! \n("AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6 6  -Ar: -Ar: \r AqE\r \n(6 \n 6 @ AqE\rA! (Ä"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6 6  -Ar: -Ar: AqE\r (Ä6 6Ä @ AqE\r (¼"AH\rA!  Alj" ( (k ( kG\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r @ Aj" N\r  kAk!A!  AsjAq"\n@@  AljAÄ; Aj! Aj" \nG\r AI\r@  Alj" AÄ;L AÄ;8 AÄ;$ AÄ; Aj" G\r 6 6 -Ar: -Ar: A6¼ AqE\r\n (¼6 6¼ \n  Aj"Al"j"(! (! \r 6 \r ("6, \r 6( \r 6$ \nAÀF@   \rAAAA¨ \rA(jOAL\r\n \r(!  O\r N"AÿM\rA!A!@@@  jAm" AtA j("Au j"AtA j(Aÿÿÿq K@ Ak!   AvAq j"AtA j(AÿÿÿqM\r Aj!  N\r  AN\r  ( kAG\r@ AqE\rA! (À"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6 6  -Ar: -Ar: AqE\r (À6 6À  ( kAG\r@ AqE\rA! (È"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6 6  -Ar: -Ar:  AqE\r (È6 6È  ( kAG\r@ AqE\rA! (Ì"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6 6  -Ar: -Ar:  AqE\r (Ì6 6Ì  A!A!  A F\r A F\r A kAI\r ( jAk,"@Aó AP\r \r((" I\r Ak!@@@ -AÄF\r ( K\r  (O\r -" Aq\r Ak" O\r  AqE\r   \rA,jAA.AAè \rA$jOAH\r \r(,!@ \nAÀF\r@  O\r (" j-A/G\r Aj  \rA,jAA/A‰A \rA$jOAH\r \r(," O\r  j-A/G\r \r Aj"6, @  O\r ( j-A?G\r Aj  \rA,jAA&AƒA¥ \rA$jOAH\r \r(,!  O\r ( j-A#G\r Aj  \rA,jAAAAë \rA$jOAH\r \r(,! @  O\rA!A!@ ( j" ( kA4"AÿM@ A F\r A F\r A kAO\r  @@@  jAm" AtA j("Au j"AtA j(Aÿÿÿq K@ Ak!   AvAq j"AtA j(AÿÿÿqM\r Aj!  N\r  AN\r ,"@A± A P\r \r($"  (xAlj"O\r@@@ -AÄF\r ( M\r ( K\r -"Aq\r Aj" I\r  AqE\r 6 6  \n:  6  6 (t" j" 6  j" 6  -Ar:  -Ar:  \r Atr 6 A q! Aj! AàqAàG@ \r Atj Aj6 Aj! As AGqE@ \r Atj Aj6 Aj! Aq! Aq!A!A!A! A!@@ \r Atj"(("AH\r  Alj! @ ( (M\r ! ! @ (("AH\r  Alj! @ ( (M\r ! ! Aj! Aj" G\r @ E\r \r Atj((" AH\r  Alj! @ ( (M\r ! ! E\r (! (! (! !\n@@@@@@  Alj"-"A!k\n AÛk A€A˜ A*Fj -"AtAuA qj!\n@ AvAk \nAj!\n  A°A´ ( (kAFj!\n  \nAj!\n @  k"  k" K@  (" (k k j"Al"j" (6  )7  )7   k" 6  6 (t j \n(6 \n 6   O\r  Alj j" (6  )7  )7  k j"6  6 A!\n@   \nAtj"("L@@ AN@  (t Alj( "6  N\r \nAj"\nAG\r (t" Alj" 6  j" 6  -Ar:  -Ar:  AqE\r A€A˜ \nA*Fj AtAuA qj!@@@ AvAk Aj!  Aj! (6  6 -"AqE\r Aq@ (t ( Alj(!  (! Aj" H\r \rA0j$ –#Ak"$@ (E\r  3! (  (pAtj("E@A!  @  ("I\r   ( (ljO\r  (F@ ( (  AE\r A!   6  6  6  Aj6 Aj!@ ("@@   Av"Atj"D"AH@   E\r Aj!  Asj "\r A! E@A!  (! Aj$  ý#A k"$ Aß: A¦Ô;A! ( "A€ q@ A$:A! A€€€q@ Aj rA=: Aj! A€€€q@ Aj rA+: Aj! A€€q@ Aj jAü: Aj! A€€q@ Aj jAÞ: Aj! A€„q@ Aj jAþ: Aj! A! Aj jA:    AjAJ ( "AŒq@ Aq@ AÀ:A! Aq@  rA:: Aj! A€q  rA.: Aj  rA:     AjJ A€jAÿAÐü A j$ ‰ (TE@A Ak! Ak!AA ("\n j-A!F j!A!@@   jAv"Atj" ( K@ E\r  AtjAk( I\r Ak!   (M\r Aj!  @  (M" E@@  kj"@ !@  Atj("  K" KA!  "kAq"\n@@  ( j-: Aj! Aj! Aj" \nG\r  kA|M@@  ( j-:  ( j-:  ( j-:  ( j-: Aj! Aj" G\r   O\r A : Aj! Aj" Atj(! A! ($"E\rAä (, A  k   \nj!  k ! A  Aj60A! Aj !\nA!@@ \n("E\r Aj i"AH\r   kK\r \nAj!\n  j" I\r A=! AH\r A \r  e E@A!  A! (0!\n@ \n(" E\r Aj i" j" K\r Aj a \nAj!\n  K\r A \r  A€Àse \r   \rH!   AHq\r A=! +0 \r   f"AN\r \n -!\n Aj! \r E\rA!@  Atj("\n@  Atj \n cA! Aj"A\nG\r A\nO@A! \n @  Atj(\rA! Aj"A\nG\r A!   :\'A! ! !   k"  H" AÿÿÿÿsJ\rA=! \r  j"  \rH" \nK\r A   e  a A0   A€€se A0  Ae a A   A€Àse ( Ak" O\r B€”ëÜT\r Ak" > @ "I@ Ak"(E\r (¬ k"6¬ ! AJ\r AH@ AjA nAj! AæF!@A A k" A O!\n@  M@AA (!  A€”ëÜ \nv!\rA \ntAs!A! !@  (" \nv j6  q \rl! Aj" I\r AA (! E\r  6 Aj! (¬ \nj"6¬   j" " Atj   kAu J! AH\r A!@  M\r  kAuA l!A\n! ("A\nI\r@ Aj!  A\nl"O\r A AæGk AçF AGqk"  kAuA lA kH@ A„`Aìc AHj A€Èj"A m"\rAtj!\nA\n!  \rA lk"AL@@ A\nl! Aj"AG\r @ \n("  n" lk"\rE \nAj" Fq\r@ AqE@D@C! A€”ëÜG\r \nO\r \nAk-AqE\r D@C! Dà?Dð?Dø?  FDø? \r Av"F \r I!@ \r -A-G\r š! š! \n  \rk"6    a\r \n  j"6 A€”ëÜO@@ \nA6 \nAk"\nK@ Ak" A6 \n \n(Aj"6 Aÿ“ëÜK\r  kAuA l!A\n! ("A\nI\r@ Aj!  A\nl"O\r \nAj"   K! @ " M"E@ Ak"(E\r @ AçG@ Aq!\r  AsA A " J A{Jq"\n j! AA~ \n j! Aq"\r\rAw!@ \r Ak("\nE\rA\n!A! \nA\np\r@ "\rAj! \n A\nl"pE\r \rAs!  kAuA l! A_qAÆF@A!\r  jA k"A AJ"  J!  A!\r  j jA k"A AJ"  J! A!\n AýÿÿÿAþÿÿÿ \rr"J\r AGjAj!@ A_q"AÆF@  AÿÿÿÿsJ\r A AJ!    Au"s k­ d"kAL@@ Ak"A0:  kAH\r Ak" : AkA-A+ AH:  k" AÿÿÿÿsJ\r  j" AÿÿÿÿsJ\r A   j" e  a A0   A€€se@@@ AÆF@ AjA r!  K"! @ 5 d!@  G@  AjM\r@ Ak"A0:  AjK\r   G\r Ak"A0:   ka Aj" M\r @ A‰Aa  M\r AL\r@ 5 d" AjK@@ Ak"A0:  AjK\r A A Na A k! Aj" O\r A J! ! \r  @ AH\r  Aj  K!\n AjA r! !@  5 d"F@ Ak"A0: @  G@  AjM\r@ Ak"A0:  AjK\r  Aa Aj! \rrE\r A‰Aa   k"  Ha k! Aj" \nO\r AN\r A0 AjAAe   ka  ! A0 A jA Ae A   A€Àse    J!\n   AtAuA qj!@ A K\r -!Dð?!@A4 Atk"A€N@Dà! AÿI@ Aÿk!  Dð!Aý  AýOAþk!  AxJ\rD`! A¸pK@ AÉj!  D!Aðh  AðhMA’j!  Aÿj­B4†¿¢! A-F@  š ¡ š!     ¡!  (¬" Au"s k­ d"F@ Ak"A0: (¬! Ar! A q! Ak"\r Aj: AkA-A+ AH: AqE ALq! Aj!@ " ü"AÀÐj- r:  ·¡D0@¢!@ Aj" AjkAG\r Da q\r A.: Aj! Db\r A!\n Aûÿÿÿ   \rk" jkJ\r A  Aj  Ajk" Ak H  " jj" e  a A0   A€€se Aj a A0  kAAe \r a A   A€Àse    J!\n A°j$ \n ¹~  (AjAxq"Aj6 )! )!#A k"$ Bÿÿÿÿÿÿ?ƒ!~ B0ˆBÿÿƒ"§"AøkAýM@ B† B<ˆ„! A€øk­!@ Bÿÿÿÿÿÿÿÿƒ"B€€€€€€€Z@ B|!  B€€€€€€€€R\r Bƒ |! B  BÿÿÿÿÿÿÿV"! ­ |  @  „P\r BÿÿR\r B† B<ˆ„B€€€€€€€„!Bÿ  Aþ‡K@B!Bÿ  A€øAø P"\n" k" AðJ@B!B   B€€€€€€À„ \n!A!\n G@ Aj! ! !@A€ k"AÀq@  A@j­†!B!  E\r  ­"† AÀ k­ˆ„!  †! 7 7 ) )„BR!\n @ AÀq@  A@j­ˆ!B!  E\r AÀ k­†  ­"ˆ„!  ˆ!  7  7 )B† )"B<ˆ„!@ \n­ Bÿÿÿÿÿÿÿÿƒ„"B€€€€€€€Z@ B|!  B€€€€€€€€R\r Bƒ |! B€€€€€€€…  BÿÿÿÿÿÿÿV"! ­ ! A j$ B€€€€€€€€€ƒ B4†„ „¿9 ¦ (T"(! (" ( ("k"  I"@   Q  ( j"6  ( k"6    K"@   Q  ( j"6  ( k6 A: (,"6 6  ¢ E@A @  AÿM\r@AÔÚ((E@ A€qA€¿F\rAèÛA6  AÿM@ A?qA€r: AvAÀr:A  A€@qA€ÀG A€°OqE@ A?qA€r: A vAàr: AvA?qA€r:A  A€€kAÿÿ?M@ A?qA€r: AvAðr: AvA?qA€r: A vA?qA€r:A  AèÛA6 AA  :A Ñ( #Ak"\n$@@@@@@@@@@ AôM@A¤Ü("A A jAøq A I"Av"v"Aq@@ AsAq j"At"AÌÜj" (Ô\\"("F@A¤Ü A~ wq6   6 6 Aj!  Ar6  j" (Ar6 A¬Ü("M\r @@ tA t"A krqh"At"AÌÜj" (Ô\\"("F@A¤Ü A~ wq"6   6  6 Ar6 j"  k"Ar6 j 6 @ AxqAÌÜj!A¸Ü(! A Avt"qE@A¤Ü  r6   ( !  6  6  6  6 Aj!A¸Ü 6A¬Ü 6 A¨Ü(" E\r hAt(Ô^"(Axq k! !@@ ("E@ ("E\r (Axq k"   K"!  ! !  (!  ( "G@ (" 6 6 \n (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6 A! A¿K\r A j"Axq!A¨Ü(" E\rA! AôÿÿM@ A& Avg"kvAq AtkA>j! A k!@@@ At(Ô^"E@A!  A! A AvkA AGt!@@ (Axq k" O\r ! "\rA! !  ("   AvAqj("F ! At! "\r rE@A!A t"A kr q"E\r hAt(Ô^! E\r @ (Axq k" I!   !  ! ("  ( "\r E\r A¬Ü( kO\r (!  ( "G@ (" 6 6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  A¬Ü("M@A¸Ü(!@ k"AO@  j" Ar6 j 6  Ar6   Ar6 j" (Ar6A!A! A¬Ü 6A¸Ü 6 Aj! A°Ü("I@A°Ü  k"6A¼ÜA¼Ü(" j"6  Ar6 Ar6 Aj! A! A/j"Aüß(@A„à(  AˆàB7A€àB€ €€€€7Aüß \nA jApqAتժs6AàA6AàßA6A€ "j"A k"q" M\rAÜß("@AÔß(" j" M\r  I\r @Aàß-AqE@@@@@A¼Ü("@Aäß!@ (" M@   (jI\r ("\r An"AF\r !A€à("Ak" q@  k  jA kqj!  O\rAÜß("@AÔß(" j" M\r I\r n" G\r   k q"n" ( (jF\r ! AF\r A0j M@ !  A„à("  kjA kq"nAF\r  j! !  AG\r AàßAàß(Ar6 n!An! AF\r AF\r M\r k" A(jM\r AÔßAÔß( j"6AØß( I@AØß 6 @A¼Ü("@Aäß!@  (" ("jF\r ("\r  A´Ü("A ME@A´Ü 6 A!Aèß 6Aäß 6AÄÜA6AÈÜAüß(6AðßA6@ At" AÌÜj"6Ô\\  6Ø\\ Aj"A G\r A°Ü A(k"Ax kAq"k"6A¼Ü  j"6  Ar6 jA(6AÀÜAŒà(6   O\r  I\r ( Aq\r  j6A¼Ü Ax kAq"j"6A°ÜA°Ü( j" k"6  Ar6  jA(6AÀÜAŒà(6  A!  A!  A´Ü( K@A´Ü 6  j!Aäß!@@  ("G@ ("\r  - AqE\r Aäß!@@ (" M@   (j"I\r (!  A°Ü A(k"Ax kAq"k"6A¼Ü  j"6  Ar6 jA(6AÀÜAŒà(6  A\' kAqjA/k" AjI"A6 Aìß)7 Aäß)7Aìß Aj6Aèß 6Aäß 6AðßA6 Aj!@ A6 Aj! Aj!  I\r  F\r  (A~q6   k"Ar6  6 AÿM@ AøqAÌÜj!A¤Ü("A Avt"qE@A¤Ü  r6  ( ! 6  6 A !A  A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÔÞj!@@A¨Ü("A t"qE@A¨Ü  r6  6  6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6  6 A! ! !A  (" 6  6  6A!A!A j 6  j 6 A°Ü(" M\rA°Ü k"6A¼ÜA¼Ü(" j"6  Ar6 Ar6 Aj!  AèÛA06A!  6 ( j6 Ax kAqj" Ar6 Ax kAqj"  j"k!@A¼Ü( F@A¼Ü 6A°ÜA°Ü( j"6  Ar6  A¸Ü( F@A¸Ü 6A¬ÜA¬Ü( j"6  Ar6  j 6  ("AqAF@ Axq! ( !@ AÿM@ (" F@A¤ÜA¤Ü(A~ Avwq6  6  6  (!@  G@ (" 6  6  @ (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  A! E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨ÜA¨Ü(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6  j"(!  j!  A~q6  Ar6  j 6 AÿM@ AøqAÌÜj!A¤Ü("A Avt"qE@A¤Ü  r6   ( !  6  6  6  6  A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÔÞj!@@A¨Ü("A t"qE@A¨Ü r6  6  6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6  6  6  6  (" 6  6 A6  6  6 Aj!  @ E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨Ü A~ wq" 6  @  (F@  6   6 E\r 6 ("@ 6  6 ("E\r 6  6 @ AM@   j"Ar6 j" (Ar6   Ar6  j" Ar6  j 6 AÿM@ AøqAÌÜj!A¤Ü("A Avt"qE@A¤Ü  r6  ( ! 6  6  6  6  A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÔÞj!@@ A t"qE@A¨Ü  r6  6  6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6  6  6  6  (" 6  6 A6  6  6 Aj!  @ E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨Ü A~ wq6  @  (F@ 6  6 E\r 6 ("@ 6  6 ("E\r 6  6 @ AM@   j"Ar6 j" (Ar6   Ar6  j" Ar6  j 6 @ AxqAÌÜj!A¸Ü(!A Avt" qE@A¤Ü  r6   ( !  6  6 6 6 A¸Ü 6A¬Ü 6 Aj! \nAj$ ‚ @ E\r Ak" Ak("Axq"j!@ Aq\r AqE\r  ("k"A´Ü(I\r j!@@@A¸Ü( G@ ( ! AÿM@  ("G\rA¤ÜA¤Ü(A~ Avwq6  (!  G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  ("AqAG\rA¬Ü 6  A~q6  Ar6  6  6  6  A! E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨ÜA¨Ü(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6  O\r ("AqE\r@@@@ AqE@A¼Ü( F@A¼Ü 6A°ÜA°Ü( j"6  Ar6 A¸Ü(G\rA¬ÜA6A¸ÜA6 A¸Ü(" F@A¸Ü 6A¬ÜA¬Ü( j"6  Ar6 j 6 Axq j! ( ! AÿM@ (" F@A¤ÜA¤Ü(A~ Avwq6   6  6  (!  G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6   A~q6  Ar6 j 6  A! E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨ÜA¨Ü(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6  Ar6 j 6  G\rA¬Ü 6 AÿM@ AøqAÌÜj!A¤Ü("A Avt"qE@A¤Ü r6   ( !  6 6  6  6 A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÔÞj!@A¨Ü("A t"qE@A¨Ü  r6  6A!A  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6A! !A ! !   (" 6  6A!A!A !  j 6  6 j 6AÄÜAÄÜ(Ak"A 6 ‰  E@ j A@O@AèÛA06A A A jAxq A I! Ak"("Axq!@ AqE@ A€I\r Aj M@ !  kA„à(AtM\r A   j!@  O@  k"AI\r   AqrAr6  j" Ar6  (Ar6  m  A¼Ü( F@A°Ü( j" M\r   AqrAr6  j"  k"Ar6A°Ü 6A¼Ü 6  A¸Ü( F@A¬Ü( j" I\r@  k"AO@   AqrAr6  j" Ar6  j" 6  (A~q6   Aq rAr6  j" (Ar6A!A! A¸Ü 6A¬Ü 6  ("Aq\r Axq j"\n I\r \n k! ( !@ AÿM@ (" F@A¤ÜA¤Ü(A~ Avwq6   6  6  (! @  G@ (" 6  6  @ (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  A! E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨ÜA¨Ü(A~ wq6  @  (F@ 6  6 E\r  6 ("@  6  6 ("E\r  6  6 AM@  Aq \nrAr6  \nj" (Ar6    AqrAr6  j" Ar6  \nj" (Ar6  m !  "@ Aj j"E@A  A|Ax Ak("Aq Axqj"   KQ k  «  j!@@ ("Aq\r AqE\r (" j!@@@ k"A¸Ü(G@ ( ! AÿM@  ("G\rA¤ÜA¤Ü(A~ Avwq6  (! G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  ("AqAG\rA¬Ü 6  A~q6 Ar6  6  6  6  A! E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨ÜA¨Ü(A~ wq6  @ (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6 @@@@ ("AqE@A¼Ü( F@A¼Ü 6A°ÜA°Ü( j"6 Ar6 A¸Ü(G\rA¬ÜA6A¸ÜA6 A¸Ü(" F@A¸Ü 6A¬ÜA¬Ü( j"6 Ar6 j 6 Axq j! ( ! AÿM@ (" F@A¤ÜA¤Ü(A~ Avwq6   6  6  (!  G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6   A~q6 Ar6 j 6  A! E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨ÜA¨Ü(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6 Ar6 j 6 G\rA¬Ü 6 AÿM@ AøqAÌÜj!A¤Ü("A Avt"qE@A¤Ü  r6   ( !  6  6 6 6 A! AÿÿÿM@ A& Avg"kvAq AtrA>s! 6 B7 AtAÔÞj!@@A¨Ü("A t"qE@A¨Ü  r6  6 6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6 6 6 6 (" 6  6 A6 6 6 W~@ ­B|BøÿÿÿƒAØÚ("­|"BÿÿÿÿX@ §"?AtM\r \r AèÛA06A AØÚ 6 ß  #Ak"$  6 @ AÓM@AÐÐAÒ A jp(!  A|O@]  AÒn"AÒl"k6AÒAÐÓ Ajp"( j! AÒkAu!@A! !@@@ ! A/F@AÓ!@ n" I\r  lF\r A\nj"n" I\r  lF\r A j"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r A$j"n" I\r  lF\r A(j"n" I\r  lF\r A*j"n" I\r  lF\r A.j"n" I\r  lF\r A4j"n" I\r  lF\r A:j"n" I\r  lF\r AA AM!@@ j"\rA”à("E\r \n  "E@u ]  \\w ¬@  (AÿÿÿÿqAkA\n , "AH"" (  "kM@ E\r ( AH! @  j  ü\n  j!@ , AH@ 6  Aÿq:  jA:   j k  A  x …Aöÿÿÿ k O@A÷ÿÿÿ! ( , AH! AòÿÿÿM@A  j" At"  K"ArAj A I! t!@ E\r E\r  ü\n @ E\r E\r  j  ü\n   j"k!\n@  F\r \nE\r  j j  j j \nü\n A\nG@ k 6 A€€€€xr6  j \nj"6  jA: u Å@@@ , "AN@A\n! A\nF\r AjAÿq:  (" (AÿÿÿÿqAk"G\r !@ AöÿÿÿG@A÷ÿÿÿ! ( , AH! AòÿÿÿM@A Aj" At"  K"ArAj A I! t!@ E\r E\r   ü\n @  F\r  k"E\r  j  j ü\n A\nG@ k 6 A€€€€xr6  u 6 ! Aj6 (! j"A: : « A÷ÿÿÿI@ (AÿÿÿÿqAkA\n , "AH" I@A ArAj A I"t! (!@ (  "Aj"E\r E\r   AH ü\n AH@ k 6 6 A€€€€xr6  u h (Aÿÿÿÿq" K@ 6 (!@ E\r E\r   ü\n  jA: Ak  kAj ("A   x Q A\nM@ : @ E\r E\r  ü\n jA: A\n A\nk - Aÿq"A  x #A k"$#Ak"$ Aj" A j" Aj"k"A L@A= A ArgkAÑ lA v"  At(°TIkAjH\r  A¿„=M@ AÎM@ AãM@ A M@  A0r: Aj   At/àT; Aj  AçM@  AÿÿqAän"A0r:   AälkAÿÿqAt/àT; Aj   ~  AŸM@  AÎn"A0j: Aj  AÎlk~     AÿÁ×/M@ Aÿ¬âM@  AÀ„=n"A0j: Aj  AÀ„=lk   €  Aÿ“ëÜM@  A€Â×/n"A0j: Aj  A€Â×/lk€   A€Â×/n"At/àT; Aj  A€Â×/lk€ !A 6  6 A j" (6  ( 6 Aj$@ ( " Aj"k"A÷ÿÿÿI@@ A\nM@ :  Ar"Ajt! Aÿÿÿÿk6 6 6 !  k!@  F\r E\r  ü\n jA:  u A j$ 2 Aän"At/àT;  AälkAt/àT; Aj * AÎn"At/àT; Aj  AÎlk~ * AÀ„=n"At/àT; Aj  AÀ„=lk  AÐjjAÐj  t E@ ( (F F@A ("-!@ ("-"E\r G\r@ -! -"E\r Aj! Aj! F\r F Ú#AÐk"$@A ( (F\r#A@j"$  ("Ak("j!@ Ak("(AÜÖ(F@A  !   "L@ B7 A6 AØÖ6  6  6 B7 B7$ B7, A6< B€€€€€€€74  Aj  AA (( (\r B7 A6 A¨Ö6  6 AØÖ6 B7 B7$ B7, B73 A6< A:;  Aj AA (( A!@@ (( (A ($AFA ( AFA (,AF!  (AG@ (,\r ( AG\r ($AG\r (! A@k$A "E\r ("E\r AjAA8ü A:K A6  6  6 A6D  Aj A (( (,"AF@  ($6 AF ! AÐj$  A6 Aç6 AŽ 6] v ($"E@ 6 6 A6$ (86 @@ ( (8G\r ( G\r (AG\r 6 A:6 A6 Aj6$  ( ((F@   … 5 ( ((F@   … ("    (( { ("Aq! -7AF@ Au" E\r ( j(  Au E\r  ((68A!A ! ("   j A Aq (( r ( ((F@   … ( ! Aj"   ˆ@ AI\r  Atj! Aj!@   ˆ -6\r Aj" I\r š A:5@  (G\r A:4@ ("E@ A6$ 6 6 AG\r (0AF\r   F@ ("AF@ 6 ! (0AG\r AF\r  ($Aj6$ A:6 Â@ ( ƒ@  (G\r (AF\r  6 @ ( ƒ@@ ( G@  (G\r AG\r A6   6 (,AF\r Aj" ( Atj!@@@ @  M\r A;4    A Œ -6\r -5AG\r -4AF@ (AF\rA!A! -AqE\r  A! -Aq\rA  AA Aq 6, \r  A6,  Aj! ( ! Aj"     AI\r  Atj! Aj!@ ("AqE@ ($AG\r @ -6\r      Aj" I\r  AqE@@ -6\r ($AF\r      Aj" I\r  @ -6\r ($AF@ (AF\r      Aj" I\r   6  ((Aj6( ($AG\r (AG\r A:6 K ("Au! ("   Aq ( j(  j A Aq  (( I ("Au! ("  Aq ( j(  j A Aq  (( Š@ ( ƒ@  (G\r (AF\r  6 ( ƒ@@ ( G@  (G\r AG\r A6   6 @ (,AF\r A;4 ("   A  (( -5AF@ A6, -4E\r  A6,  6  ((Aj6( ($AG\r (AG\r A:6 ("     (( ¦@ ( ƒ@  (G\r (AF\r  6 ( ƒE\r@ ( G@  (G\r AG\r A6   6  6  ((Aj6(@ ($AG\r (AG\r A:6 A6, › ( ƒ@    Š -5! ( ! A:5 -4! A:4 Aj"     Œ  -4"\nr!  -5" r!@ AI\r Atj! Aj!@ -6\r@ \nAq@ (AF\r -Aq\r  AqE\r -AqE\r A;4      Œ -5" rAq! -4"\n rAq! Aj" I\r  Aq:5  Aq:4 9 ( ƒ@    Š ("      ((  ( ƒ@    Š A§ 7 AàÙ6 ("Ak" (Ak"6 AH@ A kk ” k  (  $ # kApq"$ # ‘P(A€ Þ{"type":"Document"},"attributes":{summarytbodyTableBodyLatexMathDisplay\\u%04x-+ 0X0x-0X+0X 0X-0x+0x 0xTableRowdivnavmenuTextUnorderedListOrderedListstartSuperscriptSubscripttfootcountbodyRowCountheadRowCountbasefontDocumentimportantdefaulttightrightHighlightleftframesetfieldsetdt\\taddressdetailsEmphasisnoframes%s:%d: %strvectordirhrfootercenterSpoilerheaderfenceChar\\roptgroupcolgroup/Users/ernest/react-native-enriched-markdown/packages/core/cpp/wasm/ASTSerializer.cpp/emsdk/emscripten/system/lib/libcxxabi/src/private_typeinfo.cpptipcautionoptionfigcaptionAdmonitionsectionmainalignnan\\nformmenuitemListItemparamulurlcolhtmlTableHeaderCellTableCellleveldlisTasklinkLinkCodeBlocktrackSoftBreakLineBreakThematicBreaklibad_array_new_lengthParagraphStrikethroughsearchdialogStrongbasic_stringnodeTypeToStringwarningHeadinginf\\ftrueblockquoteBlockquotenotefalsebasefigureadmonitionTypeUnderlineLatexMathInlineBlankLineiframetitlearticletableTableImageCodeasidetdlegendtaskCheckedddtheadTableHead\\b"\'=<>`-_.-+_\\\\({[,"children":[NANINFcatching a class without an object?)}].!?,;mailto:,"content":h6h5h4h3h2h1http://Too many link reference definition instantiations.Unsupported abi_version.Aborted from text() callback.Aborted from enter_span() callback.Aborted from leave_span() callback.Aborted from enter_block() callback.Aborted from leave_block() callback.malloc() failed.realloc() failed.@:..!#$%&\'*+/=?^_`{|}~-_.:-+-.-_+-_*(null)Suppressing too sparse table (see https://github.com/mity/md4c/issues/345)._=().+-_~%false && "unhandled NodeType in nodeTypeToString"\\"{"type":"[]!xX \nñ<‡í bOFß*$®ŒšŸòT2q¾Îé<Ь¸ùlAè ~€ ¸+Œ ô)NSt3__220__shared_ptr_emplaceIN8Markdown15MarkdownASTNodeENS_9allocatorIS2_EEEE"ƒA€ ¹Nú‡RA  f ° à \rˆ P\rˆ \rð\rˆ ˆ  @`€ˆ ˆ  Àˆ ˆ ˆ ˆ ˆ A ?/A° Äè£\n-Aà dƒµ¯A 5KG[uzÞ9AÐ 50a\nÉ|‘\'A MÝÚ×ÔÑÎO’y¹Að A€ 8$èA  ‚–AÀ \r~XAà ¶¦ZA€ \rP¨A  Tw$AÀ E,5Â9N\nküA ®A  —  € @\n €/ _ 0A@Z€µÀ@րØ@ހ@.€2@6€9@G€J@v€xy@}€‚„†‡‰Š‹Ž‘“”–—˜œŸ @¤€¦§©¬®¯±²³µ·¸¼ÄÅÇÈÊË@Û€Þ@î€ñòôö÷ø@€ "@2€:;=>ACDEF@N€Eprv†ˆ@Š€ŒŽ‘@¡€£@«€ÂÏÐÑÕÖØ@î€ðñôõ÷ùúý@ÿ€@€@/€`@€€Š@¾€ÀÁ@Í€Ð@.€1@V€ @Å€ÇÍø@ý€€‚ƒ„…†‡ˆ‰@º€½@¿€@”€› @þ€@€@€(@/€8@?€H@M€Y[]_h@o€¸¹º»¾È@Ë€ØÙÚÛèéêëìøùúû&!*!+!2!`!@o!€ƒ!¶$@Ï$€,@/,€`,b,c,d,g,@k,€m,n,o,p,r,u,~,,€,@â,€ë,í,ò,@¦@l¦€€¦@š¦€"§@.§€2§@n§€y§{§}§~§@†§€‹§§§’§–§@¨§€ª§«§¬§­§®§°§±§²§³§´§@§€Ä§Å§Æ§Ç§É§Ë§Ì§@Ú§€Ü§Ý§â§õ§l«m«p«@¿«€!ÿ@:ÿ€@\'€°@Ó€p@z€|@Š€Œ@’€”•€ @² €P\r@e\r€ @¿€@n@_n€ n@¸n€@ßHßJßMßQßhß@n߀rß@~߀é@!é€az¼àöøþ/37:HKwÿz~sSƒ…TˆVWŒÝY[’`cih™oru¡¥€¨ƒ­ˆ°Š‹´¶’¹½ÆÆÉÉÌÌÜßïóóõ•¿ùž#3e,<šf,B€‰ŒGO¹qswó¬­¯ÌÍαÁÃËÃײ¸ÆÀÙïºÁ¸µøòû{}P_0Oa‹¿ÏÂÎÑ/a†-%-\'---ðõ24>ABBJcK¦ŠÐúýÿ•a¡ÿ \'07@EQSUW`g°±pq¹ruÐÑvwàáz{åxy|}ÉkåN!p!!„!Ð$é$0,_,a,k}}h,l,QqPRs,v,?@,ã,ì,î,ó,A¦m¦¦›¦#§/§3§o§z§|§y§‡§Œ§e‘§“§—§©§f\\aljž‡S«µ§Ã§”§‚Žȧʧdͧۧ›w|ö§K«L« ïAÿZÿ(OØû—¡£±³¹»¼À ò p\r…\rÀß`nn»nÓnAßIßKßNßRßißoßsßß"éCéAÀ1 Óß0Ið‡–—˜™šžP€@‡€ˆ@€@—€˜@Ÿ€ @§€¨@¯€²³´¶¼ÂÃÄÆÌÖäæòóôöüûûûûûûûûûû•ßA 3 ¥ssi¼nj e‚h1tw\ny\na¾ssʹ¹¹ ¹\'¹ ¹\'¹`¹g¹`¹g¹p¹±¹¬¹±B±¹t¹·¹®¹·B·¹¹BÁÅB|¹É¹Î¹ÉBɹfffiflststtvtetk~vtmssAÐ6 À°RTV·ÇÒÓ×âãç÷ûû¹ÅÅÅÅB±B¹·B¹¹¹¹BÅÅÅBÉB¹ffiffl@.À 5P A¤9 ¬!@/€:@@€[@`€{@~€¡@©€«@¬€®@±€´¶@¸€»¿×÷Â@Å€Ò@߀å@ë€íï@ÿ€u~„@…€‡ö‚Z@_€‰@Š€@€¾ÀÃÆó@ô€@€@€j@m€ÔÞéý@þ€@\r€ö@ù€þ@ÿ€0@>€^ˆd @e €p ò @ó €ú @û €ý v\nð\n@ñ\n€p ó @ú €w  „ O\ry\rô\r?OZ@[€@€@€468:@=€…¾@Å€Ç@Ì€Î@Ú€J@O€ž@Ÿ€û`@h€@™€m@n€›@œ€ë@í€5@6€Ô@Ö€Ø@Û€@\n€@D@E€Þ@ÿ€@€ @¦€¨@­€N@O€Z@j€t@€ü@ÿ€;@?€~@€À@Ç€Ó½¿@Á€Í@Ï€Ý@߀í@ï€ý@þ€ @\' €0 @^ €z @~ €Š @Ž €  @Ä €!@!€!@!€!@ !€!!@!€!@#!€%!\'!)!.!:!@;!€@!@D!€J!@M!€O!Š!@‹!€!@)$€@$@J$€œ$@é$€%@u\'€”\'@s+€v+@ÿ+€å,@ê,€ù,@ü,€þ,@ÿ,€p-.@..€0.@].€`.@c.€€.@™.€›.@ó.€/@Õ/€ð/@ÿ/€0@0€0@ 0€0060@70€=0@?0€›0@œ0€ 0û01@‘1€–1@Ÿ1€À1@å1€ï12@2€*2@G2€P2`2@2€Š2@°2€À2@ÿ3€ÀM@ÿM€¤@Ƥ€þ¤@ÿ¤€\r¦@¦€s¦~¦ò¦@÷¦€§@§€ §@!§€‰§@Ч€(¨@+¨€6¨@9¨€t¨@w¨€Î¨@Ϩ€ø¨@ú¨€ü¨.©@/©€_©Á©@Í©€Þ©@ß©€\\ª@_ª€wª@yª€Þª@ߪ€ðª@ñª€[«j«@k«€ë«)û²û@Òû€>ý@Oý€ý@‘ý€Èý@Ïý€üý@ÿý€þ@þ€0þ@Rþ€Tþ@fþ€hþ@kþ€ÿ@ÿ€ÿ@ ÿ€;ÿ@@ÿ€[ÿ@eÿ€àÿ@æÿ€èÿ@îÿ€üÿ@ýÿ€@€7@?€y@‰€Œ@Ž€@œ€ Ð@ü€ŸÐoWw@x€ ? P\n@X\n€\nÈ\nð\n@ö\n€9 @? €™ @œ €n\rŽ\r@\r€­É@Ê€Ð@Ø€U@Y€†@‰€G@M€»@¼€¾@Á€@@C€t@u€Å@È€ÍÛÝ@߀8@=€©Ô@Õ€×@Ø€K@O€Z@[€]ÆÁ@×€A@C€`@l€¹<@?€;D@F€â?@F€š@œ€ž@¢€@ €áA@E€p@q€÷@ø€C@O€Õ@ñ€ÿp$@t$€ñ/@ò/€nj@oj€õj7k@?k€Dk@Ek€mm@om€—n@šn€âoœ¼Ÿ¼Ì@ïÌ€úÌ@üÌ€Í@³Î€ºÎ@Ð΀ÒÎ@Ô΀ÝÎ@ý΀PÏ@ÃÏ€Ð@õЀÑ@&Ñ€)Ñ@dÑ€jÑ@lÑ€ƒÑ@„Ñ€ŒÑ@©Ñ€®Ñ@AÒ€EÒSÒ@ZÒ€]Ò@^Ò€`Ò@Ò€Ó@VÓ€ÁÖÛÖûÖ×5×O×o׉ש×Ãר@ÿÙ€7Ú@:Ú€mÚ@tÚ€vÚ@ƒÚ€…Ú@‹Ú€Û@Û€Oáÿâÿå^é@_逬ì°ì.íðî@ñî€ð@+ð€0ð@“ð€ ð@®ð€±ð@¿ð€Áð@Ïð€Ñð@õð€\rñ@®ñ€æñ@ò€ò@;ò€@ò@Hò€Pò@Qò€`ò@eò€ó@Ùö€Üö@ìö€ðö@üö€÷@Û÷€à÷@ë÷€ð÷@ ø€ø@Gø€Pø@Yø€`ø@‡ø€ø@­ø€°ø@»ø€Àø@Áø€Ðø@Øø€ù@Wú€`ú@mú€pú@|ú€€ú@Æú€ÈúÌú@Ýú€ßú@ëú€ïú@úú€û@’û€”û@ïû€úû AãÌ  AðÌ A  \n\n\n   AÁÍ ! \r\r  AûÍ  A‡Î  AµÎ AÁÎ  AïÎ AûÎ  A²Ï  AãÏ AïÏ  AÐ A©Ð € 0123456789ABCDEF \r%)+/5;=CGIOSYaegkmqƒ‰‹•—£§­³µ¿ÁÅÇÓ \r%)+/5;=CGIOSYaegkmqyƒ‰‹•—£§©­³µ»¿ÁÅÇѐ+Ø)NSt3__214__shared_countE, *Ð)NSt3__219__shared_weak_countEA´Ô œ\ndè\' †@B€–˜áõʚ;00010203040506070809101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899¸+4+<-N10__cxxabiv116__shim_type_infoE¸+d+(+N10__cxxabiv117__class_type_infoEX+ !"#$%&Ø+\'!"#()*¸+ä+X+N10__cxxabiv120__si_class_type_infoE4,+!"#,-.¸+@,X+N10__cxxabiv121__vmi_class_type_infoE°,/0+„,St9exception¸+ ,|,St9bad_alloc¸+¼,”,St20bad_array_new_lengthì,12¸+ø,|,St11logic_error-32¸+(-ì,St12length_error+D-St9type_infoAÔÚ  . 0€')}function getBinarySync(file){return file}async function getWasmBinary(binaryFile){return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={env:wasmImports,wasi_snapshot_preview1:wasmImports};return imports}async function createWasm(){function receiveInstance(instance){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();var instantiateWasm=Module["instantiateWasm"];if(instantiateWasm){return new Promise(resolve=>{instantiateWasm(info,inst=>resolve(receiveInstance(inst)))})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var HEAP16;var HEAP32;var HEAP64;var HEAP8;var HEAPF32;var HEAPF64;var HEAPU16;var HEAPU32;var HEAPU64;var HEAPU8;var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var onPreRuns=[];var noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var ___assert_fail=(condition,filename,line,func)=>abort(`Assertion failed: ${UTF8ToString(condition)}, at: `+[filename?UTF8ToString(filename):"unknown filename",line,func?UTF8ToString(func):"unknown function"]);class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var uncaughtExceptionCount=0;var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);uncaughtExceptionCount++;abort()};var __abort_js=()=>abort("");var getHeapMax=()=>536870912;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["arguments"])programArgs=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var preInit=Module["preInit"];if(preInit){if(typeof preInit=="function")Module["preInit"]=preInit=[preInit];while(preInit.length>0){preInit.shift()()}}}Module["ccall"]=ccall;Module["cwrap"]=cwrap;Module["UTF8ToString"]=UTF8ToString;var _parseMarkdown,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_parseMarkdown=Module["_parseMarkdown"]=wasmExports["parseMarkdown"];__emscripten_stack_restore=wasmExports["_emscripten_stack_restore"];__emscripten_stack_alloc=wasmExports["_emscripten_stack_alloc"];_emscripten_stack_get_current=wasmExports["emscripten_stack_get_current"];memory=wasmMemory=wasmExports["memory"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={__assert_fail:___assert_fail,__cxa_throw:___cxa_throw,_abort_js:__abort_js,emscripten_resize_heap:_emscripten_resize_heap};async function run(){preRun();var setStatus=Module["setStatus"];if(setStatus){setStatus("Running...");await new Promise(resolve=>setTimeout(resolve,1));setTimeout(setStatus,1,"")}if(ABORT)return;initRuntime();Module["onRuntimeInitialized"]?.();postRun()}var wasmExports;wasmExports=await createWasm();await run(); +-;return Module}})();if(typeof exports==="object"&&typeof module==="object"){module.exports=createMd4cModule;module.exports.default=createMd4cModule}else if(typeof define==="function"&&define["amd"])define([],()=>createMd4cModule); ++var createMd4cModule=(()=>{var _scriptName=globalThis.document?.currentScript?.src;return async function(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=true;var ENVIRONMENT_IS_WORKER=false;var arguments_=[];var thisProgram="./this.program";var scriptDirectory="";var readAsync;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;function binaryDecode(bin){for(var i=0,l=bin.length,o=new Uint8Array(l),c;i>8&c}return o}var readyPromiseResolve,readyPromiseReject;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);HEAPU8=new Uint8Array(b);HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["__wasm_call_ctors"]()}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){return binaryDecode('asm«````````````` `````||`~`|``Tenv __cxa_throwenv\r__assert_failenvemscripten_resize_heapenv _abort_js ¾¼   \r\r    \r\n     \n\n\n\np99€€@ Aã€ ¢memory__wasm_call_ctors\rparseMarkdown__indirect_function_table_emscripten_stack_restore½_emscripten_stack_alloc¾emscripten_stack_get_current¿ PA 8º \r{ !"#$%&\'()*+,~-E@G_mno{-cc¦¶³¨-µ²©-´¯¬-¸-¹-·»¼»» (\n¼è¼ AÈÜB7AÐÜA6AßÜA:AÔÜAµ(6AØÜA¹-:AÙÜA:AëÜA:AàÜAŽ/;AâÜA-:AãÜA:A÷ÜA:AìÜAç(6AðÜAë-:AñÜA:AƒÝA :AøÜA¯ )7A€ÝA· -:AÝA:AˆÝB칻֮سå7A“ÝA:AÝA:AŸÝA:A”ÝA¾(6A˜ÝAÂ/;AšÝA:A ÝA"6A¤ÝB‹€€€€‚€€€7 A£(6 Aœ)7 A: A·ÝA:A¬ÝA¼ (6A°ÝAÀ -:A±ÝA:AÃÝA:A¸ÝAÞ (6A¼ÝAâ -:A½ÝA:AÄÝA"6AÈÝBŽ€€€€‚€€€7 A³)7 A­)7 A:AìÞAôÝ6AÄÞA€€€6AÀÞAã€6A¤ÞA*6AÈÞAÀÜ(6 AÓÜ,AH@AÐÜ(AÈÜ(r ó‹!~#A0k"$@ E@AÈÜ„!AÈ܃!@ AO@AÈÜ…"A€A‰#Ak"$AÈ܃AÈÜAŠ A: Aj Aj‹ Aj$  AÈÜ A k A AA€† AÈÜAÈÜ(AÓÜ,AN  A:,  AG:/  AG:.  AG:-  AG:+  AG:*  AG:)  AG:(  AG:\'A4"A60 B7( B7 B7 B7 B7 B7 A j" 6 ! a"AøÿÿÿO\r@@ A O@ Ar"Aj!  Aÿÿÿÿk6  6  6   : A j! E\r E\r  ü\n  jA: Aj! A\'j!#AÐk"$@ A j"( , " AH"E@A<"B7 Aà6 B7 B7 B7 B7$ B74 B€€€ü7,  6  A j6  (! AéOA4 Aèn" A4OA jA !A<"\nB7 \nAà6 \nB7 \nB7 \nB7 \nB7$ \nB74 \nB€€€ü7,  \nA j6 (!  \n6@ E\r  ("\nAk6 \n\r  (( | Aj! ("! ( "\n G@@@ \nAk("E\r  ("Ak6 \r  (( |  \nAk"\nG\r (!  6 @A€  A€O"\n ( kAuM\r \nAt"!\n  k"@ \n  ü\n   \nj6  \n j6  \n6 E\r r !@@@@ ("\n ("I@ \n (6 \n ("6 @  (Aj6 \nAj!\n  \n ("k"\nAu"Aj"A€€€€O\rAÿÿÿÿ  k"Au"   K AøÿÿÿO"A€€€€O\r At"" \nj" (6  ("6  j!    (Aj6 ( ( ("k"\nAu  Atk! \n@   \nü\n  6  Aj"\n6  6 E\r r  \n6    Aj!\n@ ,AH@ (A: A6  A: A: \nA€” A:$@ ,3AH@ ((A: A6,  A:3 A:( (" (  , AH6 -! -! -! -! -! -! -!\n -! -! B7( A6$ A6 A6 A6 A6 A6  A tA̞AÀ– r \nAtr Atr Atr Atr Atr Atr Atr6 (  , "AH"! ( ! !\n#A€k" $@ A j"(@A! ("E\rA‹    A4jAAüü 6 6 )7 )7 )7 )7$ ( 6, \n60 AA ("Aq6œ A„j"AA€ü A;ã A:® A;à A:ß A: A;¿ A:ª A:¥ A:„ A€„q@ A:‚ A€€q@ A:â A€ q@ A:¨ A€€€q@ A:Á A€€€q@ A:¯ Aq@ A:Ä Aq@ A:¾ A€q@ A:² A€Âq@ A:€ Aq@A!@ A G A GqE@ jA: Ar"AýqA F@  jA: Aj"A€G\r A E\rA  jAk-"A\rF\r A\nF ! A6` A 6D 64 A€€  A€€OAt6X A„jAÿAÔü B7ä B7Ü@AA \n ("E@ AÌj!% A°jAr!& Aüj!\' AèjAr!( A°j!AÜ!A!@@ !@@ (" K@ B7ø B7ð B7èA! A°jAA  A°jF""j   F"#! (¨!) (!@@ j-"A G A Gq\r A|qAj Aj A F! Aj" G\r ! A°jA,A "j Aj #" 6 A°jA$A "j A j #" 6A! A°jA A "j Aj #"$A6@ (”"AL\r (œ! (! !\n@@  Alj"-A>F@ \n O\r O\r j-A>G\r Aj"!@ Aj" O\r@ j-"\nA G \nA Gq\r A|qAj Aj \nA F! Aj" G\r !   kAkA  G"\n6  6 !  \n ( "I\r  \n k"\n6 ! Aj" G\r ! ! 6ä@@ O\r j-A\nk  N\r (!@  Alj-A>F\r Aj" G\r ! % & " Aj #!A!\nA!A! !@A k!A k! („" (üjAk! (¬E! (! ( !\r (œ!! (¤!@@ ("AG@ !   6@ ! ("M@ !  @ " O\r  ( j-!@  j- AÿqG\r Aj" G\r ! @  k \rI\r@  O\r@@  j-A k Aj" G\r ! 6ä A6 A6¨  6ä  F@   (k"A M6 A6 !  (! @@@@ AG\r AL\r  N\rA! A6¤  O\r  j" -A\nk @ Aj  Aäj.AL\r A6¤ AþÿÿÿqAG\r A6 A6  A6 !  @ AG\r  (”G\r A6  (" (œk"A O6 A6¨  A6@@ AL"\r  G\r ( AljAk-"A>G6¨  A6¨ \r ( AljAk-! AÿqA>F\r  G\r (€@ !  („"A H@ !  (ü jAk-AG@ !  A6¬ !  @ Aq@ !    AL\r  (” G\r   Alj"Ak-A>F\r   G\r  (€\r  A H\r  -AG\r  6A AF\r   A$k(k"A M6 Ak ! A6¬ A6¨@ (" !O\r@ AG\r@ -"A-k !  G\r@@@ Aj" O\r j- F\r @@ j-A k Aj" G\r ! 6ä A6 AA A=F6 !   \nI\r -"" E\rA ÀATE\rA! Aj"\n O\r@@@  \nj-" F"\r A F\r A G\r j! \nAj"\n G\r !\n AI\r  AI\r A\nk @@@  N\r  G\r@ Aj  Aàj Aèj/E\r -è"A>F\r  Alj"-G\r (ð" ( K\r (ô" k j"!@ (à" O\r@ j-"A G A Gq\r A|qAj Aj A F! Aj" G\r 6ä   k6  6 !  6ä   k"6  6 O\r@ j-A\nk  !M@  j"6ô A6  Aj"6ô  Ak6  ! @ !O@ (AF@ !  A6  !k6 A6 !  Aj  Aäj Aèj/! (!@@ @ (ä!@ AG\r  G\r@@ O\r j-"A\rF\r -è! A\nG\r AÿqA>F\r   A>! -èA>F\r   Aÿq"A.G A)Gq\r (ìAF\r   (ô" (ðk j"!@ O\r@ j-"A G A Gq\r A|qAj Aj A F! Aj" G\r ! 6ä   k"6  6  ,è6@@@ O\r j-A\nk Aj6ô   !M@  j6ô A6  Aj6ô  Ak6 E@ Aj  j0\r (”! @ (˜" J@ (!  A Av j AL"6˜ ( Als"E@ (("E\rAå (0   6 (”! AÜ   F! Aj! Aj6”  Alj" )ø7  )ð7  )è7 (”! (! (! ! A F  Fq\r (ä!  ! O@ !  @ j"\n-"A#G@ ! AþF\r AàF\r !  Ak"\nF\rA  Aj"j-A#G\r  \nF\rA  Aj"j-A#G\r  \nF\rA  Aj"j-A#G\r  \nF\rA  Aj"j-A#G\r  \nF\rA  Aj"j-A#G\r  \nF\rA  Aj"j-A#G\r  \nF\r Aj!A  @@  j- G\r Aj" G\r !  k"AI@ !  6 @@@  M\r@  j-A G\r Aj" G\r   M\r@@  j-"A\nk @ AàG\r \n-AàG\r !  Aj" G\r ! 6ä A6 A6 $A6 \n A 6 ! " k "AJ@ !  @  M"\n\r (Aq\r  j-A k"AK@ !  A tA“€€q\r !  @ \n\r@  j-"A G A Gq\r Aj" G\r !  6 6ä A6  6  @@@@@  O\r  j-AF\r Aj" O\r A/G\r  j-A>F\r AjM@A!  A! \rAA  A jAE\rA! ( " O\r \r(!@@  j-"A k" AK\rA tA€€qE\r Aj" G\r  AAA A\nF A\rF! Aj$ "6¤@  (AG\r A6¤ ("A€qE\r (AG\r  (O\r ( j-"A-k Aj  Aäj. F@ A6¤ $A6 A6 AüG\r  (”G\r (€"E\r (AG\r Aäj!\n Aàj!A!A!\r@ Aj"(" M\r (" j-AüG\rA! Aj" O\r@  j-A k" AK\rA tA€€qE\r Aj" G\r ! @@@@  I@  ( j-A:Fj!  O\r (" j-A-G\r@@@  j-"A-G\r Aj" G\r \rAj!\r  A! \rAj!\r@  A:Fj" O\r@@  j-"A k"AK\rA tA€€qE\r  Aj"G\r  AüG\rA!A! Aj" O\r@  j-A k"AK\rA tA€€qE\r Aj" I\r !   O\r@  j-A\nk E\r  ! E\r \n 6  \r6A  A E\r  (à6 A\n6  A6 (”"   F  (AF! A€qE\r  jAL\r ( Alj"Ak-"E\r A?K\rB ­†B€€€€ÀƒP\r@ (ä" ("\n Aj"  \nK"O\r (!@ j-"A G A Gq\r Aj" G\r ! Aj" \nO\r (" j-AÛG\r  Aj"j,"E\rAà ATE\r  j-AÝG\r \n Aj"G@ j-A k"AK\rA tA“€€qE\r Ak ( " -Ar: Ak \'  6@ ("O\r (!@ j-A k"AK\rA tA€€qE\r Aj" G\r ! 6ä  6  Aj"6ô  6  6AÜ!A! ! ! Aj1A! Aj A@k2\r -Aq@ Aj AÜj2\r AjA0\r Aj3\rA!#Ak"\r$ \rA6$ Aj" A6@ (€AJ@ \rAøj!@@@@@@ (ø j"("AÿqAk \r AsA\nvAq6 \r /:  \r (6 \r (AsA\nvAq6 \r /:  \r AÿÿK6 \r /: \r (6  \rA6 \r /At(€a"6 /! \r 6 \r At(€6 \r \rAj6 \r \rA$j6 @@ ("A€q@@ A€qE\r Aÿq \rA j (, ("@ ($"E\rA¯ (,   ("Aÿq"AK\rA tAŽ€qE\r (Ak6 (! A€qE\r Aÿq \rA j (, ("@ ($"E\rAŠ (,   @ ("AÿqAk (Œ (Alj" -Ar: (Aj6  \rB7` \rB7X \rB7P \rB7H \rB7@ \rB78 \rB70 \rB7(@ ("AÿqA\nG\r A€€„I\r ("A€ Av"nM\r Aq! Aj!A!\nA!@ "AO@ A|q!A!@  Atj"( j ( j (j (j ( ( ( (jjjk! Aj! Aj" G\r E\r @  Atj"( j (k! Aj! \nAj"\n G\r  n AvO\r  ($"A¨ (,  (  A€~qA r"6 A! \rA6ˆ \rB7€ \rB7x \rB7p \rB7h ("@ (Œ AljAk-AqE! A!@@@@@@@ AÿqAk \r Av6h  A€€I\r ("\n ("j-! ( !@@@  ("O\r@  \nj- AÿqG\r Aj" G\r  M\r@  \nj-A G\r Aj" G\r ! @  M\r@  \njAk-A G\r Ak" K\r ! A!  \nj  kA \rAèj \rAÈj<\r (! !\n@  M\r@  \nj-A k"AMAA tA€€q\r \nAj"\n G\r !\n  j \n kA  \rA(j<\r \r :ˆ (!A!  \rA6l \r Av6h \r (Ak6p  \r Av6h @@@@@@@  Aÿq"A FqE@  \rAèj (, ("\r (! AÿqAk ($"E\rAŠ (,   (!@ AÿÿM@ E\r Aj!@ ( (F@ A j! Ak"\r  @  A lj"A k( Ak(G\r Ak"\r  Ak"E\r Aj! A  ="E\r  A Aj (="E\r  (!@@ Av"Atq"E@A! ($"E\rAÔ (,  r  @ (! ("! ! !@ !@ M\r (!@  j-A-F\r Aj" G\r ! A!  I@ ( jAk-A:F! @ M\r (!@@  j-"\nA-G\r Aj" G\r !  Ar  \nA:F!  At(°:6 Ak! Aj! AJ\r A A (, ("@ ($"E\rAŠ (,  r  A ( (  >"\rA A (, ("@ ($"E\rA¯ (,  r  AI\r@A A (, ("E@ Aj!A!  ($"E\rAŠ (,  r  @ A  Atj"( (  >"\r Aj" G\r A A (, ("E\r ($"E\rA¯ (,  r  r  Aj (?"\r A!  (Aÿq"A Fq\r  \rAèj (, ("E\r ($"@A¯ (,  ! \r \r(X@ \r(Hr \r(Lr \r(Pr \r(8E\r \r((r \r(,r \r(0r \r (! -AkAM@ A l j!  At j!  (Œ (Alj" -Aþq A\nvAqr: (Aj6 Aj" (€H\r A6€A! \rAj$ "\r -Aq@A!A!#A@j"$@ Aj"(`"E\r (pE\r (X AAW@@AA (, ("E@ (`E\r A0j!\n  ($"E\rAŠ (,   @ (X Alj"( E\r B7 B7 B7 B7 \nB7 \nB7  ( 6(  (6,@  ( (A \n Aj<@A!  A A(j (, ("@ ($"E\rAŠ (,    ( (?"\rA A(j (, ("E@A!  ($"E\rA¯ (,  (@ (r ( r (r \r Aj" (`I\r A!AA (, ("E\r ($"@A¯ (,  ! A@k$ \r A!AA (0 ("E\r (("@A¯ (0  !  \n6ä A6 ! @ (ä"Aj" ("O\r (!@@ j"-A\nk @ -A\nk @ -A\nk @  j-A\nk Aj! Aj!  I\r @ I@ (!@A!@ j-A\nk Aj" G\r ! A! A°jA(A "j Aj #"\n 6 " ("AG\r@@ ("M\r (!@  jAk-"A G A Gq\r Ak" K\r  @  O\r (!@  jAk-A#G\r Ak" K\r   F\r ( jAk-"A F@ !  A F@ !  ! -AqE\r \n 6  !@ AkAI\r  ("M\r (!@  jAk-"A G A Gq\r \n Ak"6  I\r @ ( j-A\rFj! I@ ( j-A\nFj! @ )E\r E\r  j"AL\r ( Alj"Ak-A>F\r (ü Ak(j" (A€r6 @ \r  j" (”N\r Aj 0\r AJ@ AjA Al" (j"(" -Aq ( j,A A4\r AjA (ü" -éAq" ( j,A A4\r ( j" -Aýq r: ( j 6 @ (”!@ -AqE\r ( Alj"Ak-A>G\r (AG\r \n(" ("k"AkA K\r (" j"-AÛG\r -A!G\r  jAk-AÝG\r Ak! Aj!A!@@@@ Ak @  j-"A k  AákAÿqAIAÿq -–"A k  AákAÿqAIAÿqG\r Aj" G\r A  @  j-"A k  AákAÿqAIAÿq -\r"A k  AákAÿqAIAÿqG@ A\nk Aj" G\r A  A!@  j-"A k  AákAÿqAIAÿq -\n"A k  AákAÿqAIAÿqG@ A\nG\r  Aj" G\r A  A!@  j-"A k  AákAÿqAIAÿq -ä"A k  AákAÿqAIAÿqG@ A\nG\r -"A k  AákAÿqAIAÿqAÃG\r -"A k  AákAÿqAIAÿqAÁG\r -"A k  AákAÿqAIAÿqAÕG\r -"A k  AákAÿqAIAÿqAÔG\r -"A k  AákAÿqAIAÿqAÉG\r -"A k  AákAÿqAIAÿqAÏG\r -"A k  AákAÿqAIAÿqAÎG\rA  Aj" G\r A ! Ak" -Ar: ( (”AljAk" -AÇq r: A6 (”!  k!@A!@@A ( Alj"-A)kt"AqE@ A!qE\rA! Aj1  („6 Aj  ( ,A4\r AjA (" -Aq ( j,A A4\r  AjAA -"AqA AvAqA4\r Aj" (”H\r @ (E@ Aj1@A!  A!AÜ! -AÀqE\r ( \n(I\r (ŒAj6Œ  (Œ@@ („"Aj" (ˆ"J@ A€ Av j AL"6ˆ (ü s"E@A! (("E\rAå (0   (€"@   (ükj6€ 6ü („"Aj6„  6„ (ü"\rA!   j"A; (Œ! A6  AtAr6 A6Œ @ $(E\r Aj1E\rA!  @@@@ ("Ak\n A! Aj1\r („"Aj" (ˆ"J@ A€ Av j AL"6ˆ (ü s"E@ (("E\r\nAå (0  \n (€"@   (ükj6€ („"Aj6„ 6ü  j  6„ (ü"E\r  j " (AtAÐÍj(" /Atr6 (! A6   Atr6 6€ Aj 5\r Aj1\rA!AÜ!  (€A: (€ /; (€" (A€r6A! Aj 5\r Aj1\rA! (€E@AÜ!  A6 !  (€A\n: (€ /; A 6AA Aj 5!  @  (F\r Aj1E\rA!  @ (€\r !@ Aj"\n(€"Aj" \n(„"J@ \nA€ Av j AL"6„ \n(ø s"E@A \n($"E\rAå \n(, A  \n(ü"@ \n   \n(økj6ü \n 6ø \n \n(€"Aj6€  j  \n 6€ \n(ø"E\r  j " (AtAÐÍj(" /Atr6 (! A6   Atr6 \n 6üA  A E\rA!  Aj 5! A! E\r  A!  (("E\rAŠ \n  (P"@ (T"A!@@ (P Atj("E\r (@" M@   (D (HljI\r r (T! Aj" I\r (P r (H@A!@ (@ Atj"-"Aq (r -  Aq@ ( r Aj" (HI\r (@r (l"@ (p"A!@@ (l Atj("E\r (\\" M@   (` (dljI\r r (p! Aj" I\r (l r (d@A!@ (\\ Alj(r Aj" (dI\r (\\r (8r (xr (ür (r A€j$ @A<"B7 Aà6 B7 B7 B7 B7$ B74 B€€€ü7,  6  A j6  (@ (("E\r ((" ($"G A$j!A!@@@  At"j("(AF@ ($" (("G\r Aj!   k"Au!@@ ((AF\rA! AG@@ Aj!  Atj"((AF@  O\r Ak(!  Atj(! \r \r !  G\r A! A68 B70A!  A! A68 B70 A6L B7D  A0j6@  AÄj6<@@@@@@  At"j"("(AG\r  Ak(A !  Aj"K@  Atj( rAG\r AF\r -A/F\r Aj" G\r ! F\r Aj  k” I@@ Aj ((  ,3AH j-bÀš Aj" G\r  (," ,3" AH"O\r Av! Aj!\n@ (  Aq!@  j"-S@ Aj" I\r  -A>F\r -A/F\r@@ K@ !@@  j"-A=F\r -S\r -A>F\r -A/F\r Aj" I\r !  G\r Aj!  A6P B7H AÈj  k”  I@@ AÈj ((  ,3AH j-bÀš Aj" I\r @@  (, ,3" AH""O\r (  !@@  j"-SE\r Aj" G\r  -A=G\r@@@@@@ "Aj"M\r  j"-S\r B78 A6@@ -A"F\r -A\'F\r !@  j" -S\r -A>F\r -A/F\r Aj" G\r !  Aj"!@ M\r -!@  j- F\r Aj" I\r ! k"  k" I"AøÿÿÿO\r@@ A O@ Ar" Aj!  Aÿÿÿÿk64  6,  60   :7 A,j! E\r E\r   j ü\n jA: ,CAH@ (@ (8r  (46@  ),78   (, ,3" AHIj!  A6@ B78 ! M\r k"  k" I"AøÿÿÿO\r@@ A O@ Ar" Aj!  Aÿÿÿÿk64  6,  60   :7 A,j! E\r E\r   j ü\n jA: ,CAH@ (@ (8r  (46@  ),78  AÈj6\\ A,j \n AÈj AÜj@ (,"Aj" A8jF\r ,C! ,AN@ AN@ (@6 )87  (8 (<™  (8 A8j AH" (<  ˜ ,CAN\r (@ (8r   A¤AƁ"AüÛ6 AˆÜA  !  AÈj6, A8j \n AÈj A,j (8",AH@ A6 (  A: Aj A: ,SAH@ (P (Hr (,! -3!   Aÿq ÀAH""I\r A!A!@ ( ," AHAG\r ( Aj AH"(Aöґ«s -Aïsr\r A:S A:K A»/;H A½-:J Aj!\nA! AÈj"( , " AH""! (  " !@ "AI\r Ak"Aq@ !  (A•ÓÇÞl"Av sA•ÓÇÞl A•ÓÇÞls! ! Aj ! AI\r !@ (A•ÓÇÞl"Av sA•ÓÇÞl (A•ÓÇÞl"Av sA•ÓÇÞl A•ÓÇÞlsA•ÓÇÞls! Aj! Ak"AK\r @@@@ Ak -At s! -At s!  -sA•ÓÇÞl! @ \n("E\r \n( A\rv sA•ÓÇÞl"Av s" Akq i"AM\r   K\r  p " Atj("E\r ("E\r Ak! AK!\n@@@  ("G@@ \nE@  q!   K\r  p!  G\r  ( ," AH" G\r ( Aj  U\r !  ("\r A  ! ,SAH@ (P (Hr E\r ( ," AHE\rA<"B7 A!6 Aà6 B7 B7 B7 A6( B74 B€€€ü7, A:S AŽ/;H A-:J A:K  AÈj6, A8j Aj AÈj A,j@ (8" F\r Aj! Aj! ,! ,AN@ AN@  (6  )7   ( (™   (  AH" (  ˜ ,SAH@ (P (Hr A j! Aj ,AH@ ( (r @ ,3AH@ ((A: A6,  A:3 A:( @ E\r ( ( "F\r Ak(!  6  6 A$j Aj@ ( "E\r ("Ak6 \r (( | A!  A! E\r  ("Ak6 \r  (( |  A! ( ( F\r  ( " (kA I\r@ Ak("E\r  ("Ak6 \r  (( |  Ak6 Aàj$ à%#A k"$@@ E@A!  @@@@@@@@@@@@@@@@ Ak\n \r  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6Œ  A j6ˆ  )ˆ7    A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,@ E\r (! A:• AA  AL" ANA0r:” A”ja"AøÿÿÿO\r@@ A O@ Ar"Aj!  Aÿÿÿÿk6€  6ø  6ü   :ƒ Aøj! E\r E\r  A”j ü\n  jA: AÔÜ6˜ A¤j AjAÔÜ A˜j@ (¤"Aj" AøjF\r ,ƒ! ,AN@ AN@  (€6  )ø7   (ø (ü™   (ø Aøj AH" (ü  ˜ ,ƒAN\r (€ (ør  6ô  A j6ð (Aj6  )ð7  Aj ("Ak6 \r (( |  A<"B7 A 6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6ì  A j6è  )è7  Aj \r A<"B7 A 6 Aà6 B7 B7 B7 A6( B74 B€€€ü7, A j!@ E\r ("E\r ("E\r AøÿÿÿO\r@ A O@ Ar"Aj!  Aÿÿÿÿk6€  6ø  6ü   :ƒ Aøj! @   ü\n  jA: (ü -ƒ" ÀAH@ AÄÝ Aøj ,ƒAN\r (€ (ør  6ä  6à (Aj6  )à7  Aj ("Ak6 \r (( | A<"B7 A\r6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6Ü  A j6Ø  )Ø7  A j A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7, A j!@ E\r ("AF\r Aøj œ A¬Ý Aøj ,ƒAN\r (€ (ør  6Ô  6Ð (Aj6  )Ð7(  A(j ("Ak6 \r\n (( | \n A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7, A j!@ E\r (E\r A”Ý AøjAû" , AH@ ( (r A Ý AøjAûA› -AßqAØF" , AN\r ( (r  6Ì  6È (Aj6  )È70  A0j ("Ak6 \r (( | A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7, A j!@ E\r@ - "E\r A:¥  :¤ AøÜ Aøj A¤j" , AN\r ( (r ("E\r ("E\r AøÿÿÿO\r\n@ A O@ Ar"Aj!  Aÿÿÿÿk6€  6ø  6ü   :ƒ Aøj! @   ü\n  jA: (ü -ƒ" ÀAH@ AˆÝ Aøj ,ƒAN\r (€ (ør  6Ä  6À (Aj6  )À78  A8j ("Ak6 \r (( |  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6¼  A j6¸  )¸7@  A@k  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7, A j!@ E\r Aøj (œ A¸Ý6˜ A¤j AjA¸Ý A˜j@ (¤"Aj" AøjF\r ,ƒ! ,AN@ AN@  (€6  )ø7   (ø (ü™   (ø Aøj AH" (ü  ˜ ,ƒAN\r (€ (ør  6´  6° (Aj6  )°7H  AÈj ("Ak6 \r (( |  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7, A j!@ E\r A:€ BãÞ±›ô­·ô7ø A:ƒ A¤j (œ  Aøj6” A˜j Aj" Aøj A”j@ (˜"Aj" A¤jF\r ,¯! ,AN@ AN@  (¬6  )¤7   (¤ (¨™   (¤ A¤j AH" (¨  ˜ ,¯AH@ (¬ (¤r ,ƒAH@ (€ (ør A"6ø BŒ€€€€‚€€€7ü Aù (6 Añ )7 A: A¤j (œ  Aøj6” A˜j  Aøj A”j@ (˜"Aj" A¤jF\r ,¯! ,AN@ AN@  (¬6  )¤7   (¤ (¨™   (¤ A¤j AH" (¨  ˜ ,¯AH@ (¬ (¤r ,ƒAH@ (€ (ør A"6ø BŒ€€€€‚€€€7ü Aì (6 Aä )7 A: A¤j (œ  Aøj6” A˜j  Aøj A”j@ (˜"Aj" A¤jF\r ,¯! ,AN@ AN@  (¬6  )¤7   (¤ (¨™   (¤ A¤j AH" (¨  ˜ ,¯AH@ (¬ (¤r ,ƒAN\r (€ (ør  6   6œ (Aj6  )œ7P  AÐj ("Ak6 \r (( |  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6˜  A j6”  )”7X  AØj  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6  A j6Œ  )Œ7`  Aàj  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6ˆ  A j6„  )„7h  Aèj  A<"B7 Aà6 B7 B7 B7 A6( B74 B€€€ü7, AA AF6 @ E\rAš\n! (Ak"AM@ At(Ð! A:ƒ A:ý Aá\r(6ø Aå\r-:ü a"AøÿÿÿO\r@@ A O@ Ar"Aj!  Aÿÿÿÿk6¬  6¤  6¨   :¯ A¤j! E\r E\r   ü\n jA:  Aøj6” A˜j Aj Aøj A”j@ (˜"Aj" A¤jF\r ,¯! ,AN@ AN@ (¬6 )¤7  (¤ (¨™  (¤ A¤j AH" (¨  ˜ ,¯AH@ (¬ (¤r ,ƒAN\r (€ (ør  6€  A j6|  (Aj6  )|7p  Aðj  ("Ak6 \r  (( |  A:$ ,3AH@ ((A: A6,  A:3 A:( A j$    #Ak"$@ ( ," AHE\r ( " (F\rA<"B74 B€€€ü7, A6 Aà6 B7 B7 B7$  Aj")7  (6 A: A: Ak(!  6  A j6 A$j Aj@ ( "E\r  ("Ak6 \r  (( | ,AH@ (A: A6  A: A: Aj$ ª~A!@@@ (Ak ( ," AH""E\r  ( Aj "j! !@@ 1"B?V\rB †B€Ì€€ƒP\r Aj" G\r  kAF A!  ¹  #Ak"$ ("(" ("kAu!@  F\r@  Atj(E\r Aj" G\r ! @@ " M\r  Ak"Atj(\r A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6  A j6 A0j!  At"j!  At"j!@  kAu" (" ("kAuM@ (" k"Au I@  j!  G@@ (! ("@  (Aj6  6 (!  6@ E\r  ("Ak6 \r  (( | Aj! Aj" G\r (! !  G@@  (6  ("6 @  (Aj6 Aj! Aj" G\r    kj6   G@@ (! ("@  (Aj6  6 (!  6@ E\r  ("Ak6 \r  (( | Aj! Aj" G\r (!  G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r  6  @ ! (" G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r ( (!  6 r A6 B7A! @ A€€€€O\rAÿÿÿÿ Au"  K AøÿÿÿO"A€€€€O\r  At""6  6   j6  G@@  (6  ("6 @  (Aj6 Aj! Aj" G\r  6   ( Aj@ ( "E\r  ("Ak6 \r  (( | ("(! (!  G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r  6 Aj$ A„  )A¤€"A€Ú6 A”Ú6 AèÚA  ~ AL@ (" ("kAu N@  k"Au"\n H@@   j" F@ !  ! !@  (6  ("6 @  (Aj6 Aj! Aj" G\r 6 \nAL@  At"j! !   k"K@ !@  (6  (6 B7 Aj! Aj" I\r 6  G@@ Ak")! B7 Ak(! Ak" 7@ E\r  ("Ak6 \r  (( |  G\r  F@ !@ (! ("@  (Aj6  6 (!  6@ E\r  ("Ak6 \r  (( | Aj! Aj" G\r   At"j! !   k"K@ !@  (6  (6 B7 Aj! Aj" I\r 6  G@@ Ak")! B7 Ak(! Ak" 7@ E\r  ("Ak6 \r  (( |  G\r  Atj! !@ (! ("@  (Aj6  6 (!  6@ E\r  ("Ak6 \r  (( | Aj! Aj" G\r  @  ("kAu j"A€€€€I@Aÿÿÿÿ  k"Au"   I AøÿÿÿO" A€€€€O\r AtA "  kj! At"Ak"Aq   (6  ("6 @  (Aj6 Aj! Aj !  j! @@  (6  ("6 @  (Aj6  (6  ( "6 @  (Aj6 Aj! Aj" G\r ( k"@   ü\n 6   ("k"k! @   ü\n  j6 6 ( Atj6 @ r    Ý#A k"$ ($" (("G@@ ( Aj" G\r @ (AG\r ($" (("F\r !@@@@@ (("  k"AH\r  "6  6   j6 A6 B7  Aj6  Aj6@ Aj!@@ (("  Aj!    Aj"G\r  Aj" G\r    ($"@ " (("G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r ($! 6( (, r (6$ (6( (6, A6 B7 ("E\r " ( "G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r (!  6 ( r ("E\r " ("G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r (!  6 ( r A j$ ¶ ~#A k"$@@@ ($" (("F\r@ ("("A G A GqE@  Aj" G\r ((" ($"F\r A$j! @  \nAt"\rj("(" A\rkAO \nAjA!@ ($" (("F\r !@@@ ("(AG\r (( ($"kAG\r ((A!F\r Aj" G\r  A6 B7 A6 B7@@@ ("(AG\r (( ($"kAG\r ((A!G\r (" ( "G@A<"B7  6 Aà6 B7 B7 B7 B€€€€€€€À?7(  6  A j6  (68  64  60 A6 B7 Aj @ ("E\r  ("Ak6 \r  (( | (($! (" ("I@  (6  ("6 @  (Aj6  Aj6   ("k"Au"Aj"A€€€€O\rAÿÿÿÿ  k"Au"   I AøÿÿÿO"A€€€€O\r At"" j" (6  ("6  j!    (Aj6 ( ( k"Au  Atk! @   ü\n  6  Aj"6  6 @ r  6  Aj  Aj" G\r @ (" ( "F\rA<"B7  6 Aà6 B7 B7 B7 B€€€€€€€À?7(  6  A j6  (68  64  60 A6 B7 Aj  ("E\r  ("Ak6 \r  (( | ($ \rj" ! Aj" (("G@@ )! B7 (!  7@ E\r  ("Ak6 \r  (( | Aj! Aj" G\r ((!  G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r 6( (" ("  kAu (" ! ( k! ("@ " ( "G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r (!  6 ( r (! @ " ("G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r (!  6 ( r Au! ($! ((!  \nj !\n \n  kAuI\r A j$   Þ#Ak"$ @ ("E\r ( " (F\r Ak(! ("@  (Aj6 Aj!  6  6 A$j Aj@ ( "E\r ("Ak6 \r (( |   @ ("E\r  ("Ak6 \r  (( | Aj$ ¢  } ( , " AH""! (  " !@ "AI\r Ak"Aq@ !  (A•ÓÇÞl"Av sA•ÓÇÞl A•ÓÇÞls! ! Aj ! AI\r !@ (A•ÓÇÞl"Av sA•ÓÇÞl (A•ÓÇÞl"Av sA•ÓÇÞl A•ÓÇÞlsA•ÓÇÞls! Aj! Ak"AK\r @@@@ Ak -At s! -At s!  -sA•ÓÇÞl! A\rv sA•ÓÇÞl"Av s! @ ("E\r (  Akq i"AM\r   K\r  p "Atj("E\r ("E\r Ak!\n AK! @@  ("G@@ E@  \nq!   K\r  p!  F\r  ( ," AH G\rA ( Aj AH UE\r ("\r A " 6 A6 Aj!@ (", AN@  (6  )7  (! (!\n#Ak" $@@@ \n‘@ " \n’  \nA÷ÿÿÿK\r Aj \nŒAj“ (   ("6  (   \n6   \nAj‰ Aj$   A6 B7 *!\r ( Aj³!@ @ \r ³” ]E\r @A  AkqAG AIr Atr"  \r•ü"  K"AF\r   AkqE\r w " ("M@  M\r ( ³ *•ü! @ AI\r iAK\r AA Akgkt AI  w "  K" O\r !@@ "@ A€€€€O\r At"! (!  6 @ ( r (!  6 @ A ü ("E\r Aj! (!@ i" AM@  Akq!   I\r  p! Atj 6 ("E\r Ak!\n AK! @ (!@ E@  \nq!   I\r  p! @  F@ !  Atj" (E@ 6 ! !   (6  ((6 ( 6 ("\r  (! A6 @ ( r A6   (" Ak"qE@  q!   K@ !   p! @ (" Atj"("E@  (6  6  Aj6 ("E\r (!@  Ak"qE@  q!   K\r  p!  Atj 6   (6  6  ( Aj6 A : 6 ¡#Ak"$  6 Aj Aj  Aj@ ("Aj" F\r , ! ,AN@ AN@  (6  )7   ( (™   (  AH" (  ˜ Aj$ } a"AøÿÿÿI@@@ A O@ Ar"Aj! Aÿÿÿÿk6 6 6  : ! E\r E\r   ü\n  jA:   o ("@@ (! ,AH@ ( (r ,AH@ ( (r r "\r (! A6 @ ( r  (" ("I@  (6  (6 B7 Aj6 @  ("k"Au"Aj"A€€€€I@Aÿÿÿÿ  k"Au"   I AøÿÿÿO" A€€€€O\r AtA " j" (6  (6 B7  Atk! @   ü\n  Atj6 Aj"6 6 @ r 6   Î#Ak"$ @ ("E\r ( ( "F\r Ak(! ("@ (Aj6  6  6 A$j Aj ( "E\r ("Ak6 \r (( | @ ("E\r  ("Ak6 \r  (( | Aj$ Ý#A0k"$ ("(" ("G@ !@@@ (@  Aj"G\r  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6  A j6 A: A¢\n(6 A¦\n-: A: A: AôäÕ«6 A:  A j6$ A(j Aj A j A$j@ (("Aj" F\r ,AN@  (6  )7   A˜ , AN\r ( (r ,AH@ ( ( r (! (0"@ " (4"G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r (0!  64 (8 r A68 B70  (60  (64  (68 A6 B7 ( Aj ("E\r  ("Ak6 \r  (( |  @ (  Aj" G\r ("(" ("G@@@ Ak("E\r  ("Ak6 \r  (( | Ak"G\r  6 A0j$ AßÜ,AH@AÜÜ(AÔÜ(r AëÜ,AH@AèÜ(AàÜ(r A÷Ü,AH@AôÜ(AìÜ(r AƒÝ,AH@A€Ý(AøÜ(r A“Ý,AH@AÝ(AˆÝ(r AŸÝ,AH@AœÝ(A”Ý(r A«Ý,AH@A¨Ý(A Ý(r A·Ý,AH@A´Ý(A¬Ý(r AÃÝ,AH@AÀÝ(A¸Ý(r AÏÝ,AH@AÌÝ(AÄÝ(r Aà6  Aà6 r ¬#Ak"$ A j"($"@ " (("G@@@ Ak("E\r ("Ak6 \r (( |  Ak"G\r ($!  6( (, r Aj ,AH@ (  (r Aj$  r µ  (!@@@@@@@@@@@@ ( Ak  Aj"M@  6A Ak! (!@@ ! !@ j"-"AF\r @ Aj" O\r -"A k  AákAÿqAIAÿqAÓG\r -"A k  AákAÿqAIAÿqAÃG\r -"A k  AákAÿqAIAÿqAÒG\r -"A k  AákAÿqAIAÿqAÉG\r -"A k  AákAÿqAIAÿqAÐG\r -"A k  AákAÿqAIAÿqAÔG\r  j-A>F\r @ Aj" O\r -"A k  AákAÿqAIAÿqAÓG\r -"A k  AákAÿqAIAÿqAÔG\r -"A k  AákAÿqAIAÿqAÙG\r -"A k  AákAÿqAIAÿqAÌG\r -"A k  AákAÿqAIAÿqAÅG\r  j-A>F\r A\nj" O\r -"A k AákAÿqAIAÿqAÔG\r -"A k AákAÿqAIAÿqAÅG\r -"A k AákAÿqAIAÿqAØG\r -"A k AákAÿqAIAÿqAÔG\r -"A k AákAÿqAIAÿqAÁG\r -"A k AákAÿqAIAÿqAÒG\r -"A k AákAÿqAIAÿqAÅG\r - "A k AákAÿqAIAÿqAÁG\r  j-A>F\r\r Aj" G\r !  6A Aj" O\r Ak! (!@@  j"-A\nk\n\n /A­Ús -A>srE\r Aj! Aj!  I\r !  Aj" O\r Ak! (!@@  j"-A\nk /A¿üF\r Aj! Aj!  I\r !  Aj" O\r Ak! (!@@@  j-"A\nk  A>F\r Aj! Aj!  I\r !  Aj" O\r Ak! (!@@  j"-A\nk /Aݺs -A>srE\r Aj! Aj!  I\r !  @@  O\rA! ( j-A\nk  6 ( !   6A  6A  6A  6A  6A  Aj6A Å@  ("O\r  (˜O\r @ (" j-"E\r A>F@ A>:  6  -Aüq: Aj! Aj  A?K\rB ­†B€€€€€ ƒP\r  Aj"K@  j-A k"AK\rA tA“€€qE\r  :  6  -Aüq: Aj  A! A6  A j"  I"\nO\rA! !@@ j-" A0kAÿqA K\r  A\nljA0k"6 Aj" \nG\r \n! M\r O\r@ j-" A)k  Aj"K@  j-A k"AK\rA tA“€€qE\r  :  6  -Aüq:  k jAj 6  6A!  ç  ("H@@A!@@@A (Œ Alj"Ak"-A)kt"AqE@ A!qE\rA! Ak(!A! Ak-Aq@ ( j,! A! A  A4\r A ,A4\r  AA Ak-"AqA AvAqA4E\rA!  (Ak"6  H\r A Þ#A0k"$@ (ü"E\r@@@ ("AÿqAk A€qE\r (" (O\r ( j-AÛG\r@ ("E\r Aj! AG\r !  @ \r@ A/G\r Aj" O\r \n j-A>F\r AL\r@ AßqAÁkAÿqAI\r AßF\r A:G\r  Aj"M@A!  @@@ \nj,"A0kAÿqA\nI\r A_qAÁkAÿqAI\r E@A!  A ATE\r A!  Aj"G\r  A!  A! @@ Ak A=G\r Aj!A!  A+!@@@@ A"k E\r AÑ AT\r A\rF\r A)!  A*! Aj! I\r A! E\r \rF\rA  A)FA !  Aj" Atj"(! (" I\r   M@A A!  Aj6 „#Ak"$@  O\r@@@ j"  k A j7"AÿK@A!A!@@@  jAm"AtA°j("\nAu j"AtA°j(Aÿÿÿq K@ Ak!   \nAvAq j"AtA°j(AÿÿÿqM\r Aj!  L\r  AH\r  A F\r A F\r A kAI\r -A\nk ( j" I\r Aj$  à@@@ AÿK@A­!@@@  jAm"AtAÐj("Au j"AtAÐj("Aÿÿÿq K@ Ak!  AvAq j"AtAÐj(AÿÿÿqM\r Aj!  N\r  AH\rAà9!  A6  A r AÁkAI6 A!A4!@@@  jAm"AtAÐ2j("Au j"AtAÐ2j("Aÿÿÿq M@ AvAq j"AtAÐ2j(Aÿÿÿq O@ AH\rAð9!  Aj!  Ak!  N\r A!A!@@@  jAm"AtAà7j("Au j"AtAà7j("Aÿÿÿq M@ AvAq j"AtAà7j(Aÿÿÿq O@ AH\rA€:!  Aj!  Ak!  N\r A6  6  ( ( " lAtj! At"@   ü\n  6 F\r ( Aÿÿÿq"AjF@  sAsAq j6  ( kj6 õ #A k"$ B7 B7 B7 B7 A B! A B! @@@  K@   M\r @ \nE@   AjF!A! AqE@   F! A!  Atj( Aj Atj(k"\r Aj" ( " I! Aj" (" I!\n  K\r  I\r  K\r  I\r A! A j$  iA!@ ("(" ("("I\rA!  K\r ( ( ( (D"\rA! (" ("I\r K!  ‰#Ak"$@@@  O\r j  k A j7! ( j! AÿK@A!@@@  jAm"AtA°j("\nAu j" AtA°j(Aÿÿÿq K@ Ak!   \nAvAq j"AtA°j(AÿÿÿqM\r Aj!  L\r  AN\r  A F\r A F\r A kAO\r A6 A 6  B!   C Aj$  D ("(" ("("I@A  K@A ( ( ( (D ž“#A0k"$ A6x#Ak"$ AjAA€ü @@@ E\r Ak! A€j!\r  AtjAk!@  Atj"(!@ !@ !@ Aj" ("O@ !  (! @ \r  j"-j-@ !  \r -j-@ !  \r -j-@ !  \r  j-j-@ !  Aj! Aj"!  K\r @  K@ (!@ \r  j"-" j-\r Aj" G\r Aj" I\r  @AA@@@@@@ AÜF@A! Aj" (O\r@  j-"A!kAÿqAI\r@ AÿqA\nku Aj I\r Aj! @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"\r  6t (x"Aj6x  Alj"AÜ ; B7  Aj"6  6 A!@@@@@@@@@@@@ A!k\\ \r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\n\r \r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r Aj"!\n@  M\r@  \nj- G\r \nAj"\n I\r !\n A!A!A!  (F\r Q"AÿM\rA! A!@@@@  jAm" AtA°j("Au j"AtA°j(Aÿÿÿq K@ Ak!   AvAq j" AtA°j(AÿÿÿqM\r Aj!  N\r  A! AN\r A! Aä!@@@  jAm" AtAÀ:j("Au j"AtAÀ:j(Aÿÿÿq K@ Ak!   AvAq j" AtAÀ:j(AÿÿÿqM\r Aj!  N\r A! AN   k! !@@  j-" AàG\r Aj" G\r !A  A F ! !  k"A K\r At j( "  AtjAk(O\rA!A!@ E@ ! !\n  !\n  " I\r @@@@ \nO\r@  j-"A G@ AàF\rA! Aj" \nG\r A! \n  ! \nO@A! !  @  j-AàG" @ ! !  Aj" \nG\r ! \n! @  k"F@A!\n  Atj" (" I@  jAk-A F!\n As  Gq\r \r F \nrE\r E\r Aj  @ F\rA! AK\r At jA j" (" K6 \r  Aj"M@A!   Atj"\n(! \n(!\n  ( ! \n@ Ak!  Ak(" ("O\r@  j-"A G A Gq\r Aj" G\r ! @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"\r  6t (x"Aj6x  Alj"Aà; B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"\r  6t (x"Aj6x  Alj"Aà; B7  6  6 (t (x"Alj"Ak" Ak6 A(k" Ak6  -Ar:  -Ar:A! ! ! ! (M\r@@@@   jAv"Atj"( K@ E\r  AtjAk( I\r Ak!  (K\r ! ! !  Aj! A!  M\r ! !  @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"\r  6t (x"Aj6x  Alj"A¦; B7  Aj"6  6  (x"AL\r (t" AljAk-A&G\r (|" L@ AÀ Av j AL"6|  Als"E@A! ($"\rE\rAå (, \r  6t (x! Aj6x  Alj"A»; B7  Aj6  6  ("\n - AÀq\r\n   k"  \n A jA\r (" AjM\r -A!G\r  Aj"j-A-G\r -A-G\r A! (è" K  \nAkOq\r @ Aj" \n \nI"M@  j" /A­Ús -A>srE\r\r@@ Aj! Aj" K\r " j" /A­Ús -A>sr\r  !  \nI Aj" Kq@  Atj"(! (!  6è (! A! -AqE\r Aj" O\r  j-AüG\r@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"Aü; B7  Aj"6  6  ("Aj K\r  Ak"j"/A÷îs -A÷sr\r @  F\r Q"AÿK@A!A!@@@@  jAm" AtA°j("\nAu j"AtA°j(Aÿÿÿq K@ Ak!   \nAvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r  AN\r A!Aä!@@@  jAm" AtAÀ:j("\nAu j"AtAÀ:j(Aÿÿÿq K@ Ak!   \nAvAq j"AtAÀ:j(AÿÿÿqM\r Aj!  N\r  AH\r\r  A F\r A F\r A\rkA}K\r A!kAI\r A:kAI\r AÛkAI\r AÿG AúKqE\r @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"A®; B7  Aj"6  6@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" (6 (! AÄ; B7  6  Aj!@@ (" Aj K\r  Ak"\nj(AèèуG\r  M\r /A¯ÞF\r @ Aj K\r  Ak"\nj"(Aèèуs -Aósr\r  M\r /A¯ÞF\r Aj K\r  Ak"\nj"/Aæès -Aðsr\r  M\r /A¯ÞG\r @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"Aº; B7  6  \n6@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" (6 (! AÄ; B7  6 Aj!  (Aj K\r @ Ak-"AßqAÁkAÿqAO@ Aj O\r A0kAÿqA\nO\r  Aj O\r\n  Aj"j-"A0kAÿqA\nO@ A_qAÁkAÿqAK\r\n @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"AÀ; B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" (6 (! AÄ; B7  6 Aj!  @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"AÝ; B7  Aj"6  6 \r @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" 6A!A  Aj" O\r  j-AÛG\r@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" 6A!!A !  :  : B7   j"6@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"AÄ; B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"AÄ; B7  6  6 \n @@ E@  -\rA qAvq\r  E\r @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"A:  : B7  Aj"6  6 \n !@@@@@@@@@AA@@AA@@@@@@@@@@ > AþF\r AÞG\r -AqE\r Aj" !@  M\r@@  j-AÞG\r Aj" I\r ! !  kAG\r A! A!  j ( kA7"AÿM@ A F\r A F\r A kAM\rA  @@@  jAm"AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r A  \nAN\rA  A !  (F\r Q"AÿM\rA!A!@@@  jAm" AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r !  \nAN\r !  -A qE\r@ Aj" O\r  j" -A=F\r !  kAG\rA!A!AA ( kA7"AÿM@A! A F\r A F\r A kAI  @@@  jAm" AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r A!\n \nAN !  -A€qE\r@ Aj" O\r  j" -A+F\r !  kAG\rA!A!AA ( kA7"AÿM@A! A F\r A F\r A kAI  @@@  jAm" AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r A!\n \nAN !  A! Aj"\n!@  \nM"\r@  j-AþG" \r Aj" I\r ! @  k"AF@ ( "A€€qE\r@ \rA! A!  \nj ( \nkA7"AÿM@ A F\r A F\r A kAM\rA  @@@  jAm"AtA°j(" Au j"AtA°j(Aÿÿÿq K@ Ak!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r A  AN\rA  A !  (F\r Q"AÿM\r\rA!A!@@@  jAm" AtA°j(" Au j"AtA°j(Aÿÿÿq K@ Ak!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r !  AN\r !  ! AK\r ( ! ! A€qE\rA!  (M\r\n Q"AÿM\rA!\nA!@@@@  \njAm"AtA°j(" Au j" AtA°j(Aÿÿÿq K@ Ak!   AvAq j"\nAtA°j(AÿÿÿqM\r \nAj!\n  \nN\r  AN\r A!\nAä!@@@  \njAm"AtAÀ:j(" Au j" AtAÀ:j(Aÿÿÿq K@ Ak!   AvAq j"\nAtAÀ:j(AÿÿÿqM\r \nAj!\n  \nN\r A! AN @ Aj" O" E@  j-A$F\r !  kAK\rA!  (M\r Q"AÿM\rA!\nA!@@@@  \njAm"AtA°j(" Au j" AtA°j(Aÿÿÿq K@ Ak!   AvAq j"\nAtA°j(AÿÿÿqM\r \nAj!\n  \nN\r  AN\r A!\nAä!@@@  \njAm"AtAÀ:j(" Au j" AtAÀ:j(Aÿÿÿq K@ Ak!   AvAq j"\nAtAÀ:j(AÿÿÿqM\r \nAj!\n  \nN\r A! AN  @ Aj" O\r@  j-A k"AK\rA tA€€qE\r Aj" I\r ! A F@ !  kAI\r @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\r!Aå (, \r ! Aj6x (t"E\r  6t (x"Aj6x  Alj"A:  : B7  6  6  @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\r Aå (, \r Aj6x (t"E\r  6t (x"Aj6x  Alj"A€ ; B7  Aj"6  6  A! !  kAG\r A! !  kAG\r A F\r A F\r A\rkA}K\r AÿG AúKq A!kAI A:kAIr AÛkAIrr ! @ \rA! A!@  j" ( k" A7"AÿM@ A F\r A F\r A kAO\r  @@@  jAm"AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r  \nAN\r A!Aä! A7"AÿM@ AÿG AúKq A!kAI A:kAIr AÛkAIrr  @@@  jAm" AtAÀ:j("Au j"\nAtAÀ:j(Aÿÿÿq K@ \nAk!   AvAq j"AtAÀ:j(AÿÿÿqM\r Aj!  N\r A!\n \nAN ! !  Aq "E\r @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" : A$: B7  6  6  A F\r A F\r A\rkA}K\r AÿG AúKq A!kAI A:kAIr AÛkAIrr ! @ E\rA! A!@  j" ( k" A7"AÿM@ A F\r A F\r A kAO\r  @@@  jAm"AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r  \nAN\r A!Aä! A7"AÿM@ AÿG AúKq A!kAI A:kAIr AÛkAIrr  @@@  jAm" AtAÀ:j("Au j"\nAtAÀ:j(Aÿÿÿq K@ \nAk!   AvAq j"AtAÀ:j(AÿÿÿqM\r Aj!  N\r A!\n \nAN ! !  Aq "E\r @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" : Aþ: B7  6  6  A F\r A F\r A kAM\r !  A! ! AqE\r @@ (x" (|" N@ AÀ Av j AL"6| (t Als" \rA! ($"\rE\rAå (, \r  Aj6x (t" E\r  6t (x"Aj6x Alj" : Aþ: B7  \n6  6  @@  (F\r Q"AÿK@A!A!@@@  jAm" AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r !  \nAN\r !  A F\r A F\r A kAM\r !  A! ! AqE\r\r @@ (x" (|" N@ AÀ Av j AL"6| (t Als" \rA! ($"\rE\rAå (, \r  Aj6x (t" E\r  6t (x"Aj6x Alj" : A+: B7  6  6 \r @@  (F\r Q"AÿK@A!A!@@@  jAm" AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r !  \nAN\r !  A F\r A F\r A kAM\r !  A! ! AqE\r @@ (x" (|" N@ AÀ Av j AL"6| (t Als" \rA! ($"\rE\rAå (, \r  Aj6x (t" E\r  6t (x"Aj6x Alj" : A=: B7  6  6 A F\r A F\r A kAM\r !  A! ! AqE\r @@ (x" (|" N@ AÀ Av j AL"6| (t Als" \rA! ($"\rE\rAå (, \r  Aj6x (t" E\r  6t (x"Aj6x Alj" : AÞ: B7  6  6 @ Aj" O\r -A?G\rA!@ (ì" M@ !  !  \nAkO\r  Aj" \n \nI"M@  j/A¿üF\r@@ Aj! Aj" K\r " j/A¿üG\r  !  \nI Aj"Kq  Atj"(! (!  6ì ( ! @ M\r -A!G\r  j-AßqAÁkAÿqAK\r@ Aj" O\r@  j-AßqAÁkAÿqAK\r Aj" G\r ! A! (ð" K  \nAkOq\r Aj" \n \nI"M@  j-A>F\r@@ Aj! Aj" K\r " j-A>G\r  !  \nI Aj" Kq  Atj"(! (!  6ð ( ! @ A j" O\r )B¼ÂìšÄ¨ªÁ… 1Bۅ„BR\rA! (ô" K  \nAkOq\r@ Aj" \n \nI" M@  j"/Aݺs -A>srE\r@@ Aj! Aj" K\r  "j"/Aݺs -A>sr\r  !  \nI Aj"Kq@  Atj"(! (!  6ô ( !\n ! Aj" \nO\r@ Aj" \nO\r  j-" A0kAÿqA\nIA AßqAÁkAÿqAI E\r@  j-"A0kAÿqA\nI\r AßqAÁkAÿqAI\r A+k"AK\r AF\r Aj" \nF\r@  kA K\r  j-" A:F@  \nO\r@  j-" A k"AMAA tA€€q\r A>G@ A I\r AG\r  j-A-F\rAð! Aj !@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" Ar: A<: B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" Ar: A>: B7  6  Ak6 (t (x"Alj"A k Ak6 Ak Ak6 ! !   6 @@ (x" (|"N@ AÀ Av j AL"6| (t Als"E\r 6t (x"Aj6x  Aj6x (t"E\r\n  Alj"A¼(; B7  6  6@ (x" (|"N@ AÀ Av j AL"6| (t Als"E\r 6t (x"Aj6x  Aj6x (t"E\r\n ( !  Alj"A¾0; B7  6  6 (t (x"Alj"A k Ak6 Ak Ak6A! !  (M\r@@   jAv"Atj"( K@ E@ ! \n   AtjAk(K@ ! \n Ak!  ( O@ ! Aj!  M\r A!  ($"E\rAå (,   Aj!  A F\r A F\r A\rkA}K\r AÿG AúKq A!kAI A:kAIr AÛkAIrr "! As! A! @  \nF\rA!A!@@  \nj" ( \nk"A7"AÿM@ A G\r  @@@  jAm"AtA°j(" Au j" AtA°j(Aÿÿÿq K@ Ak!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r  A! AH\r  A F\r A kAI\r A!Aä!AA  A7"AÿM@ AÿG AúKq A!kAI A:kAIr AÛkAIrr  @@@  jAm"AtAÀ:j(" Au j" AtAÀ:j(Aÿÿÿq K@ Ak!   AvAq j"AtAÀ:j(AÿÿÿqM\r Aj!  N\r A! AN "! As! ! \n!A   q AßFq""A " rE\r  kAp!@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"\r  6t (x"Aj6x  Alj" AtA# AG  MqAA  OA r" AFrA@k:  : B7  \n6  6 ! \n "M\r@@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\r Aå (, \r Aj6x (t"\r  6t (x"Aj6x  Alj"AÄ; B7  6  6 Aj" \nI\r ! \n! @@ (x"\r (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  \rAj6x (t"E\r  6t (x"\rAj6x (!  \rAlj"Aÿ ; B7  6  6A!  A! Aj$@ " \r A (xA¿AM@ (à"AN@ Ak! A€j! AØj! A j!@ (t" Al"j" (!\r@ -"AÄF\r ( !\nA! A! \rAN@   \rAlj" ( Alj! \nAl! (! -A q@ A!: Ak"6A!!  j!  I@ ( I\r  I@ ( I\r @  I@ A!G\r  AÛG\r ( A€€ÀqE\r (" (O\r ( j-AÞG\r Aj"6 (t j"(" M\r  ( j  kN"E\r ( "E@ (pAj"6p  6  (Aj6 6 (6 -A”r:  -A˜r: (! (! (  @@@@ -\rA qE\r -AÛG\r E\r (" ("kAG\r -AÛG\r E\r ( ("kAG\r -AÝG\r  AkG\r ( ("kAG\r  (AjG\r@@ Aj" \nN"@ !  (t! !@@  Alj" -"AÄF\r AüF\r ( kAäK\r -AqE\r ( ! Aj" \nH\r A!  (" F\r  kAäK\r  I@ (!@@  j-A\nk Aj" G\r A!@   Atj"("L@ (t!\r@ \r Alj( " N\r  6  ("L@ (t!\r@ \r Alj( " N\r  6 Aj"AG\r @@ ( (I@@  L\r  kAk! (t!\rA!  AsjAq"@@ \r AljAÄ; Aj! Aj" G\r AI\r@ \r Alj"AÄ;L AÄ;8 AÄ;$ AÄ; Aj" G\r -Ar: (6  @ \r \n kAk! (t!\rA! \n AsjAq"@@ \r AljAÄ; Aj! Aj" G\r AI\r@ \r Alj"AÄ;L AÄ;8 AÄ;$ AÄ; Aj" \nG\r  (6 (6  (6 (t"\r j" \n6 \r j"\r 6  -Ar: \r \r-Ar: (! (!  Aj \nO (  (6  (6 (t"\r j" \n6 \r j"\r 6  -Ar: \r \r-Ar: (! (! (  \r (!  (" ("G\r ( AjK@ -AÀq\r    ( AjP  -AÀq\r   (  AjP "AH\r E\r  (6 (t \rAlj(!\r  @  ("O\r (" j-A(G\r \nAj! A! !@@@   jAv"Atj"( K@ E\r  AtjAk( I\r Ak!   (M\r Aj!  O\r A! @@ Aj"  Atj(" I@@  j-A k"AK\rA tA€€qE\r Aj" G\r !  6@  O\r  j-A\nk Aj" O\r  Atj(!  6 @  O\r  j-A)G\rA! A6 B7  6  6 Aj! A    Atj"(" Aj Aj 8E\r@   k ( Aj A j A,j A(j A$j9@ ( " j! (," F!   j"Atj(! (!   ("6(  6$A!A! @  I@@  j-"A k"AK\rA tA€€qE\r Aj" G\r Aj" O\r   Atj("j-! AÿqA)G\r  Aj" 6 ($" (("M@A! A6 B7A   k! @ A6  6   j"6A  @ q"@  Atj!A! !@   Atj("  K" KA!  "kAq"@@  ( j-: Aj! Aj! Aj" G\r  kA|M@@  ( j-:  ( j-:  ( j-:  ( j-: Aj! Aj" G\r   O\r A\n: Aj!  Aj"Atj(! ($" E\rAÔ (,   A6  6   k6 (! A ! (x" L@  6  (t! !@  Alj"(K@@ -AqAG\r  ( "Alj( I\r \r r  Aj" H\r  6  L\r  \nkAk! (t! A!  \nAsjAq"@@ AljAÄ; Aj! Aj" G\r AI\r@ Alj"AÄ; AÄ;L AÄ;8 AÄ;$ Aj" G\r  -AÀq\r   ( ( AjP"AH\r E\r -Ar:  -Ar: Aj" (tj (6 (t j ( 6  (tj (6( Aj! (t! (@  Alj (Ð6 6Ð  Alj (6 (! (! -!  Aj \nO   AÛF"!   !   !   ! ! / AŒqE\r@@ "Aj! -$"AÄG" \r  I\r À! !@@ "\nAk! \r  K\r -%" AqE\r E\r ( (G\rA÷ ATE\r (  (tkAmG\r \nAk( (G\r AÄ:$  Aïq:% \nAkAÄ: \nAk" -Aïq: \r "AN\r B7à A6¸ @A! A6Ô (x"AL\r (t!\rA!@@ \r Alj"-"Aq\r -AüG\r ( (kAG\r A6  Ar:@ (ØAH@ 6Ø  \r (ÜAlj 6 6Ü Aj"6Ô Aj" G\r A!  A! A (xO  A! A0j$ Þ#Aðk"$ B7h B7` B7X B7P B7H B7@ B78 B70 A6( B7 B7 B7 B7A! @    Aj AÐj<\r  A Aj A0j<\r  6( (,! @  Aj (" E@A!  ($"E\rA (,    Aj (" E@A!  ($"E\rAæ (,  (`@ (Pr (Tr (Xr (@@ (0r (4r (8r Aðj$ Ý@ ( " ("I@ (!  ( Av jA "At"s"E@A ($"E\rAå (, A  6 ( Ajs"E@A ($"E\rAå (, A  6  6 ( ! ( Atj 6  ( Atj 6  ( Aj6 A Ü Aj!@@@ Aj" O\r j-A#G\r j-A rAøG\r !@@@@ j-"A0kAÿqA\nI\r AÁkAÿqAI\r  kAK\r AákAÿqAI\r   kAK\r Aj" G\r !  F\r  kAI\r  @ Aj" O"\r j-A#G\r !@@  kAK\r j-A0kAÿqA K\r Aj" G\r !  F\r  kAI\r   M\r j-AßqAÁkAÿqAK\r  \r@@ j-"AßqAÁkAÿqAO@   kA0K\r A0kAÿqA\nI\r    kA0M\r   Aj" G\r  " kAkA.K\r  M\r j-A;G\rA!  Aj6  ³#Ak"$@@  O\r (!@  j-A k"AK\rA tA€€qE\r Aj" G\r !   O\r (!@  jAk-A k"AK\rA tA€€qE\r Ak" K\r !  6  6  6@  Aj (, ("@ ($"E\rAŠ (,   AjA?"\r  Aj (, ("E@A!  ($"E\rA¯ (,  Aj$  Ê"#A0k"\r$  H@ A¸j! A€j! \rAr! \rAr! (!@@ (t" Al" j"-"Aq@ AqE\r@ E\r ,"E\r  `\r ( !  ,"\nAÿq"E\r  \n`E\r (" I\r@@@@@@@@@@@@@@@@@ A!k^  (! Aq@ AN@  Alj" -AÀr:  6  6  AH\r  Alj"( 6¸  6  6@ (ä" AN@  Alj 6  6à 6ä A6  Aj" (xN\r  Alj"-A;G\r (  (" \rKE\r  6  6  Ar:  -Ar:  6  AqE\r \r A€A˜ \nA*Fj"A j6 AÀq" AÀF@ ! A  \r Aj6 A€F\r ! A ! Aj6A! A°A´ ( kAFj!\n@ AqE\rA! \n("AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6  6  -Ar:  -Ar: \r AqE\r  \n(6 \n 6 @ AqE\rA! (Ä"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6  6  -Ar:  -Ar: AqE\r  (Ä6 6Ä @ AqE\r (¼"AH\rA!  Alj" ( (k ( kG\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r @ Aj" N\r  kAk!A!  AsjAq"\n@@  AljAÄ; Aj! Aj" \nG\r AI\r@  Alj" AÄ;L AÄ;8 AÄ;$ AÄ; Aj" G\r 6  6 -Ar:  -Ar: A6¼ AqE\r\n  (¼6 6¼ \n  Aj"Al"j"(! (! \r 6 \r ("6, \r 6( \r 6$ \nAÀF@   \rAAAA˜ \rA(jRAL\r\n \r(!  O\r Q" AÿM\rA!A!@@@  jAm" AtA°j("Au j"AtA°j(Aÿÿÿq K@ Ak!  AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r  AN\r  ( kAG\r@ AqE\rA! (À"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6  6  -Ar:  -Ar: AqE\r  (À6 6À  ( kAG\r@ AqE\rA! (È"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6  6  -Ar:  -Ar:  AqE\r  (È6 6È  ( kAG\r@ AqE\rA! (Ì"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6  6  -Ar:  -Ar:  AqE\r  (Ì6 6Ì  A! A!  A F\r A F\r A kAI\r ( jAk,"@Aã AT\r \r((" I\r Ak!@@@ -AÄF\r ( K\r  (O\r -" Aq\r Ak" O\r  AqE\r   \rA,jAA.AAØ \rA$jRAH\r \r(,! @ \nAÀF\r@ O\r (" j-A/G\r Aj  \rA,jAA/AùA \rA$jRAH\r \r(," O\r  j-A/G\r \r Aj" 6, @ O\r ( j-A?G\r Aj  \rA,jAA&AóA• \rA$jRAH\r \r(,! O\r ( j-A#G\r Aj  \rA,jAAAAÛ \rA$jRAH\r \r(,! @ O\rA!A!@ ( j" ( kA7"AÿM@ A F\r A F\r A kAO\r  @@@  jAm" AtA°j("Au j"AtA°j(Aÿÿÿq K@ Ak!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r  AN\r ,"@A¡ A T\r \r($"  (xAlj"O\r@@@ -AÄF\r ( M\r ( K\r -"Aq\r Aj" I\r  AqE\r  6  6  \n:  6  6 (t" j" 6  j" 6  -Ar:  -Ar:  \r Atr 6 A q! Aj! AàqAàG@ \r Atj Aj6 Aj! As AGqE@ \r Atj Aj6 Aj!  \r(("AljA AN"! A !@ \r(("AH\r  Alj! @ ( (M\r ! ! @ AF\r@ \r(("AH\r  Alj! @ ( (M\r ! ! AF\r@ \r( ("AH\r  Alj! @ ( (M\r ! ! AF\r@ \r(("AH\r  Alj! @ ( (M\r ! ! AF\r \r((" AH\r  Alj! @ ( (M\r ! ! E\r (! (! (! !\n@@@@@@  Alj"-"A!k\n AÛk A€A˜ A*Fj -"AtAuA qj!\n@ AvAk \nAj!\n  A°A´ ( (kAFj!\n  \nAj!\n @ k" k"K@  (" (k k j"Al" j" (6  )7  )7  k"6  6 (t j \n(6 \n 6  O\r  Alj j" (6  )7  )7  k j"6  6 A!\n@   \nAtj"("L@@ AN@  (t Alj( "6  N\r \nAj"\nAG\r (t" Alj" 6  j" 6  -Ar:  -Ar:  AqE\r A€A˜ \nA*Fj AtAuA qj!@@@ AvAk Aj!  Aj!  (6  6 -"AqE\r Aq@ (t ( Alj(!  (! Aj" H\r \rA0j$ –#Ak"$@ (E\r  6! (  (pAtj("E@A!  @  ("I\r   ( (ljO\r  (F@ ( (  DE\r A!   6  6  6  Aj6 Aj!@ ("@@   Av"Atj"G"AH@   E\r Aj!  Asj "\r A! E@A!  (! Aj$  ý#A k"$ Aß: A¦Ô;A! ( "A€ q@ A$:A! A€€€q@ Aj rA=: Aj! A€€€q@ Aj rA+: Aj! A€€q@ Aj jAü: Aj! A€€q@ Aj jAÞ: Aj! A€„q@ Aj jAþ: Aj! A! Aj jA:    AjAM ( "AŒq@ Aq@ AÀ:A! Aq@  rA:: Aj! A€q  rA.: Aj  rA:     AjM A€jAÿAÐü A j$ • (TE@A Ak! Ak!AA ("\n j-A!F j!A!@@@   jAv"Atj" ( K@ E\r  AtjAk( I\r Ak!   (M\r Aj!  O\r A! @  ("M" E@@  kq"@ !@    K" KA!  "kAq"\n@@  ( j-: Aj! Aj! Aj" \nG\r  kA|M@@  ( j-:  ( j-:  ( j-:  ( j-: Aj! Aj" G\r   O\r A : Aj! Aj" Atj"(! (! A! ($"E\rAÔ (, A  k   \nj!  k ! A  Aj60A! Aj !\nA!@@ \n("E\r Aj p"AH\r   kK\r \nAj!\n  j" I\r A=! AH\r A \r  l E@A!  A! (0!\n@ \n(" E\r Aj p" j" K\r Aj h \nAj!\n  K\r A \r  A€Àsl \r   \rH!   AHq\r A=! +0 \r  m"AN\r \n -!\n Aj! \r E\rA!@  Atj("\n@  Atj \n jA! Aj"A\nG\r A\nO@A! \n @  Atj(\rA! Aj"A\nG\r A!   :\'A! ! !   k"  H" AÿÿÿÿsJ\rA=! \r  j"  \rH" \nK\r A   l  h A0   A€€sl A0  Al h A   A€Àsl ( Ak" O\r B€”ëÜT\r Ak" > @ "I@ Ak"(E\r (, k"6, ! AJ\r AH@ AjA nAj! AæF!@A A k" A O!\n@  M@AA (!  A€”ëÜ \nv!\rA \ntAs!A! !@  (" \nv j6  q \rl! Aj" I\r AA (! E\r  6 Aj! (, \nj"6,   j" " Atj   kAu J! AH\r A!@  M\r  kAuA l!A\n! ("A\nI\r@ Aj!  A\nl"O\r A AæGk AçF AGqk"  kAuA lA kH@ A0jA„`A¤b AHj A€Èj"A m"\rAtj!\nA\n!  \rA lk"AL@@ A\nl! Aj"AG\r @ \n("  n" lk"\rE \nAj" Fq\r@ AqE@D@C! A€”ëÜG\r \nO\r \nAk-AqE\r D@C! Dà?Dð?Dø?  FDø? \r Av"F \r I!@ \r -A-G\r š! š! \n  \rk"6    a\r \n  j"6 A€”ëÜO@@ \nA6 \nAk"\nK@ Ak" A6 \n \n(Aj"6 Aÿ“ëÜK\r  kAuA l!A\n! ("A\nI\r@ Aj!  A\nl"O\r \nAj"   K! @ " M"E@ Ak"(E\r @ AçG@ Aq!\r  AsA A " J A{Jq"\n j! AA~ \n j! Aq"\r\rAw!@ \r Ak("\nE\rA\n!A! \nA\np\r@ "\rAj! \n A\nl"pE\r \rAs!  kAuA l! A_qAÆF@A!\r  jA k"A AJ"  J!  A!\r  j jA k"A AJ"  J! A!\n AýÿÿÿAþÿÿÿ \rr"J\r AGjAj!@ A_q"AÆF@  AÿÿÿÿsJ\r A AJ!    Au"s k­ k"kAL@@ Ak"A0:  kAH\r Ak" : AkA-A+ AH:  k" AÿÿÿÿsJ\r  j" AÿÿÿÿsJ\r A   j" l  h A0   A€€sl@@@ AÆF@ AjA r!  K"! @ 5 k!@  G@  AjM\r@ Ak"A0:  AjK\r   G\r Ak"A0:   kh Aj" M\r @ AùAh  M\r AL\r@ 5 k" AjK@@ Ak"A0:  AjK\r A A Nh A k! Aj" O\r A J! ! \r  @ AH\r  Aj  K!\n AjA r! !@  5 k"F@ Ak"A0: @  G@  AjM\r@ Ak"A0:  AjK\r  Ah Aj! \rrE\r AùAh   k"  Hh k! Aj" \nO\r AN\r A0 AjAAl   kh  ! A0 A jA Al A   A€Àsl    J!\n   AtAuA qj!@ A K\rA k!D0@!@ D0@¢! Ak"\r -A-F@  š ¡ š!     ¡!  (," Au"s k­ k"F@ Ak"A0: (,! Ar!\r A q! Ak" Aj: AkA-A+ AH: AqE ALq! Aj!@ " ü"AÐÑj- r:  ·¡D0@¢!@ Aj" AjkAG\r Da q\r A.: Aj! Db\r A!\n Aýÿÿÿ \r  k" j"kJ\r A   Aj  Ajk" Ak H  "j" l  \rh A0   A€€sl Aj h A0  kAAl  h A   A€Àsl    J!\n A°j$ \n ¹~  (AjAxq"Aj6 )! )!#A k"$ Bÿÿÿÿÿÿ?ƒ!~ B0ˆBÿÿƒ"§"AøkAýM@ B† B<ˆ„! A€øk­!@ Bÿÿÿÿÿÿÿÿƒ"B€€€€€€€Z@ B|!  B€€€€€€€€R\r Bƒ |! B  BÿÿÿÿÿÿÿV"! ­ |  @  „P\r BÿÿR\r B† B<ˆ„B€€€€€€€„!Bÿ  Aþ‡K@B!Bÿ  A€øAø P"\n" k" AðJ@B!B   B€€€€€€À„ \n!A!\n G@ Aj! ! !@A€ k"AÀq@  A@j­†!B!  E\r  ­"† AÀ k­ˆ„!  †! 7 7 ) )„BR!\n @ AÀq@  A@j­ˆ!B!  E\r AÀ k­†  ­"ˆ„!  ˆ!  7  7 )B† )"B<ˆ„!@ \n­ Bÿÿÿÿÿÿÿÿƒ„"B€€€€€€€Z@ B|!  B€€€€€€€€R\r Bƒ |! B€€€€€€€…  BÿÿÿÿÿÿÿV"! ­ ! A j$ B€€€€€€€€€ƒ B4†„ „¿9 ¦ (T"(! (" ( ("k"  I"@   V  ( j"6  ( k"6    K"@   V  ( j"6  ( k6 A: (,"6 6  ¢ E@A @  AÿM\r@AìÞ((E@ A€qA€¿F\rAÐÝA6  AÿM@ A?qA€r: AvAÀr:A  A€@qA€ÀG A€°OqE@ A?qA€r: A vAàr: AvA?qA€r:A  A€€kAÿÿ?M@ A?qA€r: AvAðr: AvA?qA€r: A vA?qA€r:A  AÐÝA6 AA  :A Ñ( #Ak"\n$@@@@@@@@@@ AôM@Aß("A A jAøq A I"Av"v"Aq@@ AsAq j"At"A¸ßj" (À_"("F@Aß A~ wq6   6 6 Aj!  Ar6  j" (Ar6 A˜ß("M\r @@ tA t"A krqh"At"A¸ßj" (À_"("F@Aß A~ wq"6   6  6 Ar6 j"  k"Ar6 j 6 @ AxqA¸ßj!A¤ß(! A Avt"qE@Aß  r6   ( !  6  6  6  6 Aj!A¤ß 6A˜ß 6 A”ß(" E\r hAt(Àa"(Axq k! !@@ ("E@ ("E\r (Axq k"   K"!  ! !  (!  ( "G@ (" 6 6 \n (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6 A! A¿K\r A j"Axq!A”ß(" E\rA! AôÿÿM@ A& Avg"kvAq AtkA>j! A k!@@@ At(Àa"E@A!  A! A AvkA AGt!@@ (Axq k" O\r ! "\rA! !  ("   AvAqj("F ! At! "\r rE@A!A t"A kr q"E\r hAt(Àa! E\r @ (Axq k" I!   !  ! ("  ( "\r E\r A˜ß( kO\r (!  ( "G@ (" 6 6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  A˜ß("M@A¤ß(!@ k"AO@  j" Ar6 j 6  Ar6   Ar6 j" (Ar6A!A! A˜ß 6A¤ß 6 Aj! Aœß("I@Aœß  k"6A¨ßA¨ß(" j"6  Ar6 Ar6 Aj! A! A/j"Aèâ(@Aðâ(  AôâB7AìâB€ €€€€7Aèâ \nA jApqAتժs6AüâA6AÌâA6A€ "j"A k"q" M\rAÈâ("@AÀâ(" j" M\r  I\r @AÌâ-AqE@@@@@A¨ß("@AÐâ!@ (" M@   (jI\r ("\r Au"AF\r !Aìâ("Ak" q@  k  jA kqj!  O\rAÈâ("@AÀâ(" j" M\r I\r u" G\r   k q"u" ( (jF\r ! AF\r A0j M@ !  Aðâ("  kjA kq"uAF\r  j! !  AG\r AÌâAÌâ(Ar6 u!Au! AF\r AF\r M\r k" A(jM\r AÀâAÀâ( j"6AÄâ( I@AÄâ 6 @A¨ß("@AÐâ!@  (" ("jF\r ("\r  A ß("A ME@A ß 6 A!AÔâ 6AÐâ 6A°ßA6A´ßAèâ(6AÜâA6@ At" A¸ßj"6À_  6Ä_ Aj"A G\r Aœß A(k"Ax kAq"k"6A¨ß  j"6  Ar6 jA(6A¬ßAøâ(6   O\r  I\r ( Aq\r  j6A¨ß Ax kAq"j"6AœßAœß( j" k"6  Ar6  jA(6A¬ßAøâ(6  A!  A!  A ß( K@A ß 6  j!AÐâ!@@  ("G@ ("\r  - AqE\r AÐâ!@@ (" M@   (j"I\r (!  Aœß A(k"Ax kAq"k"6A¨ß  j"6  Ar6 jA(6A¬ßAøâ(6  A\' kAqjA/k" AjI"A6 AØâ)7 AÐâ)7AØâ Aj6AÔâ 6AÐâ 6AÜâA6 Aj!@ A6 Aj! Aj!  I\r  F\r  (A~q6   k"Ar6  6 AÿM@ AøqA¸ßj!Aß("A Avt"qE@Aß  r6  ( ! 6  6 A !A  A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÀáj!@@A”ß("A t"qE@A”ß  r6  6  6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6  6 A! ! !A  (" 6  6  6A!A!A j 6  j 6 Aœß(" M\rAœß k"6A¨ßA¨ß(" j"6  Ar6 Ar6 Aj!  AÐÝA06A!  6 ( j6 Ax kAqj" Ar6 Ax kAqj"  j"k!@A¨ß( F@A¨ß 6AœßAœß( j"6  Ar6  A¤ß( F@A¤ß 6A˜ßA˜ß( j"6  Ar6  j 6  ("AqAF@ Axq! ( !@ AÿM@ (" F@AßAß(A~ Avwq6  6  6  (!@  G@ (" 6  6  @ (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  A! E\r@ ("At"(Àa F@ AÀáj 6 \rA”ßA”ß(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6  j"(!  j!  A~q6  Ar6  j 6 AÿM@ AøqA¸ßj!Aß("A Avt"qE@Aß  r6   ( !  6  6  6  6  A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÀáj!@@A”ß("A t"qE@A”ß r6  6  6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6  6  6  6  (" 6  6 A6  6  6 Aj!  @ E\r@ ("At"(Àa F@ AÀáj 6 \rA”ß A~ wq" 6  @  (F@  6   6 E\r 6 ("@ 6  6 ("E\r 6  6 @ AM@   j"Ar6 j" (Ar6   Ar6  j" Ar6  j 6 AÿM@ AøqA¸ßj!Aß("A Avt"qE@Aß  r6  ( ! 6  6  6  6  A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÀáj!@@ A t"qE@A”ß  r6  6  6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6  6  6  6  (" 6  6 A6  6  6 Aj!  @ E\r@ ("At"(Àa F@ AÀáj 6 \rA”ß A~ wq6  @  (F@ 6  6 E\r 6 ("@ 6  6 ("E\r 6  6 @ AM@   j"Ar6 j" (Ar6   Ar6  j" Ar6  j 6 @ AxqA¸ßj!A¤ß(!A Avt" qE@Aß  r6   ( !  6  6 6 6 A¤ß 6A˜ß 6 Aj! \nAj$ ‚ @ E\r Ak" Ak("Axq"j!@ Aq\r AqE\r  ("k"A ß(I\r j!@@@A¤ß( G@ ( ! AÿM@  ("G\rAßAß(A~ Avwq6  (!  G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  ("AqAG\rA˜ß 6  A~q6  Ar6  6  6  6  A! E\r@ ("At"(Àa F@ AÀáj 6 \rA”ßA”ß(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6  O\r ("AqE\r@@@@ AqE@A¨ß( F@A¨ß 6AœßAœß( j"6  Ar6 A¤ß(G\rA˜ßA6A¤ßA6 A¤ß(" F@A¤ß 6A˜ßA˜ß( j"6  Ar6 j 6 Axq j! ( ! AÿM@ (" F@AßAß(A~ Avwq6   6  6  (!  G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6   A~q6  Ar6 j 6  A! E\r@ ("At"(Àa F@ AÀáj 6 \rA”ßA”ß(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6  Ar6 j 6  G\rA˜ß 6 AÿM@ AøqA¸ßj!Aß("A Avt"qE@Aß r6   ( !  6 6  6  6 A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÀáj!@A”ß("A t"qE@A”ß  r6  6A!A  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6A! !A ! !   (" 6  6A!A!A !  j 6  6 j 6A°ßA°ß(Ak"A 6 ‰  E@ q A@O@AÐÝA06A A A jAxq A I! Ak"("Axq!@ AqE@ A€I\r Aj M@ !  kAðâ(AtM\r A   j!@  O@  k"AI\r   AqrAr6  j" Ar6  (Ar6  t  A¨ß( F@Aœß( j" M\r   AqrAr6  j"  k"Ar6Aœß 6A¨ß 6  A¤ß( F@A˜ß( j" I\r@  k"AO@   AqrAr6  j" Ar6  j" 6  (A~q6   Aq rAr6  j" (Ar6A!A! A¤ß 6A˜ß 6  ("Aq\r Axq j"\n I\r \n k! ( !@ AÿM@ (" F@AßAß(A~ Avwq6   6  6  (! @  G@ (" 6  6  @ (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  A! E\r@ ("At"(Àa F@ AÀáj 6 \rA”ßA”ß(A~ wq6  @  (F@ 6  6 E\r  6 ("@  6  6 ("E\r  6  6 AM@  Aq \nrAr6  \nj" (Ar6    AqrAr6  j" Ar6  \nj" (Ar6  t !  "@ Aj q"E@A  A|Ax Ak("Aq Axqj"   KV r  «  j!@@ ("Aq\r AqE\r (" j!@@@ k"A¤ß(G@ ( ! AÿM@  ("G\rAßAß(A~ Avwq6  (! G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  ("AqAG\rA˜ß 6  A~q6 Ar6  6  6  6  A! E\r@ ("At"(Àa F@ AÀáj 6 \rA”ßA”ß(A~ wq6  @ (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6 @@@@ ("AqE@A¨ß( F@A¨ß 6AœßAœß( j"6 Ar6 A¤ß(G\rA˜ßA6A¤ßA6 A¤ß(" F@A¤ß 6A˜ßA˜ß( j"6 Ar6 j 6 Axq j! ( ! AÿM@ (" F@AßAß(A~ Avwq6   6  6  (!  G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6   A~q6 Ar6 j 6  A! E\r@ ("At"(Àa F@ AÀáj 6 \rA”ßA”ß(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6 Ar6 j 6 G\rA˜ß 6 AÿM@ AøqA¸ßj!Aß("A Avt"qE@Aß  r6   ( !  6  6 6 6 A! AÿÿÿM@ A& Avg"kvAq AtrA>s! 6 B7 AtAÀáj!@@A”ß("A t"qE@A”ß  r6  6 6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6 6 6 6 (" 6  6 A6 6 6 W~@ ­B|BøÿÿÿƒAÄÜ("­|"BÿÿÿÿX@ §"?AtM\r \r AÐÝA06A AÄÜ 6  ² #Ak"$  6 @ AÓM@AàÑA Ó A jx(!  A|O@y  AÒn"AÒl"k6A ÓAàÔ Ajx"( j! A ÓkAu!@A!@@@ "A/F\r At(àQ"n" I\r Aj!  lG\r A/I\r AÓ!@ n" I\r  lF\r A\nj"n" I\r  lF\r A j"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r A$j"n" I\r  lF\r A(j"n" I\r  lF\r A*j"n" I\r  lF\r A.j"n" I\r  lF\r A4j"n" I\r  lF\r A:j"n" I\r  lF\r A`-_.-+_\\\\({[,"children":[NANINFcatching a class without an object?)}].!?,;mailto:,"content":h6h5h4h3h2h1http://Too many link reference definition instantiations.Unsupported abi_version.Aborted from text() callback.Aborted from enter_span() callback.Aborted from leave_span() callback.Aborted from enter_block() callback.Aborted from leave_block() callback.malloc() failed.realloc() failed.@:..!#$%&\'*+/=?^_`{|}~-_.:-+-.-_+-_*(null)Suppressing too sparse table (see https://github.com/mity/md4c/issues/345)._=().+-_~%false && "unhandled NodeType in nodeTypeToString"\\"{"type":"[]!xX \n™Jìi¿e£<‡ ¢°üOs{±2‡+F< ÂÎ._VÉšAÜ ~ô H, „*NSt3__220__shared_ptr_emplaceIN8Markdown15MarkdownASTNodeENS_9allocatorIS2_EEEE8™(A€ ä A  f\r0\r`\r\r\rÐ\r\rp\r\r€ Àà\r\r @ \r\r\r\rA UmŒA° !þ\n-Aà Á™ÅA 5¨]¸‹;OAÐ 5F¾\n&’îa=A MM J G D A > ¬¨Að `A€ •EA  ܘóAÀ \r”nAà ¼¨A€ \r’A  ±Ô$AÀ Eu,’Ø–«gYA  A  ”A° —  € @\n €/ _ 0A@Z€µÀ@րØ@ހ@.€2@6€9@G€J@v€xy@}€‚„†‡‰Š‹Ž‘“”–—˜œŸ @¤€¦§©¬®¯±²³µ·¸¼ÄÅÇÈÊË@Û€Þ@î€ñòôö÷ø@€ "@2€:;=>ACDEF@N€Eprv†ˆ@Š€ŒŽ‘@¡€£@«€ÂÏÐÑÕÖØ@î€ðñôõ÷ùúý@ÿ€@€@/€`@€€Š@¾€ÀÁ@Í€Ð@.€1@V€ @Å€ÇÍø@ý€€‚ƒ„…†‡ˆ‰@º€½@¿€@”€› @þ€@€@€(@/€8@?€H@M€Y[]_h@o€¸¹º»¾È@Ë€ØÙÚÛèéêëìøùúû&!*!+!2!`!@o!€ƒ!¶$@Ï$€,@/,€`,b,c,d,g,@k,€m,n,o,p,r,u,~,,€,@â,€ë,í,ò,@¦@l¦€€¦@š¦€"§@.§€2§@n§€y§{§}§~§@†§€‹§§§’§–§@¨§€ª§«§¬§­§®§°§±§²§³§´§@§€Ä§Å§Æ§Ç§É§Ë§Ì§@Ú§€Ü§Ý§â§õ§l«m«p«@¿«€!ÿ@:ÿ€@\'€°@Ó€p@z€|@Š€Œ@’€”•€ @² €P\r@e\r€ @¿€@n@_n€ n@¸n€@ßHßJßMßQßhß@n߀rß@~߀é@!é€az¼àöøþ/37:HKwÿz~sSƒ…TˆVWŒÝY[’`cih™oru¡¥€¨ƒ­ˆ°Š‹´¶’¹½ÆÆÉÉÌÌÜßïóóõ•¿ùž#3e,<šf,B€‰ŒGO¹qswó¬­¯ÌÍαÁÃËÃײ¸ÆÀÙïºÁ¸µøòû{}P_0Oa‹¿ÏÂÎÑ/a†-%-\'---ðõ24>ABBJcK¦ŠÐúýÿ•a¡ÿ \'07@EQSUW`g°±pq¹ruÐÑvwàáz{åxy|}ÉkåN!p!!„!Ð$é$0,_,a,k}}h,l,QqPRs,v,?@,ã,ì,î,ó,A¦m¦¦›¦#§/§3§o§z§|§y§‡§Œ§e‘§“§—§©§f\\aljž‡S«µ§Ã§”§‚Žȧʧdͧۧ›w|ö§K«L« ïAÿZÿ(OØû—¡£±³¹»¼À ò p\r…\rÀß`nn»nÓnAßIßKßNßRßißoßsßß"éCéAÐ2 Óß0Ið‡–—˜™šžP€@‡€ˆ@€@—€˜@Ÿ€ @§€¨@¯€²³´¶¼ÂÃÄÆÌÖäæòóôöüûûûûûûûûûû•ßA°4 ¥ssi¼nj e‚h1tw\ny\na¾ssʹ¹¹ ¹\'¹ ¹\'¹`¹g¹`¹g¹p¹±¹¬¹±B±¹t¹·¹®¹·B·¹¹BÁÅB|¹É¹Î¹ÉBɹfffiflststtvtetk~vtmssAà7 À°RTV·ÇÒÓ×âãç÷ûû¹ÅÅÅÅB±B¹·B¹¹¹¹BÅÅÅBÉB¹ffifflА.P05à  A´: ½!@/€:@@€[@`€{@~€¡@©€«@¬€®@±€´¶@¸€»¿×÷Â@Å€Ò@߀å@ë€íï@ÿ€u~„@…€‡ö‚Z@_€‰@Š€@€¾ÀÃÆó@ô€@€@€j@m€ÔÞéý@þ€@\r€ö@ù€þ@ÿ€0@>€^ˆd @e €p ò @ó €ú @û €ý v\nð\n@ñ\n€p ó @ú €w  „ O\ry\rô\r?OZ@[€@€@€468:@=€…¾@Å€Ç@Ì€Î@Ú€J@O€ž@Ÿ€û`@h€@™€m@n€›@œ€ë@í€5@6€Ô@Ö€Ø@Û€@\n€@D@E€Þ@ÿ€@€ @¦€¨@­€N@O€Z@j€t@€ü@ÿ€;@?€~@€À@Ç€Ó½¿@Á€Í@Ï€Ý@߀í@ï€ý@þ€ @\' €0 @^ €z @~ €Š @Ž €  @Ä €!@!€!@!€!@ !€!!@!€!@#!€%!\'!)!.!:!@;!€@!@D!€J!@M!€O!Š!@‹!€!@)$€@$@J$€œ$@é$€%@u\'€”\'@s+€v+@ÿ+€å,@ê,€ù,@ü,€þ,@ÿ,€p-.@..€0.@].€`.@c.€€.@™.€›.@ó.€/@Õ/€ð/@ÿ/€0@0€0@ 0€0060@70€=0@?0€›0@œ0€ 0û01@‘1€–1@Ÿ1€À1@å1€ï12@2€*2@G2€P2`2@2€Š2@°2€À2@ÿ3€ÀM@ÿM€¤@Ƥ€þ¤@ÿ¤€\r¦@¦€s¦~¦ò¦@÷¦€§@§€ §@!§€‰§@Ч€(¨@+¨€6¨@9¨€t¨@w¨€Î¨@Ϩ€ø¨@ú¨€ü¨.©@/©€_©Á©@Í©€Þ©@ß©€\\ª@_ª€wª@yª€Þª@ߪ€ðª@ñª€[«j«@k«€ë«)û²û@Òû€>ý@Oý€ý@‘ý€Èý@Ïý€üý@ÿý€þ@þ€0þ@Rþ€Tþ@fþ€hþ@kþ€ÿ@ÿ€ÿ@ ÿ€;ÿ@@ÿ€[ÿ@eÿ€àÿ@æÿ€èÿ@îÿ€üÿ@ýÿ€@€7@?€y@‰€Œ@Ž€@œ€ Ð@ü€ŸÐoWw@x€ ? P\n@X\n€\nÈ\nð\n@ö\n€9 @? €™ @œ €n\rŽ\r@\r€­É@Ê€Ð@Ø€U@Y€†@‰€G@M€»@¼€¾@Á€@@C€t@u€Å@È€ÍÛÝ@߀8@=€©Ô@Õ€×@Ø€K@O€Z@[€]ÆÁ@×€A@C€`@l€¹<@?€;D@F€â?@F€š@œ€ž@¢€@ €áA@E€p@q€÷@ø€C@O€Õ@ñ€ÿp$@t$€ñ/@ò/€nj@oj€õj7k@?k€Dk@Ek€mm@om€—n@šn€âoœ¼Ÿ¼Ì@ïÌ€úÌ@üÌ€Í@³Î€ºÎ@Ð΀ÒÎ@Ô΀ÝÎ@ý΀PÏ@ÃÏ€Ð@õЀÑ@&Ñ€)Ñ@dÑ€jÑ@lÑ€ƒÑ@„Ñ€ŒÑ@©Ñ€®Ñ@AÒ€EÒSÒ@ZÒ€]Ò@^Ò€`Ò@Ò€Ó@VÓ€ÁÖÛÖûÖ×5×O×o׉ש×Ãר@ÿÙ€7Ú@:Ú€mÚ@tÚ€vÚ@ƒÚ€…Ú@‹Ú€Û@Û€Oáÿâÿå^é@_逬ì°ì.íðî@ñî€ð@+ð€0ð@“ð€ ð@®ð€±ð@¿ð€Áð@Ïð€Ñð@õð€\rñ@®ñ€æñ@ò€ò@;ò€@ò@Hò€Pò@Qò€`ò@eò€ó@Ùö€Üö@ìö€ðö@üö€÷@Û÷€à÷@ë÷€ð÷@ ø€ø@Gø€Pø@Yø€`ø@‡ø€ø@­ø€°ø@»ø€Àø@Áø€Ðø@Øø€ù@Wú€`ú@mú€pú@|ú€€ú@Æú€ÈúÌú@Ýú€ßú@ëú€ïú@úú€û@’û€”û@ïû€úû A€Î A  \n\n\n   AÑÎ ! \r\r  A‹Ï  A—Ï  AÅÏ AÑÏ  AÿÏ A‹Ð  AÂÐ  AóÐ AÿÐ  A­Ñ A¹Ñ € 0123456789ABCDEF \r%)+/5;=CGIOSYaegkmqƒ‰‹•—£§­³µ¿ÁÅÇÓ \r%)+/5;=CGIOSYaegkmqyƒ‰‹•—£§©­³µ»¿ÁÅÇÑ ,h*NSt3__214__shared_countE¤,œ*`*NSt3__219__shared_weak_countEAÄÕ ø\ndè\' †@B€–˜áõʚ;00010203040506070809101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899H,Ä+(.N10__cxxabiv116__shim_type_infoEH,ô+¸+N10__cxxabiv117__class_type_infoEè+ !"#$%&h,\'!"#()*H,t,è+N10__cxxabiv120__si_class_type_infoEÄ,+!"#,-.H,Ð,è+N10__cxxabiv121__vmi_class_type_infoEL-/0h-124-34 ,<-St9exceptionH,X-4-St9bad_allocH,t-L-St20bad_array_new_length¤-56H,°-4-St11logic_errorÔ-76H,à-¤-St12length_error.86H,.¤-St12out_of_range ,0.St9type_infoAÁÜ  1€')}function getBinarySync(file){return file}async function getWasmBinary(binaryFile){return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={env:wasmImports,wasi_snapshot_preview1:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var ___assert_fail=(condition,filename,line,func)=>abort(`Assertion failed: ${UTF8ToString(condition)}, at: `+[filename?UTF8ToString(filename):"unknown filename",line,func?UTF8ToString(func):"unknown function"]);class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var exceptionLast=0;var uncaughtExceptionCount=0;var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var __abort_js=()=>abort("");var getHeapMax=()=>536870912;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};for(var base64ReverseLookup=new Uint8Array(123),i=25;i>=0;--i){base64ReverseLookup[48+i]=52+i;base64ReverseLookup[65+i]=i;base64ReverseLookup[97+i]=26+i}base64ReverseLookup[43]=62;base64ReverseLookup[47]=63;{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}Module["ccall"]=ccall;Module["cwrap"]=cwrap;Module["UTF8ToString"]=UTF8ToString;var _parseMarkdown,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_parseMarkdown=Module["_parseMarkdown"]=wasmExports["parseMarkdown"];__emscripten_stack_restore=wasmExports["_emscripten_stack_restore"];__emscripten_stack_alloc=wasmExports["_emscripten_stack_alloc"];_emscripten_stack_get_current=wasmExports["emscripten_stack_get_current"];memory=wasmMemory=wasmExports["memory"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={__assert_fail:___assert_fail,__cxa_throw:___cxa_throw,_abort_js:__abort_js,emscripten_resize_heap:_emscripten_resize_heap};function run(){preRun();function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})} ++;return moduleRtn}})();if(typeof exports==="object"&&typeof module==="object"){module.exports=createMd4cModule;module.exports.default=createMd4cModule}else if(typeof define==="function"&&define["amd"])define([],()=>createMd4cModule); +diff --git a/node_modules/react-native-enriched-markdown/src/types/MarkdownRenderers.ts b/node_modules/react-native-enriched-markdown/src/types/MarkdownRenderers.ts +new file mode 100644 +--- /dev/null ++++ b/node_modules/react-native-enriched-markdown/src/types/MarkdownRenderers.ts +@@ -0,0 +1,21 @@ ++import type { ComponentType, ComponentProps, ReactNode } from 'react'; ++import type { LinkPressEvent } from './events'; ++ ++export interface CodeBlockRendererProps { ++ /** Code content, including its original trailing whitespace. */ ++ code: string; ++ /** Fence language, or an empty string for an unlabelled block. */ ++ language: string; ++} ++ ++export interface LinkRendererProps { ++ link: LinkPressEvent; ++ anchorProps: ComponentProps<'a'>; ++ children: ReactNode; ++} ++ ++/** Custom web components. Overrides own their styling and interactions. */ ++export interface MarkdownRenderers { ++ codeBlock?: ComponentType; ++ link?: ComponentType; ++} +diff --git a/node_modules/react-native-enriched-markdown/lib/typescript/src/types/MarkdownRenderers.d.ts b/node_modules/react-native-enriched-markdown/lib/typescript/src/types/MarkdownRenderers.d.ts +new file mode 100644 +--- /dev/null ++++ b/node_modules/react-native-enriched-markdown/lib/typescript/src/types/MarkdownRenderers.d.ts +@@ -0,0 +1,20 @@ ++import type { ComponentType, ComponentProps, ReactNode } from 'react'; ++import type { LinkPressEvent } from './events'; ++export interface CodeBlockRendererProps { ++ /** Code content, including its original trailing whitespace. */ ++ code: string; ++ /** Fence language, or an empty string for an unlabelled block. */ ++ language: string; ++} ++export interface LinkRendererProps { ++ link: LinkPressEvent; ++ anchorProps: ComponentProps<'a'>; ++ children: ReactNode; ++} ++ ++/** Custom web components. Overrides own their styling and interactions. */ ++export interface MarkdownRenderers { ++ codeBlock?: ComponentType; ++ link?: ComponentType; ++} ++//# sourceMappingURL=MarkdownRenderers.d.ts.map +\ No newline at end of file +diff --git a/node_modules/react-native-enriched-markdown/src/index.tsx b/node_modules/react-native-enriched-markdown/src/index.tsx +--- a/node_modules/react-native-enriched-markdown/src/index.tsx ++++ b/node_modules/react-native-enriched-markdown/src/index.tsx +@@ -1,3 +1,8 @@ ++export type { ++ MarkdownRenderers, ++ CodeBlockRendererProps, ++ LinkRendererProps, ++} from './types/MarkdownRenderers'; + export type { LinkVariantStyle, LinkPillStyle } from './types/MarkdownStyle'; + export type { LinkPillContent } from './types/MarkdownTextProps'; + export type { +diff --git a/node_modules/react-native-enriched-markdown/lib/typescript/src/index.d.ts b/node_modules/react-native-enriched-markdown/lib/typescript/src/index.d.ts +--- a/node_modules/react-native-enriched-markdown/lib/typescript/src/index.d.ts ++++ b/node_modules/react-native-enriched-markdown/lib/typescript/src/index.d.ts +@@ -1,3 +1,4 @@ ++export type { MarkdownRenderers, CodeBlockRendererProps, LinkRendererProps, } from './types/MarkdownRenderers'; + export type { LinkVariantStyle, LinkPillStyle } from './types/MarkdownStyle'; + export type { LinkPillContent } from './types/MarkdownTextProps'; + export type { LinkContextMenuItem, LinkRecognition, } from './types/MarkdownTextProps'; +diff --git a/node_modules/react-native-enriched-markdown/src/types/MarkdownTextProps.ts b/node_modules/react-native-enriched-markdown/src/types/MarkdownTextProps.ts +--- a/node_modules/react-native-enriched-markdown/src/types/MarkdownTextProps.ts ++++ b/node_modules/react-native-enriched-markdown/src/types/MarkdownTextProps.ts +@@ -1,3 +1,4 @@ ++import type { MarkdownRenderers } from './MarkdownRenderers'; + import type { ColorValue, ViewProps, ViewStyle, TextStyle } from 'react-native'; + import type { MarkdownStyle, Md4cFlags } from './MarkdownStyle'; + import type { AccessibilityLabels } from './AccessibilityLabels'; +@@ -154,6 +155,8 @@ + * @platform ios, android, web + */ + markdown: string; ++ /** Custom block components. Ignored on native. @platform web */ ++ renderers?: MarkdownRenderers; + /** + * Style configuration for markdown elements. + * @platform ios, android, web +diff --git a/node_modules/react-native-enriched-markdown/src/index.web.tsx b/node_modules/react-native-enriched-markdown/src/index.web.tsx +--- a/node_modules/react-native-enriched-markdown/src/index.web.tsx ++++ b/node_modules/react-native-enriched-markdown/src/index.web.tsx +@@ -1,3 +1,8 @@ ++export type { ++ MarkdownRenderers, ++ CodeBlockRendererProps, ++ LinkRendererProps, ++} from './types/MarkdownRenderers'; + export { EnrichedMarkdownText, default } from './web/EnrichedMarkdownText'; + export type { EnrichedMarkdownTextProps } from './types/MarkdownTextProps.web'; + export type { MarkdownStyle, Md4cFlags } from './types/MarkdownStyle'; +diff --git a/node_modules/react-native-enriched-markdown/lib/typescript/src/index.web.d.ts b/node_modules/react-native-enriched-markdown/lib/typescript/src/index.web.d.ts +--- a/node_modules/react-native-enriched-markdown/lib/typescript/src/index.web.d.ts ++++ b/node_modules/react-native-enriched-markdown/lib/typescript/src/index.web.d.ts +@@ -1,3 +1,4 @@ ++export type { MarkdownRenderers, CodeBlockRendererProps, LinkRendererProps, } from './types/MarkdownRenderers'; + export { EnrichedMarkdownText, default } from './web/EnrichedMarkdownText'; + export type { EnrichedMarkdownTextProps } from './types/MarkdownTextProps.web'; + export type { MarkdownStyle, Md4cFlags } from './types/MarkdownStyle'; +diff --git a/node_modules/react-native-enriched-markdown/src/web/renderers/InlineRenderers.tsx b/node_modules/react-native-enriched-markdown/src/web/renderers/InlineRenderers.tsx +index 091eb19..bbee3ce 100644 +--- a/node_modules/react-native-enriched-markdown/src/web/renderers/InlineRenderers.tsx ++++ b/node_modules/react-native-enriched-markdown/src/web/renderers/InlineRenderers.tsx +@@ -48,9 +48,9 @@ function HighlightRenderer({ node, styles, renderChildren }: RendererProps) { + return {renderChildren(node)}; + } + +-function CodeRenderer({ node, styles, renderChildren }: RendererProps) { ++function CodeRenderer({ node, styles, parentType, renderChildren }: RendererProps) { + return ( +- {node.content ?? renderChildren(node)} ++ {node.content ?? renderChildren(node)} + ); + } + +@@ -58,16 +58,25 @@ function LinkRenderer({ + node, + style, + callbacks, ++ capabilities, + renderChildren, + }: RendererProps) { + const url = node.attributes?.url; + + if (!url) return <>{renderChildren(node)}; + ++ const isRecognizedLink = node.attributes?.recognizedLink === 'true'; ++ const containsInlineCode = node.children?.some((child) => child.type === 'Code'); ++ let sourceType: 'markdown' | 'text' | 'inline-code' = 'markdown'; ++ if (isRecognizedLink) { ++ sourceType = containsInlineCode ? 'inline-code' : 'text'; ++ } ++ const link = { url, sourceType }; ++ + const handleClick = (event: MouseEvent) => { + if (callbacks.onLinkPress) { + event.preventDefault(); +- callbacks.onLinkPress({ url }); ++ callbacks.onLinkPress(link); + } + }; + +@@ -78,18 +87,27 @@ function LinkRenderer({ + } + }; + +- return ( +- +- {renderChildren(node)} +- +- ); ++ const anchorProps = { ++ href: url, ++ title: node.attributes?.title, ++ style: linkStyleForUrl(style, url), ++ target: '_blank', ++ rel: 'noopener noreferrer', ++ onClick: handleClick, ++ onContextMenu: handleContextMenu, ++ }; ++ const CustomLink = capabilities.renderers?.link; ++ if (CustomLink) { ++ return ( ++ ++ {renderChildren(node)} ++ ++ ); ++ } ++ return {renderChildren(node)}; + } + + function LatexMathInlineRenderer({ +diff --git a/node_modules/react-native-enriched-markdown/lib/module/web/renderers/InlineRenderers.js b/node_modules/react-native-enriched-markdown/lib/module/web/renderers/InlineRenderers.js +index c8311d4..d1e698c 100644 +--- a/node_modules/react-native-enriched-markdown/lib/module/web/renderers/InlineRenderers.js ++++ b/node_modules/react-native-enriched-markdown/lib/module/web/renderers/InlineRenderers.js +@@ -92,10 +92,11 @@ function HighlightRenderer({ + function CodeRenderer({ + node, + styles, ++ parentType, + renderChildren + }) { + return /*#__PURE__*/_jsx("code", { +- style: styles.code, ++ style: parentType === 'Link' ? { ...styles.code, color: 'inherit' } : styles.code, + children: node.content ?? renderChildren(node) + }); + } +@@ -103,18 +104,24 @@ function LinkRenderer({ + node, + style, + callbacks, ++ capabilities, + renderChildren + }) { + const url = node.attributes?.url; + if (!url) return /*#__PURE__*/_jsx(_Fragment, { + children: renderChildren(node) + }); ++ const isRecognizedLink = node.attributes?.recognizedLink === 'true'; ++ const containsInlineCode = node.children?.some(child => child.type === 'Code'); ++ let sourceType = 'markdown'; ++ if (isRecognizedLink) { ++ sourceType = containsInlineCode ? 'inline-code' : 'text'; ++ } ++ const link = { url, sourceType }; + const handleClick = event => { + if (callbacks.onLinkPress) { + event.preventDefault(); +- callbacks.onLinkPress({ +- url +- }); ++ callbacks.onLinkPress(link); + } + }; + const handleContextMenu = event => { +@@ -125,15 +132,24 @@ function LinkRenderer({ + }); + } + }; +- return /*#__PURE__*/_jsx("a", { ++ const anchorProps = { + href: url, ++ title: node.attributes?.title, + style: linkStyleForUrl(style, url), +- target: "_blank", +- rel: "noopener noreferrer", ++ target: '_blank', ++ rel: 'noopener noreferrer', + onClick: handleClick, +- onContextMenu: handleContextMenu, +- children: renderChildren(node) +- }); ++ onContextMenu: handleContextMenu ++ }; ++ const CustomLink = capabilities.renderers?.link; ++ if (CustomLink) { ++ return /*#__PURE__*/_jsx(CustomLink, { ++ link, ++ anchorProps, ++ children: renderChildren(node) ++ }); ++ } ++ return /*#__PURE__*/_jsx("a", { ...anchorProps, children: renderChildren(node) }); + } + function LatexMathInlineRenderer({ + node, +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/CodeBlockContainerView.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/CodeBlockContainerView.kt +index cba0825..21387f0 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/CodeBlockContainerView.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/segments/CodeBlockContainerView.kt +@@ -21,7 +21,6 @@ import android.view.GestureDetector + import android.view.MotionEvent + import android.view.View + import android.widget.FrameLayout +-import android.widget.HorizontalScrollView + import android.widget.ImageView + import androidx.appcompat.widget.AppCompatImageButton + import androidx.appcompat.widget.AppCompatTextView +@@ -33,6 +32,7 @@ import com.swmansion.enriched.markdown.utils.common.CodeBlockHighlighter + import com.swmansion.enriched.markdown.utils.common.CodeBlockNode + import com.swmansion.enriched.markdown.utils.text.extensions.applyCodeBlockTextStyle + import com.swmansion.enriched.markdown.utils.text.span.SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE ++import com.swmansion.enriched.markdown.views.ClaimingHorizontalScrollView + import com.swmansion.enriched.markdown.views.ContextMenuPopup + import com.swmansion.enriched.markdown.views.CopyGlyph + import kotlin.math.ceil +@@ -102,7 +102,7 @@ class CodeBlockContainerView( + } + + private val scrollView = +- HorizontalScrollView(context).apply { ++ ClaimingHorizontalScrollView(context).apply { + isHorizontalScrollBarEnabled = true + overScrollMode = View.OVER_SCROLL_NEVER + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/ClaimingHorizontalScrollView.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/ClaimingHorizontalScrollView.kt +new file mode 100644 +index 0000000..d80ecbd +--- /dev/null ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/ClaimingHorizontalScrollView.kt +@@ -0,0 +1,41 @@ ++package com.swmansion.enriched.markdown.views ++ ++import android.content.Context ++import android.view.MotionEvent ++import android.view.ViewConfiguration ++import android.widget.HorizontalScrollView ++import kotlin.math.abs ++ ++/** Claims overflowing blocks before ancestor gesture handlers can activate. */ ++internal class ClaimingHorizontalScrollView(context: Context) : HorizontalScrollView(context) { ++ private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop ++ private var downX = 0f ++ private var downY = 0f ++ private var claimed = false ++ ++ override fun dispatchTouchEvent(event: MotionEvent): Boolean { ++ if (event.actionMasked == MotionEvent.ACTION_DOWN) { ++ downX = event.x ++ downY = event.y ++ claimed = canScrollHorizontally(-1) || canScrollHorizontally(1) ++ if (claimed) parent.requestDisallowInterceptTouchEvent(true) ++ } ++ ++ val handled = super.dispatchTouchEvent(event) ++ when (event.actionMasked) { ++ MotionEvent.ACTION_MOVE -> { ++ if (claimed) { ++ val dx = abs(event.x - downX) ++ val dy = abs(event.y - downY) ++ if (dy > dx && dy > touchSlop) { ++ // Release after native dispatch, which can itself disallow interception. ++ parent.requestDisallowInterceptTouchEvent(false) ++ claimed = false ++ } ++ } ++ } ++ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> claimed = false ++ } ++ return handled ++ } ++} +diff --git a/node_modules/react-native-enriched-markdown/ios/utils/ENRMHorizontalScrollView.h b/node_modules/react-native-enriched-markdown/ios/utils/ENRMHorizontalScrollView.h +index a0c77f6..644e801 100644 +--- a/node_modules/react-native-enriched-markdown/ios/utils/ENRMHorizontalScrollView.h ++++ b/node_modules/react-native-enriched-markdown/ios/utils/ENRMHorizontalScrollView.h +@@ -1,30 +1,7 @@ + #import "ENRMUIKit.h" + + /// Horizontal scroll view shared by code blocks, tables and display math. +-/// +-/// A rightward pan that starts while the view is settled at its leading edge is +-/// refused, so a full-width back gesture (react-native-screens +-/// `fullScreenGestureEnabled`, iOS 26 content pop) can begin instead. Inward +-/// pans, pans from inside the content, and pans during deceleration or bounce +-/// scroll as usual. Cost: no bounce on a fresh rightward pan at the leading +-/// edge. Left edge only; RTL is not handled. macOS keeps RCTUIScrollView. +-/// +-/// TODO: move this decision to react-native-screens. RNSScreenStack already +-/// inspects competing scroll-view pans (`shouldRequireFailureOfGestureRecognizer:` +-/// on iOS 26, `shouldRecognizeSimultaneouslyWithGestureRecognizer:` before); +-/// treating a scroll view settled at its leading edge as not competing would +-/// cover every horizontal scroll view, keep the bounce when nothing can pop, +-/// and handle RTL. Then this subclass is only needed without RNS. ++/// Keep the native scroll recognizer at the leading edge so enclosing sidebar ++/// gestures cannot take an outward drag that starts in an overflowing block. + @interface ENRMHorizontalScrollView : RCTUIScrollView + @end +- +-/// Decision behind `-gestureRecognizerShouldBegin:`, kept pure so it can be +-/// tested without a live scroll view. `leadingBoundary` is the content offset +-/// at the leading edge, i.e. `-adjustedContentInset.left`. +-static inline BOOL ENRMShouldYieldOutwardPan(CGFloat contentOffsetX, CGFloat leadingBoundary, BOOL decelerating, +- CGPoint velocity) +-{ +- BOOL outward = velocity.x > fabs(velocity.y); +- BOOL settledAtLeadingEdge = !decelerating && fabs(contentOffsetX - leadingBoundary) <= 0.5; +- return outward && settledAtLeadingEdge; +-} +diff --git a/node_modules/react-native-enriched-markdown/ios/utils/ENRMHorizontalScrollView.m b/node_modules/react-native-enriched-markdown/ios/utils/ENRMHorizontalScrollView.m +index b98d10c..b645629 100644 +--- a/node_modules/react-native-enriched-markdown/ios/utils/ENRMHorizontalScrollView.m ++++ b/node_modules/react-native-enriched-markdown/ios/utils/ENRMHorizontalScrollView.m +@@ -1,17 +1,4 @@ + #import "ENRMHorizontalScrollView.h" + + @implementation ENRMHorizontalScrollView +- +-#if !TARGET_OS_OSX +-- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer +-{ +- if (gestureRecognizer == self.panGestureRecognizer && +- ENRMShouldYieldOutwardPan(self.contentOffset.x, -self.adjustedContentInset.left, self.isDecelerating, +- [self.panGestureRecognizer velocityInView:self])) { +- return NO; +- } +- return [super gestureRecognizerShouldBegin:gestureRecognizer]; +-} +-#endif +- + @end +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/MeasurementSizeCache.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/MeasurementSizeCache.kt +new file mode 100644 +index 0000000..11911c8 +--- /dev/null ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/MeasurementSizeCache.kt +@@ -0,0 +1,31 @@ ++package com.swmansion.enriched.markdown ++ ++/** Size-only cache: no views, spans, Contexts or callbacks survive unmount. */ ++internal class MeasurementSizeCache(private val capacity: Int = 512) { ++ data class Key(val markdown: String, val propsHash: Int, val width: Float, val splitTableSegments: Boolean) ++ ++ private val sizes = LinkedHashMap(16, 0.75f, true) ++ ++ @Synchronized ++ fun get(key: Key): Long? = sizes[key] ++ ++ @Synchronized ++ fun put(key: Key, size: Long) { ++ sizes[key] = size ++ if (sizes.size > capacity) { ++ val oldest = sizes.entries.iterator() ++ oldest.next() ++ oldest.remove() ++ } ++ } ++ ++ @Synchronized ++ fun removeMatchingMarkdown(markdown: String) { ++ sizes.keys.removeAll { it.markdown == markdown } ++ } ++ ++ @Synchronized ++ fun clear() { ++ sizes.clear() ++ } ++} +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +index f4840e4..e36b444 100644 +--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +@@ -66,9 +66,11 @@ object MeasurementStore { + val spannable: CharSequence?, + val paintParams: PaintParams, + val markdownHash: Int, ++ val sizeKey: MeasurementSizeCache.Key? = null, + ) + + private val data = ConcurrentHashMap() ++ private val sizes = MeasurementSizeCache() + + // Store font scaling settings per view ID + private data class FontScalingSettings( +@@ -133,7 +135,10 @@ object MeasurementStore { + YogaMeasureOutput.getHeight(newSize) + PixelUtil.toDIPFromPixel(trailingMarginBottomPx), + ) + } +- data[id] = MeasurementParams(width, newSize, spannable, paintParams, existingHash) ++ if (oldSize != newSize) { ++ cached?.sizeKey?.let { sizes.removeMatchingMarkdown(it.markdown) } ++ } ++ data[id] = MeasurementParams(width, newSize, spannable, paintParams, existingHash, cached?.sizeKey) + return oldSize != newSize + } + +@@ -144,7 +149,7 @@ object MeasurementStore { + } + + fun invalidate(id: Int) { +- data.remove(id) ++ data.remove(id)?.sizeKey?.let { sizes.removeMatchingMarkdown(it.markdown) } + } + + // Content-box width (px) the measure pass last wrapped at; the display view insets to match (#805). +@@ -212,7 +217,7 @@ object MeasurementStore { + val previous = fontScalingSettings[viewId] + fontScalingSettings[viewId] = FontScalingSettings(allowFontScaling, maxFontSizeMultiplier) + if (previous != null && (previous.allowFontScaling != allowFontScaling || previous.maxFontSizeMultiplier != maxFontSizeMultiplier)) { +- data.remove(viewId) ++ invalidate(viewId) + } + } + +@@ -285,7 +290,43 @@ object MeasurementStore { + + val fontScale = checkAndUpdateFontScale(context, allowFontScaling, maxFontSizeMultiplier) + +- // Split measurement always goes through the full measure path (no spannable caching) ++ // Use resolved layout settings, including values supplied by native setters. ++ var layoutHash = computePropsHash(props, allowFontScaling, fontScale, maxFontSizeMultiplier) ++ layoutHash = 31 * layoutHash + resolveBreakStrategy(id) ++ layoutHash = 31 * layoutHash + resolveMaxLines(id) ++ layoutHash = 31 * layoutHash + (resolveEllipsize(id)?.hashCode() ?: 0) ++ layoutHash = 31 * layoutHash + props.getBooleanOrDefault("streamingAnimation", false).hashCode() ++ layoutHash = 31 * layoutHash + (id?.let { streamingTableModes[it] } ?: TableStreamingMode.PROGRESSIVE).hashCode() ++ layoutHash = 31 * layoutHash + (id?.let { streamingCodeBlockModes[it] } ?: CodeBlockStreamingMode.PROGRESSIVE).hashCode() ++ layoutHash = 31 * layoutHash + context.resources.displayMetrics.density.toBits() ++ val key = MeasurementSizeCache.Key(props.getStringOrDefault("markdown", ""), layoutHash, width, splitTableSegments) ++ val cached = id?.let { data[it] } ++ if (cached?.sizeKey == key) return cached.cachedSize ++ sizes.get(key)?.let { size -> ++ if (id != null) { ++ data[id] = MeasurementParams(width, size, null, PaintParams(Typeface.DEFAULT, 0f), 0, key) ++ } ++ return size ++ } ++ ++ val size = measureUncached(context, id, width, props, splitTableSegments, allowFontScaling, fontScale, maxFontSizeMultiplier) ++ sizes.put(key, size) ++ if (id != null) { ++ data.computeIfPresent(id) { _, measured -> measured.copy(sizeKey = key) } ++ } ++ return size ++ } ++ ++ private fun measureUncached( ++ context: Context, ++ id: Int?, ++ width: Float, ++ props: ReadableMap?, ++ splitTableSegments: Boolean, ++ allowFontScaling: Boolean, ++ fontScale: Float, ++ maxFontSizeMultiplier: Float, ++ ): Long { + if (splitTableSegments) { + return measureAndCacheSplit(context, id, width, props, allowFontScaling, fontScale, maxFontSizeMultiplier) + } +@@ -295,7 +336,7 @@ object MeasurementStore { + + val currentHash = computePropsHash(props, allowFontScaling, fontScale, maxFontSizeMultiplier) + +- if (cached.markdownHash != currentHash) { ++ if (cached.markdownHash != currentHash || cached.spannable == null) { + return measureAndCache(context, safeId, width, props, allowFontScaling, fontScale, maxFontSizeMultiplier) + } + +@@ -332,8 +373,11 @@ object MeasurementStore { + val imageRequestHeaders = parseImageRequestHeaders(props.getArrayOrNull("imageRequestHeaders")) + val linkPillContent = parseLinkPillContent(props.getArrayOrNull("linkPillContent")) + var result = markdown.hashCode() +- result = 31 * result + (styleMap?.hashCode() ?: 0) +- result = 31 * result + (md4cFlagsMap?.hashCode() ?: 0) ++ // ReadableNativeArray hashes its backing array identity, not its values. ++ result = 31 * result + (styleMap?.toHashMap()?.hashCode() ?: 0) ++ result = 31 * result + (md4cFlagsMap?.toHashMap()?.hashCode() ?: 0) ++ result = 31 * result + props.getBooleanOrDefault("isGFM", false).hashCode() ++ result = 31 * result + props.getStringOrDefault("textBreakStrategy", BreakStrategyUtils.DEFAULT_STRATEGY).hashCode() + result = 31 * result + parseLinkRecognition(props.getMapOrNull("linkRecognition")).hashCode() + result = 31 * result + fontScale.toBits() + result = 31 * result + allowFontScaling.hashCode() +@@ -356,6 +400,7 @@ object MeasurementStore { + if (lastKnownFontScale != 1.0f) { + lastKnownFontScale = 1.0f + data.clear() ++ sizes.clear() + } + return 1.0f + } +@@ -368,6 +413,7 @@ object MeasurementStore { + if (currentFontScale != lastKnownFontScale) { + lastKnownFontScale = currentFontScale + data.clear() ++ sizes.clear() + } + return currentFontScale + } +diff --git a/node_modules/react-native-enriched-markdown/android/src/test/java/com/swmansion/enriched/markdown/MeasurementSizeCacheTest.kt b/node_modules/react-native-enriched-markdown/android/src/test/java/com/swmansion/enriched/markdown/MeasurementSizeCacheTest.kt +new file mode 100644 +index 0000000..b3394c4 +--- /dev/null ++++ b/node_modules/react-native-enriched-markdown/android/src/test/java/com/swmansion/enriched/markdown/MeasurementSizeCacheTest.kt +@@ -0,0 +1,51 @@ ++package com.swmansion.enriched.markdown ++ ++import org.junit.Assert.* ++import org.junit.Test ++ ++class MeasurementSizeCacheTest { ++ private fun key(markdown: String = "hello", propsHash: Int = 1, width: Float = 320f) = ++ MeasurementSizeCache.Key(markdown, propsHash, width, true) ++ ++ @Test fun reusesSizeAcrossIndependentEqualKeys() { ++ val cache = MeasurementSizeCache(2) ++ cache.put(key(), 42L) ++ assertEquals(42L, cache.get(key())) ++ assertNull(cache.get(key(propsHash = 2))) ++ assertNull(cache.get(key(width = 321f))) ++ assertNull(cache.get(key().copy(splitTableSegments = false))) ++ } ++ ++ @Test fun comparesMarkdownEvenWhenItsHashCollides() { ++ val cache = MeasurementSizeCache(2) ++ assertEquals("Aa".hashCode(), "BB".hashCode()) ++ cache.put(key("Aa"), 42L) ++ assertNull(cache.get(key("BB"))) ++ } ++ ++ @Test fun evictsLeastRecentlyUsedSize() { ++ val cache = MeasurementSizeCache(2) ++ cache.put(key("one"), 1L) ++ cache.put(key("two"), 2L) ++ assertEquals(1L, cache.get(key("one"))) ++ cache.put(key("three"), 3L) ++ assertNull(cache.get(key("two"))) ++ assertEquals(1L, cache.get(key("one"))) ++ assertEquals(3L, cache.get(key("three"))) ++ } ++ ++ @Test fun invalidatesAllWidthsAndPropsForChangedContent() { ++ val cache = MeasurementSizeCache(4) ++ cache.put(key(), 1L) ++ cache.put(key(width = 640f), 2L) ++ cache.put(key(propsHash = 2), 3L) ++ cache.put(key("other"), 4L) ++ cache.removeMatchingMarkdown("hello") ++ assertNull(cache.get(key())) ++ assertNull(cache.get(key(width = 640f))) ++ assertNull(cache.get(key(propsHash = 2))) ++ assertEquals(4L, cache.get(key("other"))) ++ cache.clear() ++ assertNull(cache.get(key("other"))) ++ } ++} +diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/MarkdownRenderDispatch.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/MarkdownRenderDispatch.kt +new file mode 100644 +--- /dev/null ++++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/MarkdownRenderDispatch.kt +@@ -0,0 +1,9 @@ ++package com.swmansion.enriched.markdown ++ ++import java.util.concurrent.Executor ++ ++// Keep populated views in step with Fabric geometry when streaming blocks move or collapse. ++// First mounts retain worker preparation so large history rows do not parse on the UI thread. ++internal fun dispatchMarkdownRender(hasContent: Boolean, executor: Executor, render: () -> Unit) { ++ if (hasContent) render() else executor.execute(render) ++} +diff --git a/node_modules/react-native-enriched-markdown/android/src/test/java/com/swmansion/enriched/markdown/MarkdownRenderDispatchTest.kt b/node_modules/react-native-enriched-markdown/android/src/test/java/com/swmansion/enriched/markdown/MarkdownRenderDispatchTest.kt +new file mode 100644 +--- /dev/null ++++ b/node_modules/react-native-enriched-markdown/android/src/test/java/com/swmansion/enriched/markdown/MarkdownRenderDispatchTest.kt +@@ -0,0 +1,46 @@ ++package com.swmansion.enriched.markdown ++ ++import java.util.concurrent.CountDownLatch ++import java.util.concurrent.Executors ++import java.util.concurrent.TimeUnit ++import org.junit.Assert.* ++import org.junit.Test ++ ++class MarkdownRenderDispatchTest { ++ @Test fun populatedViewUpdatesBeforeReturningEvenWithBusyWorker() { ++ val executor = Executors.newSingleThreadExecutor() ++ val release = CountDownLatch(1) ++ val started = CountDownLatch(1) ++ try { ++ executor.execute { started.countDown(); release.await() } ++ assertTrue(started.await(5, TimeUnit.SECONDS)) ++ val caller = Thread.currentThread() ++ var rendered = false ++ dispatchMarkdownRender(true, executor) { ++ assertSame(caller, Thread.currentThread()) ++ rendered = true ++ } ++ assertTrue(rendered) ++ } finally { ++ release.countDown() ++ executor.shutdownNow() ++ } ++ } ++ ++ @Test fun firstMountPreparesOnWorker() { ++ val executor = Executors.newSingleThreadExecutor() ++ val finished = CountDownLatch(1) ++ val caller = Thread.currentThread() ++ var worker: Thread? = null ++ try { ++ dispatchMarkdownRender(false, executor) { ++ worker = Thread.currentThread() ++ finished.countDown() ++ } ++ assertTrue(finished.await(5, TimeUnit.SECONDS)) ++ assertNotSame(caller, worker) ++ } finally { ++ executor.shutdownNow() ++ } ++ } ++} diff --git a/patches/react-native-markdown-display+7.0.2.patch b/patches/react-native-markdown-display+7.0.2.patch deleted file mode 100644 index ad8ac0bf66..0000000000 --- a/patches/react-native-markdown-display+7.0.2.patch +++ /dev/null @@ -1,74 +0,0 @@ -diff --git a/node_modules/react-native-markdown-display/src/lib/AstRenderer.js b/node_modules/react-native-markdown-display/src/lib/AstRenderer.js -index 608e945..edafe12 100644 ---- a/node_modules/react-native-markdown-display/src/lib/AstRenderer.js -+++ b/node_modules/react-native-markdown-display/src/lib/AstRenderer.js -@@ -1,6 +1,5 @@ - import {StyleSheet} from 'react-native'; - --import getUniqueID from './util/getUniqueID'; - import convertAdditionalStyles from './util/convertAdditionalStyles'; - - import textStyleProps from './data/textStyleProps'; -@@ -180,7 +179,7 @@ export default class AstRenderer { - * @return {*} - */ - render = (nodes) => { -- const root = {type: 'body', key: getUniqueID(), children: nodes}; -+ const root = {type: 'body', key: 'rnmr_root', children: nodes}; - return this.renderNode(root, [], true); - }; - } -diff --git a/node_modules/react-native-markdown-display/src/lib/util/tokensToAST.js b/node_modules/react-native-markdown-display/src/lib/util/tokensToAST.js -index b0ed265..68e2150 100644 ---- a/node_modules/react-native-markdown-display/src/lib/util/tokensToAST.js -+++ b/node_modules/react-native-markdown-display/src/lib/util/tokensToAST.js -@@ -1,4 +1,3 @@ --import getUniqueID from './getUniqueID'; - import getTokenTypeByToken from './getTokenTypeByToken'; - - /** -@@ -7,9 +6,10 @@ import getTokenTypeByToken from './getTokenTypeByToken'; - * @param {number} tokenIndex - * @return {{type: string, content, tokenIndex: *, index: number, attributes: {}, children: *}} - */ --function createNode(token, tokenIndex) { -+function createNode(token, tokenIndex, parentKey) { - const type = getTokenTypeByToken(token); - const content = token.content; -+ const keyPath = parentKey ? `${parentKey}.${tokenIndex}` : `${tokenIndex}`; - - let attributes = {}; - -@@ -27,12 +27,12 @@ function createNode(token, tokenIndex) { - sourceMeta: token.meta, - block: token.block, - markup: token.markup, -- key: getUniqueID() + '_' + type, -+ key: `rnmr_${keyPath}_${type}`, - content, - tokenIndex, - index: 0, - attributes, -- children: tokensToAST(token.children), -+ children: tokensToAST(token.children, keyPath), - }; - } - -@@ -41,7 +41,7 @@ function createNode(token, tokenIndex) { - * @param {Array<{type: string, tag:string, content: string, children: *, attrs: Array}>}tokens - * @return {Array} - */ --export default function tokensToAST(tokens) { -+export default function tokensToAST(tokens, parentKey = '') { - let stack = []; - let children = []; - -@@ -51,7 +51,7 @@ export default function tokensToAST(tokens) { - - for (let i = 0; i < tokens.length; i++) { - const token = tokens[i]; -- const astNode = createNode(token, i); -+ const astNode = createNode(token, i, parentKey); - - if ( - !( diff --git a/plugins/cursor-usage-source/index.server.ts b/plugins/cursor-usage-source/index.server.ts index 5b19939833..a357b3d1bb 100644 --- a/plugins/cursor-usage-source/index.server.ts +++ b/plugins/cursor-usage-source/index.server.ts @@ -1,6 +1,6 @@ import type { PluginServerContext } from "@getpaseo/plugin/server"; import { inputSchema } from "./shared/input.js"; -import { fetchUsage, discover } from "./server/usage.js"; +import { fetchUsage, discover, discoverSession } from "./server/usage.js"; export default function contribute(server: PluginServerContext) { server.registerUsageSource({ @@ -8,7 +8,7 @@ export default function contribute(server: PluginServerContext) { label: "Cursor", icon: "icon.svg", input: inputSchema, - discover: (scope) => (scope.kind === "global" ? discover() : Promise.resolve([])), + discover: (scope) => (scope.kind === "global" ? discover() : discoverSession(scope)), fetch: fetchUsage, }); return () => {}; diff --git a/plugins/cursor-usage-source/server/usage.test.ts b/plugins/cursor-usage-source/server/usage.test.ts index 21cca3b192..91b657c264 100644 --- a/plugins/cursor-usage-source/server/usage.test.ts +++ b/plugins/cursor-usage-source/server/usage.test.ts @@ -3,7 +3,7 @@ import { createRequire } from "node:module"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { discover, fetchUsage } from "./usage.js"; +import { discover, discoverSession, fetchUsage } from "./usage.js"; import type { UsageReport } from "@getpaseo/plugin/server/usage"; // node:sqlite has no @types/node@20 typings; require it with a narrow local type. @@ -256,6 +256,78 @@ describe("cursor usage source", () => { writeFileSync(join(directory, "state.vscdb"), "invalid database"); expect(await discover()).toEqual([]); }); + + describe("session discovery", () => { + let sessionHome: string; + beforeEach(() => { + sessionHome = mkdtempSync(join(tmpdir(), "usage-session-home-")); + }); + afterEach(() => { + rmSync(sessionHome, { recursive: true, force: true }); + }); + function cursorSession(env: Record, provider = "cursor") { + return { kind: "session" as const, provider, env: { HOME: sessionHome, ...env } }; + } + function writeAuthJson(path: string): void { + mkdirSync(join(path, ".."), { recursive: true }); + writeFileSync(path, JSON.stringify({ accessToken: "session-token" })); + } + + it("reads the login cursor-agent uses under the session's XDG_CONFIG_HOME", async () => { + const configHome = join(sessionHome, "xdg"); + const authPath = join(configHome, "cursor", "auth.json"); + writeAuthJson(authPath); + expect( + await discoverSession(cursorSession({ XDG_CONFIG_HOME: configHome }), "linux"), + ).toEqual([{ key: "default", input: { store: "file", locator: authPath } }]); + }); + + it("reads the login under the session's HOME, not the daemon's", async () => { + writeCursorAuthJson(homeDir, "daemon-token"); + writeCursorAuthJson(sessionHome, "session-token"); + expect(await discoverSession(cursorSession({}), "linux")).toEqual([ + { + key: "default", + input: { store: "file", locator: join(sessionHome, ".config", "cursor", "auth.json") }, + }, + ]); + }); + + it("reads %APPDATA%/Cursor/auth.json on Windows", async () => { + const appData = join(sessionHome, "AppData", "Roaming"); + const authPath = join(appData, "Cursor", "auth.json"); + writeAuthJson(authPath); + expect(await discoverSession(cursorSession({ APPDATA: appData }), "win32")).toEqual([ + { key: "default", input: { store: "file", locator: authPath } }, + ]); + }); + + it("reads ~/.cursor/auth.json on macOS only when cursor-agent uses the file store", async () => { + const authPath = join(sessionHome, ".cursor", "auth.json"); + writeAuthJson(authPath); + expect(await discoverSession(cursorSession({}), "darwin")).toEqual([]); + expect( + await discoverSession(cursorSession({ AGENT_CLI_CREDENTIAL_STORE: "file" }), "darwin"), + ).toEqual([{ key: "default", input: { store: "file", locator: authPath } }]); + }); + + it.each([ + ["another provider", cursorSession({}, "claude")], + ["a CURSOR_API_KEY session", cursorSession({ CURSOR_API_KEY: "key" })], + ["a CURSOR_AUTH_TOKEN session", cursorSession({ CURSOR_AUTH_TOKEN: "token" })], + ["an in-memory credential store", cursorSession({ AGENT_CLI_CREDENTIAL_STORE: "memory" })], + ])("returns no login for %s", async (_scenario, scope) => { + writeCursorAuthJson(sessionHome, "session-token"); + expect(await discoverSession(scope, "linux")).toEqual([]); + }); + + it("never falls back to the daemon's default stores", async () => { + writeCursorStateDb(homeDir, { "cursorAuth/accessToken": "desktop-token" }); + writeCursorAuthJson(homeDir, "daemon-token"); + process.env["CURSOR_ACCESS_TOKEN"] = "daemon-env-token"; + expect(await discoverSession(cursorSession({}), "linux")).toEqual([]); + }); + }); }); it("discovery returns a locator when fetch finds cursor credentials", async () => { diff --git a/plugins/cursor-usage-source/server/usage.ts b/plugins/cursor-usage-source/server/usage.ts index d313b1d739..c5d2ab1391 100644 --- a/plugins/cursor-usage-source/server/usage.ts +++ b/plugins/cursor-usage-source/server/usage.ts @@ -11,6 +11,7 @@ import { type UsageAccount, type UsageReport, type UsageBalance, + type UsageScope, } from "@getpaseo/plugin/server/usage"; const ApiNumberSchema = z.coerce.number().finite(); @@ -209,6 +210,7 @@ async function readToken(input: UsageInput): Promise { : await readCursorTokenFromAuthJson(input.locator); return token ?? undefined; } + export async function discover(): Promise { const home = homedir(); const candidates: UsageInput[] = ["CURSOR_ACCESS_TOKEN", "CURSOR_TOKEN"].map((locator) => ({ @@ -238,3 +240,30 @@ export async function discover(): Promise { for (const input of candidates) if (await readToken(input)) return [{ key: "default", input }]; return []; } + +// Resolves the login a cursor-agent session runs on. The key matches global discovery so +// the session's login joins the host's Cursor card instead of adding a second one. +export async function discoverSession( + scope: Extract, + platform: NodeJS.Platform = process.platform, +): Promise { + if (scope.provider !== "cursor") return []; + const locator = cursorAgentAuthPath(scope.env, platform); + if (!locator) return []; + const input: UsageInput = { store: "file", locator }; + return (await readToken(input)) ? [{ key: "default", input }] : []; +} + +// Mirrors cursor-agent's credential store. Env credentials win over the stored login and +// can't be named by an input. AGENT_CLI_CREDENTIAL_STORE picks "file" or "memory"; by +// default macOS keeps the login in the Keychain and other platforms in auth.json. +function cursorAgentAuthPath(env: Record, platform: NodeJS.Platform) { + if (env.CURSOR_API_KEY || env.CURSOR_AUTH_TOKEN) return null; + const store = env.AGENT_CLI_CREDENTIAL_STORE; + if (store === "memory" || (platform === "darwin" && store !== "file")) return null; + const home = env.HOME || env.USERPROFILE || homedir(); + if (platform === "win32") + return join(env.APPDATA || join(home, "AppData", "Roaming"), "Cursor", "auth.json"); + if (platform === "darwin") return join(home, ".cursor", "auth.json"); + return join(env.XDG_CONFIG_HOME || join(home, ".config"), "cursor", "auth.json"); +} diff --git a/plugins/package.json b/plugins/package.json index 4bd6ca429b..8f37f073c8 100644 --- a/plugins/package.json +++ b/plugins/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/builtin-plugins", - "version": "0.11.2", + "version": "0.12.0-beta.1", "private": true, "type": "module", "scripts": { diff --git a/public-docs/plugins/reference.md b/public-docs/plugins/reference.md index 409bb3f5a6..525ffe1423 100644 --- a/public-docs/plugins/reference.md +++ b/public-docs/plugins/reference.md @@ -411,11 +411,14 @@ Built-in session routes: | Claude | `pi`, `omp` with `anthropic/…` model | That harness's Anthropic login store | | Codex | `codex` | `CODEX_HOME/auth.json`, or the default | | Codex | `pi`, `opencode`, `omp` with `openai/…` model | That harness's OpenAI login store | +| Cursor | `cursor` | cursor-agent's `auth.json` for that session's `XDG_CONFIG_HOME`, `APPDATA`, or home directory | | Muse | `muse` | That session's own Muse launch/config | | Other built-in sources | Any | No session discovery | Claude excludes Bedrock, Vertex, and foreign `ANTHROPIC_BASE_URL` sessions. Codex excludes sessions -with `OPENAI_BASE_URL` set. +with `OPENAI_BASE_URL` set. Cursor excludes sessions that authenticate with `CURSOR_API_KEY` or +`CURSOR_AUTH_TOKEN`, and macOS sessions that keep the login in the Keychain, the default unless +`AGENT_CLI_CREDENTIAL_STORE=file`. Use a stable account key: 1–128 characters from `[A-Za-z0-9._-]`. It identifies the account or organization whose quota is metered and survives token rotation. Never use a credential or raw diff --git a/scripts/ci-workflow.test.mjs b/scripts/ci-workflow.test.mjs index 8ea5411026..d71fa6e184 100644 --- a/scripts/ci-workflow.test.mjs +++ b/scripts/ci-workflow.test.mjs @@ -37,7 +37,7 @@ function jobBlocks(source) { let currentJob; for (const line of source.split("\n")) { - const jobMatch = /^ ([a-z0-9-]+):\s*$/.exec(line); + const jobMatch = /^ ([a-z0-9_-]+):\s*$/.exec(line); if (jobMatch) { currentJob = jobMatch[1]; jobs.set(currentJob, []); @@ -303,3 +303,31 @@ test("desktop packaging smokes main pushes and only the pull requests that touch assert.match(source, new RegExp(`${action}@[0-9a-f]{40} # v\\d+\\.\\d+\\.\\d+`)); } }); + +test("stable iOS releases upload once and distribute to external testers before App Store review", () => { + const source = readFileSync( + new URL("packages/app/.eas/workflows/release-mobile.yml", repoRoot), + "utf8", + ); + const jobs = jobBlocks(source); + const uploads = [...jobs.entries()].filter( + ([, lines]) => + lines.some((line) => /^ type: (submit|testflight)$/.test(line)) && + lines.some((line) => line.includes("needs.build_ios.outputs.build_id")), + ); + assert.equal(uploads.length, 1, "the iOS binary must be uploaded exactly once"); + const [distributionId, distributionLines] = uploads[0]; + const distribution = distributionLines.join("\n"); + assert.match(distribution, /^ type: testflight$/m); + assert.match(distribution, /external_groups:\s*\n\s+- Paseo Beta/); + assert.match(distribution, /^ submit_beta_review: true$/m); + + const review = jobs.get("submit_ios_for_review").join("\n"); + assert.match(review, new RegExp(`needs: \\[build_ios, ${distributionId}\\]`)); + assert.match(review, /APP_VERSION="\$\{\{ needs\.build_ios\.outputs\.app_version \}\}"/); + assert.match( + review, + /APP_BUILD_VERSION="\$\{\{ needs\.build_ios\.outputs\.app_build_version \}\}"/, + ); + assert.match(review, /bundle exec fastlane ios submit_review/); +}); diff --git a/scripts/postinstall-patches.mjs b/scripts/postinstall-patches.mjs index 3eeafc6371..a571614c5a 100644 --- a/scripts/postinstall-patches.mjs +++ b/scripts/postinstall-patches.mjs @@ -9,8 +9,9 @@ import { join, relative } from "node:path"; // the patch's node_modules/... paths relative to its working directory. const patchedPackages = [ { - nodeModulesPath: "node_modules/react-native-markdown-display", - patchPrefix: "react-native-markdown-display+", + nodeModulesPath: "packages/app/node_modules/react-native-enriched-markdown", + patchPrefix: "react-native-enriched-markdown+", + cwd: "packages/app", }, // Remove after react-native-unistyles ships // https://github.com/jpudysz/react-native-unistyles/pull/1203. diff --git a/skills/paseo/SKILL.md b/skills/paseo/SKILL.md index 11b5f2ff97..b7fe95811d 100644 --- a/skills/paseo/SKILL.md +++ b/skills/paseo/SKILL.md @@ -21,7 +21,7 @@ For a local daemon, `project create` defaults to the current directory and resol ## Workspaces -**`create_workspace`** — create a workspace independently of any agent. Required: `isolation` (`local` or `worktree`). Worktree isolation supports `mode: "branch-off" | "checkout-branch" | "checkout-pr"`: use `branchName`/`baseBranch` for a new branch, `branch` for an existing branch, or `prNumber` plus optional `forge`/`projectPath` for a change request. `worktreeSlug` controls the managed path. Returns the workspace descriptor centered on `workspaceId`. +**`create_workspace`** — create a workspace independently of any agent. Required: `isolation` (`local` or `worktree`). Worktree isolation supports `mode: "branch-off" | "checkout-branch" | "checkout-pr"`: use `branchName`/`baseBranch` for a new branch, `branch` for an existing branch, or `prNumber` plus optional `forge` for a change request. Use `path` for a local directory or source checkout and `projectId` for the owning project; when no path is supplied, a worktree may resolve its source from `projectId`. `worktreeSlug` controls the managed path. Returns the workspace descriptor centered on `workspaceId`. `background: true` hides a workspace from default lists and the sidebar while its contents remain durable and accessible; omit `background` to inherit your current workspace’s value (false without a caller), and override it only when the user explicitly asks.