Repository navigation
Conversation
|
🤖 Review round 1: Princess Donut, reviewing exact head Verdict: 8/10 with one blocker. Scope is clean and the slice works on top of #2762. Scope matches the slice. Compared with the combined #2761 head Works on top of #2762 alone. The guide section, author type test and installed-document journey use only the editor plus #2762's Markdown and workspace surfaces. The shared composer changes don't alter existing behavior: the formatting tools only gain an optional always-open mode, and the link dialog keeps its "message" wording by default. Verified:
Blocker1. The editor keeps showing edits the plugin didn't accept, so what the user sees and what gets saved can differ. (Severity: blocker, controlled-contract and data-loss defect)
How it fails: a plugin enforcing its byte limit by ignoring over-limit changes, a normal pattern for controlled inputs, shows text it will never save. The user presses Cmd/Ctrl+Enter and the older Fix:
Optional
|
|
🤖 Fixed the controlled-editor blocker at Declined rich edits restore the accepted Validation: both declined-change tests fail against The split proof remains |
|
🤖 Review round 2: Princess Donut, scoped to the round 1 blocker and the fix commit Verdict: 7/10. The original blocker is fixed, but the rollback adds a new blocker and a medium defect. All results below come from temporary component tests run at this head. The probe files are deleted and the worktree is clean. Round 1 blocker: fixed
Blocker: after a rollback the caret jumps to the end, so the next keystroke edits the wrong place(Severity: blocker, correctness and silent unintended edit) Reproduced: document "Intro paragraph.\n\nClosing line." with the caret after "Intro". The plugin declines a change that would make the document longer, standing in for a byte limit. Typing "Z" is correctly declined. Pressing Backspace then removes the final period of the document, and the plugin accepts that because it is shorter. The saved value becomes "Intro paragraph.\n\nClosing line". In a separate probe, typing after a declined character appended text at the end ("Hello! world?") instead of at the caret. Why it matters: this happens in exactly the byte-limit situation the fix exists for. A user at the limit who backspaces to make room deletes text at the end of the document, possibly off-screen, without noticing. Fix: when rolling back, keep the selection from before the declined change, clamped to the restored text, and keep focus. Add a test for decline, then Backspace or typing, that asserts the result is next to the original caret. Medium: a declined edit can trigger an unrequested second change with the whole document reformatted(Severity: medium; Ned specifically asked about second callbacks) Impact: the plugin has no way to tell this from a user edit. A normal plugin accepts it, so the document is marked dirty and its next save reformats syntax the user never touched. The new test asserts a single callback only for "Hello world", which round-trips exactly, so it can't catch this. Fix: suppress the change that echoes the restored content itself, for example by recording the restored draft's serialization. Don't compare against the raw Out of scope as agreed: the two round 1 nits. |
|
🤖 Addressed both rollback defects in Declined edits restore the host editor’s last accepted checkpoint, including selection and undo state. Restoration avoids an edit transaction, and programmatic synchronization does not notify the plugin, so equivalent Markdown spellings cannot cause a second normalized change. Five new cases fail against the prior adapter and pass with the repair. All 43 focused checks pass. The full temporary installed-module editor file passes in Chromium and WebKit (4 checks), including decline→Backspace at the original caret, exact saved content, and single callbacks/raw source for The new recombined tree |
|
🤖 Review round 3: Princess Donut, scoped to the round 2 findings and the fix commit Verdict: 9/10, no blockers. Both round 2 findings are fixed, and I found no new defects. I checked with temporary component tests at this head; the probe files are deleted and the worktree is clean. Round 2 blocker (caret after rollback): fixedThe editor now restores its last accepted state using the engine's existing checkpoint, which covers the document, selection and undo history. The caret no longer moves.
Round 2 medium (second change callback): fixedRestoring no longer goes through an edit transaction, and echoes during the sync are suppressed.
Do the new tests catch regressions? YesWith only the adapter reverted to Defects introduced: none found
Optional
|
bd003ea to
2df7cd4
Compare
8027f49 to
c8129c6
Compare
|
🤖 Conflict-only rebase onto #2762 at |
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 Two findings: rich edits can change persisted Markdown paragraph semantics, and the unconditional bracket shortcut prevents literal bracket entry.
c8129c6 to
03617c5
Compare
|
🤖 Addressed both review findings in
The paragraph/break probes failed before the fix, and both literal-bracket cases failed before the destination guard. Adapter/composer files pass 82 checks; the editor component file passes 21. The full installed-plugin browser file passes 34 Chromium/WebKit checks, including native Enter/Shift+Enter and reader output. An additional temporary installed-module probe passes in both engines for literal brackets and rich/read structure. No permanent browser cases were added or removed; the existing document journey gained assertions. Browser checks and these captures were made at Built app with an installed example, disposable data and modeled relay/native inventory. Wide-viewport captures show the workspace pane; narrow uses enlarged text. Native packaged/live-relay and human acceptance remain pending. New-head CI and review remain pending.
|
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 Three document serialization defects remain: hard breaks before inline code, clipboard structure loss, and Shift+Enter after a typed backslash.
|
🤖 @kalvinnchau Fixed all three document serialization issues at
Validation: four inline-code regressions, three slash regressions and seven of eight clipboard structure cases fail before their fixes. The four complete affected unit files pass 114 checks. The final-head installed-plugin file passes all 34 Chromium/WebKit checks, including page-local clipboard events, reader structure, inline paste formatting and native typed Shift+Enter. The current-head capture journey passes in both engines (2 checks). Mandatory hooks pass. No permanent browser cases were added or removed; the existing installed-document journey gained assertions and component/conversion tests own the matrix. The browser clipboard checks use page-local ClipboardEvent/DataTransfer; the operating-system clipboard, packaged native build, live relay and human acceptance remain untested. New-head CI/review remain pending. Built app at
|
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 One document clipboard regression remains: unsupported multiline source is corrupted by both paste flavors.
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 Partial selections still corrupt raw task-list/table source in both clipboard flavors; full-selection round trips are repaired.
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 One new paste regression remains in paragraphs containing a hard break; the prior partial raw-source selection defect is repaired.
… an update (#3024) ## TL;DR A `buzz://plugin-install` Git link pinned to both a `commit` and a `path`, for a plugin already installed from that same repository and folder, now opens the **Update** review that a plugin's Git `requestInstall` already gets. If the link names another branch or tag than the plugin tracks, the review says so. Before, it hit the Add path's **Already installed** refusal, so there was no reviewed way to update from such a link. Links without both pins, and links to a plugin installed from another source, are still refused as duplicates. ## What changed - When the installed plugin was imported (not bundled), `PluginImport` treats a link with both `commit` and `path` like a bound `requestInstall` request when deciding between update and duplicate (`src/app/PluginImport.tsx`). It reuses the existing same-source check: the installed `updateSource` must be a Git source whose repository matches the fetched preview's source and whose folder matches the candidate's folder, and the candidate must have the installed manifest ID. Native import saves the requested `ref`, and later update checks follow it. So when a link's or plugin request's `ref` differs from the installed Git source's ref, the review adds "Future updates will follow <new> instead of <old>." An empty ref reads as "the repository default". - Confirming a requested update (link or plugin request) re-reads the live catalog and refuses with "The installed plugin changed. Close this review and check again." if the installed revision, enabled state or saved source changed after the review opened, so the review's "stays disabled" line can't go stale. - A requested update of a disabled plugin now says "This plugin will stay disabled.", as the Details **Update** review already does. Native install already keeps the enabled state, and the review offers no enable switch for updates. - Docs: `docs/plugin-architecture.md`, `docs/deep-links.md` and `docs/buzz-help.md` describe the pinned-link update. Protections that still apply: the commit pin (native refuses a ref whose tip differs, and the review re-checks the fetched commit), the folder filter, the access review against the installed manifest, the publisher line and native publisher-continuity refusal, the unsigned-to-signed rollback warning, explicit **Update** confirmation, and preview discard on cancel. Links with only `commit`, only `path`, or neither, and pinned links to a bundled plugin, keep showing **Already installed** / **Open in Buzz**. Not in scope: update discovery, Update-all, managed policy feeds or checkers (#2504, #2621, #637), signature or publisher-trust policy (#2212, #2746), and the compact request dialog (#2625). This PR doesn't touch their hunks. #2983 and #2762 also edit `tests/browser/plugin-import.spec.mjs`; this PR leaves that file unchanged. ## Supported operation On a Buzz desktop build that includes this change, open `buzz://plugin-install?repository=<repo>&ref=<ref>&path=<folder>&commit=<40-or-64-hex>` for a plugin whose installed copy came from a Git import of that same repository and folder. The review shows **Update <name>**, with access changes listed against the installed version, and states any change to the branch or tag later updates follow. Nothing changes until you choose **Update**. Native import still refuses the update if the ref's tip isn't the pinned commit, or if the publisher changed or a signed plugin became unsigned. ## Tests New `src/app/PluginImport.test.tsx` cases, all with fictional `example.com` fixtures: - A pinned link to the installed source reviews as **Update**, installs the pinned folder, and doesn't call enable. This is the regression test for the original failure. - A disabled plugin stays disabled and the review says so. - A pinned link with another ref than the installed plugin tracks (a named ref, or the repository default) reviews as **Update**, says which ref future updates will follow, and installs on **Update**. A same-ref link shows no such line. - A link without both pins (none, path only, commit only), or a pinned link to a bundled plugin, still shows **Already installed** with **Open in Buzz**. - An installed source in another repository or folder, a local folder, or an unknown source is refused with the source-mismatch message and **Open in Buzz**. - When only another plugin ID is installed from that source, the link offers a new install. - A fetched commit that differs from the pin offers no **Update**. - Cancel discards the preview without installing. - If the installed revision, enabled state or source changes after the review opens, **Update** is refused, for both a link and a plugin's `requestInstall`. The request and link harnesses now return a ready catalog from `plugins.snapshot()`, which the new confirm-time check reads. Checked on a Linux box at `37cf73e1`: `biome check` on both files, `pnpm typecheck`, and `vitest run src/app/PluginImport.test.tsx` (100 passed). After the first review fixes, the same three checks passed locally (102 tests), and the bundled-plugin test fails on the earlier predicate. The later enabled-state drift case (two lines plus one test input) passed biome locally; host load refused a local test run, so PR CI covers it. With only the `requested` line reverted, 6 of the 8 `pinned link` tests fail. Against `main`'s `PluginImport.tsx`, the same 6 fail and the other 94 pass. The two that pass either way are the existing pin-retry test and the other-plugin-ID new-install case, which should behave the same before and after. After review feedback, the tracked-ref disclosure was verified on a Blox box at `e913727f`: `biome check`, `pnpm typecheck` and `vitest run src/app/PluginImport.test.tsx` (103 passed). With the review line disabled, the new tracked-ref test fails. The before/after screenshots predate this line and show a same-ref update. ### Before / after These screenshots come from a temporary Chromium journey using the existing `nativeImports` IPC fixture and fictional `example` data; the journey is not committed. It installs `example.linked` from `https://github.com/example/plugins` at `plugins/linked`, then delivers the same pinned link again with a newer candidate. | | Light | Dark | |---|---|---| | Before (`main`) |  |  | | After |  |  | ## Remaining gaps - No native (Tauri) run: the Playwright screenshots use the existing IPC fixture. Native import, publisher continuity and the deep-link handoff were not changed here. - No human has tried it in the app yet, so `buzz-review-completed` is not set. ## Initiative context Status: https://g2.sqprod.co/apps/kylez-buzz-improvements-status (buzz-improvements T027). Fixes NEXUS-4327 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Kyle Zemek <kylez@squareup.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
d5e4786 to
7082669
Compare
ed63a40 to
416b9ba
Compare
…ments (#3322) ## TL;DR Plugins can now register a headless composer annotator with `ctx.conversation.registerAnnotator`. The host gives it the focused draft, validates the ranges it publishes, draws the marks, and replaces text only when the person picks one of the offered replacements, as one undo step. Checking, language, scheduling, timeouts and wording all stay in the plugin, so a spelling, grammar or style checker can ship as a standalone plugin. ## API ```ts ctx.conversation.registerAnnotator({ id, title, component }); // component: ComponentType<CompletionContext & { // observation: { revision, text, prose: { start, end }[] }; // frozen, one per document // publish(annotations: ComposerAnnotation[]): (() => void) | false; // }> // ComposerAnnotation = { start, end, text, label, kind?: "spelling" | "grammar", replacements?: { text, label? }[] } ``` The types are exported from `@buzz/author`. The full contract is in `docs/plugin-architecture.md` under "Composer annotators". **Who owns what.** | Host | Plugin | |---|---| | Mounting per focused composer, destination and enablement; revoking `publish` on change, removal or render failure | What to check, language, ignore rules, tokenization | | Draft projection, prose ranges and UTF-16 mapping | Debounce, timeouts, retries, failure policy | | Validating revision, expected text, prose containment, grapheme boundaries, labels and replacements; caps | Results, labels, replacement choice and ranking | | Underline, `aria-invalid`, right-click / ⌘. menu, focus return, one-step Undo, IME and read-only refusal | — | **Disclosure.** An annotator sees what completion providers already see, under the same eligibility (focused, enabled, outbox-capable composer), plus the prose ranges. It never sees the caret, and no observation is offered while unfocused, disabled, read-only or composing. **Publishing.** `publish` is bound to one observation. It returns `false` once the draft changed, the destination or enablement changed, the annotator was removed or failed to render, or the value is not an array. Otherwise it replaces that annotator's marks and returns a disposer that withdraws only them. A disposer for an earlier draft leaves the mapped underlines in place until the next publication, so per-observation cleanup does not flicker. The menu closes when its annotation is withdrawn or replaced, even for the same draft. **Validation and bounds.** - An annotation must match its text exactly, lie inside one prose range, not split a surrogate pair or grapheme cluster, and carry a 1–120 unit label. - Replacements: at most 5, distinct, at most 64 units, different from the marked text. An empty replacement deletes the range and needs a label. - No control, bidi or line-separator characters and no lone surrogates. - At most 200 annotations per publication and 400 marks per editor; earlier annotators win overlaps; at most one redraw per animation frame. **Applying a replacement.** The host rechecks revision, range, text, editability and composition; refuses mixed formatting; keeps the range's formatting; and closes history on both sides so typing, the replacement and further typing undo as three steps. ## Review notes - Adds an extension point to three `FOUNDATION` files: `conversation/contracts.ts`, `conversation/service.tsx` and `plugins/author.ts`. The change is additive and needs FOUNDATION review. - The native spellcheck default and autocorrection preference are unchanged. - Related open work this does not overlap: native spellcheck default (#2201, #2546), a generic draft read/replace API (#2656), other composer work (#2983, #3312). Other open PRs touching the same composer files: #2988, #3260, #3257, #2896. ## Tests - `composer-annotations.test.ts` (Node): prose-range exclusions, same-text revisions, range/grapheme/label/replacement validation and caps, overlap and merge, mark-preserving replacement and deletion with Undo/Redo, stale and mixed-format refusal, composition mapping, the hidden mark on the word being typed. - `ComposerAnnotators.test.tsx` (jsdom, annotator registered through the real `PluginRuntime` and `ConversationService`): observation shape and context, emoji offsets, keyboard and right-click menus, Escape and focus return, replacement with Undo/Redo, `aria-invalid` kinds, stale and invalid publications, disposer ownership, menu closing on same-draft withdrawal or replacement, earlier underlines kept across a stale disposer, an immediately publishing annotator surviving a destination change, same-text structural change, retirement on disable, destination change, plugin removal and render failure, IME composition, disabled and read-only editors, unmount. - `MessageComposer.test.tsx`: the real composer wiring, including retirement on a destination change. - No browser journey was added; jsdom covers the contract and the remaining gaps are native-only. On a Linux remote box against this head rebased on current `main`: `pnpm check` (Biome, `tsc`, design typecheck and design checks) passed, and `vitest run` over `src/features/conversation`, `src/plugins`, `EditableInput`, `MessageComposer` and `composer-annotations` passed (35 files, 1374 tests). The four new lifecycle tests fail against the previous source and pass with the fix. The local pre-push hook was skipped because CI runs the same checks. **Not yet verified:** - right-click selection and context-menu behavior in native WebKit; - whether ⌘. reaches the webview on macOS; - screen-reader announcement of `aria-invalid`; - real IME composition in the desktop app; - both themes, narrow and wide widths, and enlarged text, with a real annotator installed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Kyle Zemek <kylez@squareup.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Reuse the host editing engine, prototype Markdown adapter, persistent formatting and generic link destinations through conversation.ui.DocumentEditor. Keep content, source mode, link intent and save policy controlled by the plugin. Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@feedback.communities.buzz.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Keep rich text synchronized with the accepted controlled value, and rebuild it from accepted source content instead of source keystrokes. Suppress rollback echoes through onChange and cover declined rich/source edits plus accepted source transitions. Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@feedback.communities.buzz.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Restore the last accepted editor checkpoint so a declined edit retains selection and undo state without a normalized change callback. Cover a following Backspace and equivalent italic, bold, bullet and numbered Markdown forms. Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@feedback.communities.buzz.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Use opt-in CommonMark serialization for documents, retaining nested paragraph/list structure and marked hard breaks. Keep chat defaults and align document Enter, Shift+Enter and serialized editing budgets with the reader. Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@feedback.communities.buzz.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Leave literal double brackets editable when there are no destinations, while preserving the existing destination shortcut. Cover omitted and empty choices plus the opt-in path. Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@feedback.communities.buzz.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Route prose before code spans through the hard-break-aware leaf. Cover root, quote and list round trips. Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@feedback.communities.buzz.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Escape an odd authored trailing slash before the generated hard-break marker. Cover slash runs and typed Shift+Enter without changing chat output. Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@feedback.communities.buzz.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Use document policy for wire output, editing budgets and clipboard import/export. Retain hard breaks and list spacing in owned HTML, parse plain Markdown through the existing bounded adapter, and keep default chat clipboard output unchanged. Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@feedback.communities.buzz.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Retain embedded fallback source newlines during plain paste and carry them through owned HTML with the existing source-span codec. Keep supported prose normalization and default chat behavior unchanged; cover task lists, tables and supported prose in both clipboard flavors. Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@feedback.communities.buzz.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Use the raw destination context before interpreting copied fragments as document blocks. Keep task-list and table source intact through both clipboard flavors, with partial-selection regressions beside the existing full-selection cases. Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@feedback.communities.buzz.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Inspect text children for raw-source newlines so document prose with hard breaks keeps structured paste and URL-over-selection behavior. Cover both clipboard flavors in hard-break and ordinary prose destinations while retaining the raw-source regressions. Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@feedback.communities.buzz.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@feedback.communities.buzz.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Cover authored clipboard output in chat and document modes after integrating namesake send qualification. Keep written names and explicit qualifiers literal in document source without signed recipients. Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@feedback.communities.buzz.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Exercise the shared paste path with office text and its image sidecar in the document editor, alongside main's existing composer and clipboard classification coverage. Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@feedback.communities.buzz.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
416b9ba to
a88e640
Compare
7082669 to
9e00e26
Compare
















Overview
Category: improvement
User Impact: Installed document plugins can offer rich-text and Markdown-source editing while preserving paragraphs, hard breaks and clipboard content.
Problem: Plugins need editing without copying app code, while chat serialization and clipboard rules can change a document’s meaning when reused directly.
Solution: Expose a controlled
conversation.ui.DocumentEditorusing the existing input engine, formatting controls and link dialog with document policy. Plugins own persistence, permissions and mode controls; leavingvalueunchanged restores a declined edit and caret without another change notification. Read mode uses the parent PR’s shared renderer with document-scale headings, wrapping inline pills and filled, wrapped code blocks capped at a partial bottom line.This PR is stacked on #2762 and should merge after it. The formatting bar remains formatting-only; the example’s Read/source controls are plugin-owned and sit beside it. Mainline namesake-send qualification, authored-only copying, literal folder paths and office/long-paste behavior are preserved alongside document mode and raw-source/hard-break handling.
Changes
File changes
docs/plugin-authoring-guide.md
Document controlled editing, CommonMark/clipboard policy, optional destinations, limits and plugin-owned saving.
src/features/conversation/service.test.tsx
Verify the injected editor is the existing host component.
src/features/conversation/service.tsx
Expose the existing controlled DocumentEditor through conversation UI injection.
src/features/messages/ComposerFormattingTools.tsx
Reuse persistent formatting controls for document editing.
src/features/messages/ComposerLinkDialog.tsx
Reuse validated links with optional loaded document destinations.
src/features/messages/DocumentEditor.module.css
Keep document-editor layout with its feature owner.
src/features/messages/DocumentEditor.test.tsx
Cover controlled acceptance/decline, caret/history, paragraph and break semantics, source preservation, paste and limits, including office text with an image sidecar.
src/features/messages/DocumentEditor.tsx
Keep the plugin value authoritative while selecting document policy for the existing rich/source input.
src/features/messages/EditableInput.tsx
Apply the selected Markdown policy to budgets and clipboard paths while distinguishing literal source newlines from semantic hard breaks.
src/features/messages/composer-clipboard.test.ts
Retain chat behavior and cover authored-only namesake copying in chat/document modes alongside mainline paste classification.
src/features/messages/composer-clipboard.ts
Export document Markdown and sanitized rich HTML while retaining authored-only copy and literal fallback source.
src/features/messages/composer-document.test.ts
Verify backward-compatible document list-spacing metadata.
src/features/messages/composer-document.ts
Retain document list spacing in the existing editor schema.
src/features/messages/composer-dom.ts
Distinguish document paragraphs from hard breaks in the existing DOM conversion.
src/features/messages/composer-markdown.ts
Preserve chat defaults and namesake send qualification while serializing document structure, literal folder paths and hard breaks.
src/features/messages/document-editor-markdown.test.ts
Compare parsed document meaning across nested structure, hard breaks, inline code, backslashes and bounded fallback source.
src/features/messages/document-editor-markdown.ts
Reuse the bounded Markdown parser and existing composer schema for supported document structure.
src/features/messages/selection-copy.test.tsx
Keep selection-copy behavior asserted with the schema defaults.
src/features/messages/selection-copy.ts
Carry only whitelisted document spacing metadata through sanitized clipboard import.
src/plugins/author.test.mjs
Compile an independent consumer of the public contracts and reject host-only workspace options.
src/plugins/author.ts
Export the supported document editor props, handle and selection types.
tests/browser/document-plugin.mjs
Exercise the injected host UI from an installed module that owns its content and saving.
tests/browser/plugin-import.spec.mjs
Extend the installed-document workflow through rich/source/read modes, formatting, clipboard flavors, links and controlled saving.
Reproduction steps
docs/plugin-authoring-guide.md. Switch between rich, Markdown-source and read modes; save with Cmd/Ctrl+Enter.valueunchanged inonChangeto decline an edit. The accepted document and caret restore; the next edit and save use that accepted state.Validation and limits
Mandatory publication hooks pass on the full editor stack at
a88e640f1d: TypeScript and related unit tests, design type checks/guards, and publication policy. The parent9e00e26fd8was pushed in the same atomic update; its dedicated PR CI remains authoritative for that parent-only state. The Clippy lane correctly skipped because this stack changes no Rust inputs.The full shared-Markdown browser file passes 3 cases with 1 existing WebKit screenshot-only skip across Chromium/WebKit, covering both themes, three widths and normal/enlarged text. It checks wrapping, the partial bottom line, scrolling to the end, literal source text and the existing copy interactions. Three filled-code capture journeys pass, including frame/snippet color parity and wheel scrolling in the installed reader/editor. The full installed-plugin file last passed 40/40 before the color-only refinement; fresh PR CI covers the combined update.
A fresh rich/source capture journey also passes on the published editor head, including both themes and a source-value round trip.
Captures use the real built app components with disposable documents and modeled relay/native inventory. They establish browser behavior, not packaged-native, live-relay or operating-system clipboard acceptance. The editor clipboard journey uses page-local ClipboardEvent/DataTransfer. The most recent repository-wide unit run had a dialog Cancel-focus assertion that also failed on its untouched base; no assertion or timeout was changed to hide it. Required review approvals and fresh CI remain pending.
Screenshots
Headless built-app fixtures with disposable documents and modeled relay/native inventory. The rich/source example places plugin-owned mode controls beside the host’s formatting-only bar. Read mode uses the shared document heading scale and code treatment from #2762.