Skip to content

fix(ui): fix Skill chip alignment, slash matching, IME commit and transcript rendering - #5555

Merged
Astro-Han merged 8 commits into
apache:mainfrom
Astro-Han:fix/skill-pill-inline-rendering
Sep 21, 2026
Merged

Astro-Han merged 8 commits into
apache:mainfrom
Astro-Han:fix/skill-pill-inline-rendering

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Four defects reported from a desktop session, all on the Skill-chip surface. Each one is a place where the chip stopped being a faithful rendering of the /skill:<id> invocation the composer stages.

  1. A staged chip sat below the text's line. insertToken anchors a chip with vertical-align: middle, which centres the chip's box on the parent's x-height midline. A 20px chip in a 22px line therefore reads low against CJK and mixed-case text (measured 1.8px against the line centre).
  2. The / menu looked like it did not match until the third letter. Candidates were filtered by id, name and description with no order, so one or two typed letters of a Skill's own name sorted below every Skill whose prose merely mentioned the word. With a 28-Skill catalog, w matched 20 of them and de put debug behind Skills that only mention the word.
  3. The first Chinese word typed after a chip arrived as pinyin. insertToken leaves the caret on a child offset of the contenteditable (setStartAfter). Chromium anchors an IME composition to the DOM boundary it starts from, and from an element boundary the first preedit commits as its raw letters instead of the composed character.
  4. A Skill chip in the transcript appeared and disappeared. A user row chose between two renderers on the truthiness of inlineReferences — a hint the Host freezes from the invocation receipts — so a row carrying the token with an empty array drew the raw /skill:<id> text, then flipped to a chip once its canonical copy arrived.

Fixes:

  • The chip's box is one line box tall and the pill is centred inside it, aligned to the line box (height: 1lh, align-items: center, vertical-align: top). middle ignores a box height, so it cannot express "centred on the line". The transcript's token wrapper carries the same declarations; the two must agree, or a token moves when the message is sent.
  • /-menu candidates are ranked (id/name prefix, id/name substring, every query word in id/name, description only) and sorted inside each group. Equal ranks keep the catalog order, so an empty query is unchanged. Commands are ranked the same way, with their keywords counting as their name.
  • Every programmatic caret move lands inside the text node it points at: Astryx's insertToken, expandToken, token paste, insertTextAtCursor, placeCaretAtEnd, ensureCaretInside and the controlled-write caret restore, plus the composer's own caret-to-end. Tokens are contenteditable="false", so a trailing chip is never entered.
  • InlineReferenceText is the single renderer for a user row: it draws the references the message carries and the /skill:<id> invocations the text spells. A token-only chip is labelled with its id, which is what the Host's frozen reference labels itself from whenever a Skill's name is its id.

Three of the four live in the @astryxdesign/core patch, alongside the existing hunks; the src/ counterparts are edited to match. The patch was regenerated against the pristine 0.6.2 tarball and applies to a clean tree byte for byte.

Verification

  • npm --workspace @maka/ui run build and node --test --test-concurrency=4 "dist/**/*.test.js": 562 tests, 561 pass. The one failure (composer-model-picker-recovery) fails identically on main.
  • New coverage: packages/ui/src/__tests__/chat-turn-inline-references.test.tsx (token-only row, absent field, frozen label wins, file + Skill side by side, a stale reference skipped) and the mentionMatchRank cases in chat-input-behavior.test.ts.
  • Two new stories pin the browser-side contracts and fail on main: product-composer-skill-draft--staged-skill-sits-on-the-text-line ("expected 2 to be less than 1": a 20px chip box in a 22px line) and product-attachments--skill-token-without-a-frozen-reference (no chip at all).
  • Chip geometry measured on the composer story itself, the state the screenshots come from: the chip box is 20px → 22px inside a 22px line, and its centre sits 1.81px → 0.5px below the line's centre.
  • Real Electron (fixture) driven through CDP, before → after: an IME commit of ni after a chip produced Project Only n你Project Only 你; a Skill-only message's transcript row showed the raw token → a chip.
  • npm --workspace @maka/desktop run typecheck, typecheck:stories, check:architecture, and biome lint on the changed files: clean. The seven stories that touch these surfaces were rendered and their play functions finish without errors.

Composer: where the staged chip sits on the text line

light dark
composer chip before and after, light composer chip before and after, dark

Transcript: a chip drawn from the token

light dark
transcript row before and after, light transcript row before and after, dark

Review focus

  • The dependency patch is most of the diff. Three of the four fixes are in vendored Astryx code (insertToken, ChatTokenizedText, chatComposerSelection, ChatComposerInput), and the caret guard is duplicated in the composer's own caretToContentEnd because that helper has focus semantics Astryx's does not.
  • InlineReferenceText now also tokenizes /skill:<id> in the text gaps. A structured reference still wins when the message carries one, so the Host's frozen label (the Skill's name) is what a settled row shows.

AI use

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Maka diagnosed the four defects, implemented the fixes and wrote the tests; every commit carries a Generated-by: Maka trailer.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

Follow-up commits (review fixes)

  • 819feeac7 — transcript chips now render at grammar match positions: ChatTokenizedText re-locates a token by its value, which chipped positions the grammar rejects (a/skill:x beside a real invocation, a token glued to a reference span, a longer id split by a shorter one). Each grammar match now gets an island holding exactly the matched text; three regression tests pin this.
  • 8b9b11c2b — when the editable ends in a token whose trailing space was deleted, a <br>, or nothing, the caret walk left an element boundary and the IME defect survived there; it now appends an empty text node (serializes to '') and lands inside. The ./Chat barrel re-exports placeCaretAtEnd, so the composer's own caret-to-end uses the vendored path instead of a duplicated walk (~36 lines removed); export ask tracked in [Bug] ChatComposerInput: programmatic carets anchor on element boundaries, so the first IME preedit commits as raw letters facebook/astryx#6411. patches/README.md gained the rationale + delete-when entries for the caret, chip-geometry and re-export hunks.
  • f02bedadmentionMatchRank's every-word-in-name tier was unreachable (the / query never contains whitespace); dropped, docblock corrected.
  • 3f2229542 — the new row tests use the shared transcript-test-dom mount instead of a third hand-rolled copy.

The `/` menu filtered Skills by id, name and description with no order, so
one or two typed letters of a Skill's own name sorted below every Skill whose
description merely mentioned the word: the list looked like it had not matched
until the query grew long enough to exclude those descriptions.

Rank every candidate — id/name prefix, id/name substring, every query word
present in id/name, description only — and sort inside each group. Equal ranks
keep the catalog order, so an empty query is unchanged. Commands are ranked the
same way, with their keywords counting as their name.

Generated-by: Maka
`insertToken` anchors a chip with `vertical-align: middle`, which centers the
chip's box on the parent's x-height midline. A 20px chip in a 22px line is
therefore ~1px low against CJK and mixed-case text, which is what a staged
Skill chip next to typed Chinese showed.

Give the chip's box the height of one line box and center the chip inside it
(`height: 1lh`, `align-items: center`), aligned to the line box with
`vertical-align: top`. `middle` ignores a box height, so it cannot express
"centered on the line". The transcript's token wrapper in ChatTokenizedText
carries the same declarations: the two must agree, or a token moves when the
message is sent.

Generated-by: Maka
@github-actions github-actions Bot added the effort/L Under 1000 readable lines label Sep 21, 2026
@Astro-Han
Astro-Han force-pushed the fix/skill-pill-inline-rendering branch from df1a7c6 to 5785e10 Compare September 21, 2026 05:10
Chromium anchors an IME composition to the DOM boundary it starts from. A caret
sitting on a *child offset of the contenteditable* — what `setStartAfter` and
`selectNodeContents` + `collapse(false)` produce — is the same visual caret as
the end of the adjacent text node, but from it the first preedit commits as its
raw letters instead of the composed character. So the first Chinese word typed
after a Skill chip arrived as pinyin.

Land the caret inside the text node it points at: in Astryx's `insertToken`,
`expandToken`, token paste, `insertTextAtCursor` and the two end-of-content
helpers, and in the composer's own `caretToContentEnd`. Tokens are
`contenteditable="false"`, so a trailing chip is never entered.

Generated-by: Maka
A user row rendered `inlineReferences` when the field was present and the raw
`/skill:<id>` text otherwise. The field is a frozen hint the Host composes from
the invocation receipts, so the optimistic row, the desktop's local copy and
any invocation with no successful receipt carry the token as plain text *and*
an empty array — truthy, so the row took the branch that had no Skill chip to
draw. The same message then flipped to a chip once its canonical copy arrived.

Make InlineReferenceText the one renderer for a user row: it draws the
references the message carries and the `/skill:<id>` invocations the text
itself spells, so the chip no longer depends on a hint being populated.
Token-only chips are labelled with the id, which is what the Host's frozen
reference labels itself from whenever a Skill's name is its id.

Generated-by: Maka
@Astro-Han
Astro-Han force-pushed the fix/skill-pill-inline-rendering branch from 5785e10 to 804892c Compare September 21, 2026 05:23
@Astro-Han
Astro-Han marked this pull request as ready for review September 21, 2026 13:44

@jackwener jackwener left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I reviewed 804892cde18f65668a42215d89a840f8913b6961.

No P0–P2.

The four Skill-chip defects are real and the cuts are the right size. A chip is one line box (1lh, vertical-align: top) so it sits on the line rather than the x-height midline. / menu ranking puts id/name matches ahead of description hits. insertToken lands the caret inside the trailing NBSP text node so Chromium IME can compose. InlineReferenceText draws /skill:<id> from the text as well as frozen Host references, so an empty inlineReferences array no longer flashes raw tokens.

Required check test is green on this head.

简体中文

我审查了 804892cde18f65668a42215d89a840f8913b6961。没有 P0–P2。

四个 Skill chip 缺陷都成立:行盒对齐、/ 菜单按名字排序、IME 落在文本节点里、transcript 从正文画出 /skill:<id>。本 head 上 test 绿。


Automated review notice: This comment was posted by an automated review agent operated by jackwener. It is not an independent human review and does not replace one.

Astro-Han and others added 4 commits September 21, 2026 22:44
The hand-rolled linkedom mount was a third copy of what
transcript-test-dom.ts already installs; the row tests need only
render() and cleanup().

Generated-by: Devin

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
ChatTokenizedText re-locates a token by its value, so tokenizing a gap
by value set chipped positions the grammar rejects: an 'a/skill:x' URL
shape beside a real invocation (permanent when its receipt fails), a
token glued to a reference span (a false '^' at the gap's start), and a
longer id split by a shorter one. Each grammar match now gets an island
holding exactly the matched text.

Generated-by: Devin

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
When the editable ends in a token with its trailing space deleted, a
<br>, or nothing, the boundary walk left the caret on the element
boundary — the same IME anchor defect this PR fixes elsewhere. The walk
now appends an empty text node (serializes to '') and lands inside it.

The ./Chat barrel re-export of placeCaretAtEnd lets the composer's own
caret-to-end use the vendored path instead of duplicating the walk;
upstream asked for the helpers to be exported in facebook/astryx#6411.
README gains the rationale and delete-when entries the patch convention
requires, covering the caret, chip-geometry and re-export hunks.

Generated-by: Devin

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The slash-menu query never contains whitespace, so the every-word-in-name
rank could only be reached by tests. Removing it also removes the
duplicate of mentionQueryMatches' tokenize-every-includes loop, and the
docblock now says what the code does: rank 0 is a prefix of primary, not
'a prefix of its id or name'.

Generated-by: Devin

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@Astro-Han
Astro-Han merged commit 8bde344 into apache:main Sep 21, 2026
2 checks passed
@Astro-Han
Astro-Han deleted the fix/skill-pill-inline-rendering branch September 21, 2026 16:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/L Under 1000 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants