Skip to content

feat(chat): add inline paste token composer - #8310

Merged
iamwhatever merged 1 commit into
kirodotdev:mainfrom
Pearcekieser:feat/lexical-paste-token-composer
Sep 7, 2026
Merged

feat(chat): add inline paste token composer#8310
iamwhatever merged 1 commit into
kirodotdev:mainfrom
Pearcekieser:feat/lexical-paste-token-composer

Conversation

@Pearcekieser

@Pearcekieser Pearcekieser commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Problem / Motivation

Collapsed paste markers are rendered in a native textarea while their background and hover hit area come from a separate mirrored div. Browser wrapping, resize, and scroll behavior can make those two layouts diverge, detaching the highlight and preview from the marker text.

Why it matters

A detached paste token is visually misleading and difficult to target. Additional mirror synchronization cannot guarantee parity because textarea layout is browser-owned.

What changed (motivation → approach → change)

This PR adds a default-off Lexical composer path that represents collapsed pastes as real atomic inline editor nodes. The existing serialized [ Paste #N · M lines ] value and PasteBlock[] sidecar remain canonical, so textarea rollback and stored drafts remain compatible.

The slice includes:

  • exact-pinned Lexical dependencies, lazy-loaded into a separate chunk
  • an atomic non-editable PasteTokenNode
  • editor-neutral focus and canonical-selection controls
  • existing text/image clipboard precedence, raw paste, normalization, and collapse policy
  • expanded copy/cut and atomic deletion
  • node-owned hover/focus/click preview anchoring
  • prompt-history, dictation-caret, picker anchoring, and focus integration
  • recoverable lazy-load fallback to the production textarea
  • composer-resize attribution on the Lexical path (markComposerResize), so the transcript's bounce fix from perf(chat): reachable archived history and stable phone scrolling #7916 holds when the editor grows under typing

No production caller enables lexicalComposer; the textarea remains the default and rollback path while broader rollout evidence is gathered.

Tests

  • 410 affected tests across 17 composer/paste files (rebased onto current main)
  • focused Lexical hydration, editing, paste collapse, clipboard, selection offsets, history, activation, image precedence, raw paste, lazy failure, and rollback coverage
  • TypeScript and diff-scoped ESLint
  • i18n gate (19 checks) passing; the Lexical namespace config identifier is exempted in eslint.i18n.config.js with a measured rationale (single occurrence, no baseline handed back)
  • duplication check
  • production and analyze builds
  • bundle-size gate: chunks within budget
  • dependency-tree and diff-hygiene checks

Manual verification

A real Chromium harness verified that inline nodes remain inside the editor during wrapping, narrowing, and internal scrolling, previews anchor to the actual node rectangle, large pastes create new atomic nodes, no mirror is rendered, and no browser Lexical errors are emitted.

Screenshots / video

Before — the textarea/mirror path with the highlight detached from the marker text (identifying regions redacted):

Collapsed paste highlight detached from composer text

After — the paste token as a real inline Lexical node laid out with the surrounding text:

Inline paste token rendered as an atomic Lexical node

After — the preview anchored to the token's own DOM element:

Paste preview anchored to the inline token

Related Issues

Related to #8309 — deliberately NOT Fixes: this slice ships default-off with no production caller, so the defect remains user-visible until a rollout PR enables lexicalComposer; auto-closing the bug here would be dishonest.

Pattern harvest

Rule candidate: review-prompt
Pattern: Do not style native textarea substrings through a separately laid-out mirror when alignment must survive wrapping and scrolling; use one editor-owned inline node.

Checklist

  • At most two commits (one is the norm), with a Conventional Commits title (feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)
  • Existing tests pass and new tests added for new functionality
  • Self-review completed; code follows project style guidelines
  • Documentation updated (N/A — default-off internal migration seam; behavior is covered in code comments and tests)
  • No secrets, credentials, or internal references in the diff

@Pearcekieser
Pearcekieser requested a review from a team September 3, 2026 23:10
@Pearcekieser
Pearcekieser requested a review from a team as a code owner September 3, 2026 23:10
@github-actions github-actions Bot added readiness: checking Automated validation is still running fork Pull request from a fork (external contributor) labels Sep 3, 2026
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Sep 4, 2026
@github-actions github-actions Bot added the merge conflict Branch has merge conflicts with its base — author must resolve before merge label Sep 4, 2026
@Pearcekieser
Pearcekieser force-pushed the feat/lexical-paste-token-composer branch from 998b4c4 to 3aed02a Compare September 6, 2026 01:26
@github-actions github-actions Bot removed the merge conflict Branch has merge conflicts with its base — author must resolve before merge label Sep 6, 2026
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5.1, fork) — 🟡 CONCERNS

Premise-level review of 64b5b6775e2f3680e2d9d864802707fa7c9b6d6e via the fork AI-review pipeline — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Confirmed: no prior Lexical or @lexical usage exists in website/src (this is genuinely new, no duplicate mechanism), and temp-screenshots/<feature>/ is a documented repo convention (.github/PULL_REQUEST_TEMPLATE.md:47, ci.yml, cleanup-temp-screenshots.yml) — so the committed PNGs are derived, not cruft. Dropping that candidate.

First-Principles-Verdict: CONCERNS

Honestly-framed incremental migration, but the whole slice ships behind lexicalComposer (default off, zero production callers) so it removes no user harm until a later rollout PR flips it.

What this change ships

Intent: fix the textarea/mirror paste-highlight detach by representing pastes as real inline editor nodes — an ADDITION (author explicitly declines Fixes #8309).

  1. Opt-in lexicalComposer prop + Lexical editor path (default off) — zero production consumers
  2. Paste markers as real inline chips vs textarea+mirror — justified (the actual fix), gated off
  3. Lazy-load error boundary + "editor unavailable" fallback notice — justified
  4. ~9 new npm dependency families (lexical*, @floating-ui/react, yjs, lib0…) + notices — justified by item 1
  5. Extracted composerPastePolicy.ts shared helpers — justified (real second consumer)
  6. composerControl engine-neutral abstraction — justified (supports the seam)
  7. New i18n strings (editor_unavailable, show/hide_paste_preview) — justified
  8. eslint.i18n namespace exemption — justified (documented aliases class)
  9. temp-screenshots/lexical-paste-token/*.png — justified (documented convention)
  10. editorRef prop on LexicalComposerInput — zero production consumers (test-only)

Watch

  • lexicalComposer (ChatInput.tsx:1128) has zero production callers — grepped lexicalComposer|LexicalComposerInput across website/src: only the defining site and tests. Speculative surface until a rollout PR enables it; the author states this openly, so it is a staged-migration risk a human should weigh, not a defect.

Subtractions

  • Drop the editorRef prop and EditorRefPlugin from LexicalComposerInput.tsx — 0 production consumers (ChatInput renders it without editorRef; only the test ControlledHost passes one). Test-only surface on an exported component.

[FIRST-PRINCIPLES-REVIEWED] 64b5b67

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5.1, fork) — 🟡 CONCERNS

Design-level review of 64b5b6775e2f3680e2d9d864802707fa7c9b6d6e via the fork AI-review pipeline — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Verified two things before judging: temp-screenshots/ is an established "committed deliverable" convention in this repo (gitignore line 97, hundreds of existing subdirs), so the three new PNGs are not cruft — not a finding. And no caller wires the new lexicalComposer prop (grep found none), confirming this is slice-1 infrastructure landing behind a default-off flag, reachable only from tests for now — expected for a migration slice.

Design assessment: this is a well-structured incremental migration — opt-in off by default, textarea kept as the declared rollback target with a lazy-load error boundary and recovery notice, paste policy and IME latch factored into shared modules, and an engine-neutral ComposerControl interface. Reversibility is strong and explicitly tested. The genuine risk is the dual-implementation state.

Design-Verdict: CONCERNS

Sound, reversible migration-slice design; the real risk is two full composer implementations that must stay behavior-identical for an open-ended window with no parity harness pinning them.

Watch

  • Parity drift: the textarea (fallback) and Lexical paths independently reimplement paste-collapse, undo/redo, prompt-history nav, IME-Enter, and caret handling. Shared composerPastePolicy/useImeGuard help, but tests exercise each path separately — nothing pins the two to identical observable behavior, so a fix to one can silently diverge from the fallback users actually land on. Consider a shared behavioral parity test and a tracked migration-completion milestone so the dual-path state doesn't persist indefinitely.

Suggestions

  • @lexical/react drags in yjs/lib0/isomorphic.js (CRDT collaboration) as optional peers for what is today a plain-text composer; confirm the bundler tree-shakes them out of the lazy chunk so the opt-in path doesn't ship a CRDT engine it never uses.

[DESIGN-REVIEWED] 64b5b67

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

UX Review (Fable 5.1, fork) — 🟡 CONCERNS

UX-level review of 64b5b6775e2f3680e2d9d864802707fa7c9b6d6e via the fork AI-review pipeline — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

This is the fork lane: PR is from owner Pearcekieser, and the three temp-screenshots/lexical-paste-token/*.png the PR adds are not on disk. No blind read ran.

Reconcile — user-visible surface this diff adds (all gated behind the default-off lexicalComposer prop; no production caller enables it):

  • Inline composer paste-token chip (PasteTokenNode.tsx) with a click/hover Show paste preview / Hide paste preview toggle → floating tooltip.
  • New strings show_paste_preview / hide_paste_preview.
  • Fallback ErrorNotice: "The upgraded editor could not load, so the standard input is active. Your draft is unchanged." (editor_unavailable_standard_input).
  • The Lexical contenteditable composer replacing the textarea, plus its loading skeleton (role="status", aria-busy).

None appears in a materialized screenshot; no first-time reader has seen any of them. The default textarea path (the production path) is behaviorally unchanged by this diff.

Checks against the read-off BLOCK exits: no blind read → the primary-control exit cannot fire. The textarea↔Lexical substitution is gated on an opt-in flag / async lazy-load failure (lens-4 lifecycle), not a runtime flip of a persistent element the user identified — not a lens-13 hard swap. The fallback notice asserts what happened and the recovered state (draft unchanged) — it neither hedges nor is content-free. So no BLOCK; verdict is capped at CONCERNS by the evidence gaps.

UX-Verdict: CONCERNS

Every new user-visible control ships unseen — fork lane has no screenshots and no blind read, so comprehension of the paste-token chip and fallback notice is unverified.

Watch

  • Chip verb/behavior drift vs sibling. The new composer chip's toggle reads "Show/Hide paste preview" and opens a transient floating tooltip, while the same-looking transcript chip (PastedChip.tsx:99) reads "Expand/Collapse paste" and reveals content inline. A user habituated to the transcript chip may click the composer chip expecting inline expansion and instead get a dismissible preview. Low severity (default-off, no production caller today), but the collision surfaces the moment rollout enables it — worth confirming the two verbs are intentional before a caller ships.

Evidence gaps

  • Inline paste-token chip + its Show/Hide preview toggle: shown in no materialized screenshot; after-lexical-inline-token.png / after-lexical-token-preview.png would close it if the branch were pushed to this repo for a blind read.
  • Fallback ErrorNotice ("The upgraded editor could not load…"): no screenshot of the error state.
  • Lexical loading skeleton (aria-busy) state: no screenshot.

[UX-REVIEWED] 64b5b67

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review (fork) — ✅ no blocking findings

Reviewed 64b5b6775e2f3680e2d9d864802707fa7c9b6d6e via the fork AI-review pipeline; updated in place on each push.

Review details

No findings.

[OPUS-REVIEWED] 64b5b67

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review (fork) — ✅ no blocking findings

Reviewed 64b5b6775e2f3680e2d9d864802707fa7c9b6d6e via the fork AI-review pipeline; updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] 64b5b67

@Pearcekieser
Pearcekieser force-pushed the feat/lexical-paste-token-composer branch from 3aed02a to bd8225a Compare September 6, 2026 01:49
@Pearcekieser

Copy link
Copy Markdown
Contributor Author
  • Token activation sends the draft — span=853ae4f13b06 — fixed in bd8225ad579a.

The chip is non-editable DOM inside the editor root, so its Enter keydown reached Lexical's root listener before the chip's own React activation handler, and the editor's send-on-Enter handler submitted the draft on the same keystroke that opened the preview.

Fix: KEY_ENTER_COMMAND now claims the event when it originates inside a token chip (event.target.closest('[data-paste-seq]')) and returns without sending or inserting a break, leaving activation to the chip's own handler. Regression added: the %s activation test now renders with an onSend spy and asserts it is never called on chip Enter/Space activation (website/src/test/LexicalComposerInput.test.tsx). Verified: tsc, eslint, and all 47 composer tests pass; the new assertion fails against the pre-fix handler.

@Pearcekieser
Pearcekieser force-pushed the feat/lexical-paste-token-composer branch from bd8225a to d803aad Compare September 6, 2026 02:31
@Pearcekieser

Copy link
Copy Markdown
Contributor Author
  • WebKit IME commit Enter sends the draft — span=853ae4f13b06 — fixed in d803aad7a26e.

On WebKit the keydown that commits an IME candidate arrives after compositionend with isComposing already false, so the handler's native-flag check (event.isComposing || event.keyCode === 229) could not identify it and the draft was sent mid-composition-commit.

Fix: the plugin now uses the repository's shared IME latch (createImeLatch from src/hooks/useImeGuard.ts — the ImeEnterClaimRatchet reference implementation) instead of a private native-flag check. Composition and stranded-latch recovery listeners are attached to the editor root via registerRootListener, and KEY_ENTER_COMMAND declines through latch.claimKey, which recognizes the post-compositionend window and owns both halves of the consume contract (preventDefault only where the browser would otherwise act; never mid-composition). Regression added: does not send on the WebKit commit Enter that lands after compositionend drives compositionstart → compositionend → commit Enter inside the latch window (must not send) and a plain Enter past the window (must send exactly once). Verified: tsc, eslint, and all 48 composer tests pass.

@Pearcekieser
Pearcekieser force-pushed the feat/lexical-paste-token-composer branch from d803aad to 65ef15d Compare September 6, 2026 03:11
@Pearcekieser

Copy link
Copy Markdown
Contributor Author
  • Lazy-load failures are silently hidden — span=10f93377d1b3 — fixed in 65ef15d4240c.

ComposerLoadBoundary.componentDidCatch swallowed the chunk-load error entirely: the user's fallback to the textarea was seamless, but a failing editor chunk after a deploy would disable the opt-in path fleet-wide with nothing to diagnose.

Fix: the boundary now logs the error and component stack via the repository's boundary convention (AppHost's AppErrorBoundary): console.error('[ChatInput] Lexical composer failed to load; falling back to textarea:', error, info.componentStack) before triggering the textarea fallback. The recovery test now asserts the failure is logged (call matching Lexical composer failed to load) in addition to the seamless draft-intact fallback. Verified: tsc, eslint, and all 48 composer tests pass.

@Pearcekieser
Pearcekieser force-pushed the feat/lexical-paste-token-composer branch from 65ef15d to a025deb Compare September 6, 2026 03:55
@Pearcekieser

Copy link
Copy Markdown
Contributor Author
  • Composer load failure bypasses ErrorNotice — span=10f93377d1b3 — fixed in a025debdcda6.

The previous round made the chunk failure diagnosable (console + component stack) but still invisible to the person using the composer: someone who opted into the editor was moved back to the textarea with no user-facing signal.

Fix: ChatInput now renders the shared ErrorNotice (inline variant, dismissible, data-testid="composer-fallback-notice") above the composer when lexicalLoadFailed is set, with a new localized message (components.chatInput.editor_unavailable_standard_input) added to all 12 catalogs plus the regenerated en-XA pseudolocale — the i18n gate passes 19/19. askAgent stays off per ErrorNotice's own draft-safety contract (the hand-off unmounts the composer, and the draft is exactly what is unsaved). The recovery test now asserts the notice's localized text renders, dismisses on click with the draft intact, and the operator log still fires. Verified: tsc, eslint, i18n:check, and all 48 composer tests pass.

@Pearcekieser
Pearcekieser force-pushed the feat/lexical-paste-token-composer branch from a025deb to 6e1ebc7 Compare September 6, 2026 04:35
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: passed Eligible automated validation passed for the current revision and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 6, 2026
@github-actions github-actions Bot added the merge conflict Branch has merge conflicts with its base — author must resolve before merge label Sep 6, 2026

@bolichen97 bolichen97 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tech Lead review: not approving.

Security and i18n are clean (no innerHTML/dangerouslySetInnerHTML, paste-derived label is numeric-only, i18n gate green). Three substantive issues:

  1. Fixes #8309 would auto-close a still-open bug: zero production callers currently set lexicalComposer, so this PR does not actually exercise the fixed path in production yet. Please remove the auto-close linkage or confirm a production caller.
  2. openTrigger now inserts @/$ at the caret instead of appending, for every current user on the DEFAULT textarea path — this contradicts the PR's own stated claim that the textarea path is an untouched rollback target. This is an undeclared behavior change.
  3. PasteTokenNode reuses the expand_paste action string for what is actually a preview-only action; please use a distinct, accurately-named string.

Also structurally blocked: this touches 5 non-exempt website/src/** files, which requires a UX-ruleset (team) approval on top of a general approval — neither exists yet.

@Pearcekieser
Pearcekieser force-pushed the feat/lexical-paste-token-composer branch from 6e1ebc7 to 2568209 Compare September 7, 2026 14:20
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running and removed readiness: passed Eligible automated validation passed for the current revision readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running merge conflict Branch has merge conflicts with its base — author must resolve before merge labels Sep 7, 2026
Add a default-off Lexical composer path with atomic inline paste nodes, editor-neutral selection and focus controls, shared clipboard policy, lazy-load rollback, and focused regression coverage while preserving the textarea fallback.
@Pearcekieser
Pearcekieser force-pushed the feat/lexical-paste-token-composer branch from 2568209 to 64b5b67 Compare September 7, 2026 15:08
@Pearcekieser

Copy link
Copy Markdown
Contributor Author

@bolichen97 Thank you for the review — all three substantive issues are addressed in 64b5b6775e2f:

  1. Auto-close linkage removed. The PR body now reads "Related to Collapsed paste highlights detach from composer text #8309" with an explicit note that the defect stays user-visible until a rollout PR enables lexicalComposer. You are right that Fixes would have closed a still-open bug.

  2. openTrigger restored to base semantics. The @/$ sigil appends at the end of the draft exactly as on current main; only the focus/caret placement routes through the engine-neutral ComposerControl (identical observable behavior on the textarea path: focus + caret at end of value). The caret-insertion variant is gone.

  3. Distinct action strings. The token chip now uses new accurately-named components.pastedChip.show_paste_preview / hide_paste_preview strings in all 12 catalogs (pseudolocale regenerated); the transcript chip's expand_paste/collapse_paste remain untouched for the action that actually expands inline. The activation regression pins the new title.

On the structural gate: understood on the UX-ruleset approval — the branch is ready for that review whenever the team can take it. On the flag-enablement question raised here and by the First Principles lane, we're deciding between enabling lexicalComposer for the dashboard ChatPage in this PR versus keeping the slice default-off and following up with a rollout PR; will update shortly.

@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: passed Eligible automated validation passed for the current revision and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 7, 2026
@iamwhatever
iamwhatever merged commit 15084b5 into kirodotdev:main Sep 7, 2026
70 of 76 checks passed
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fork Pull request from a fork (external contributor)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants