fix(ui): fix Skill chip alignment, slash matching, IME commit and transcript rendering - #5555
Conversation
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
df1a7c6 to
5785e10
Compare
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
5785e10 to
804892c
Compare
jackwener
left a comment
There was a problem hiding this comment.
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.
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>
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.insertTokenanchors a chip withvertical-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)./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,wmatched 20 of them anddeputdebugbehind Skills that only mention the word.insertTokenleaves 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.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:
height: 1lh,align-items: center,vertical-align: top).middleignores 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.insertToken,expandToken, token paste,insertTextAtCursor,placeCaretAtEnd,ensureCaretInsideand the controlled-write caret restore, plus the composer's own caret-to-end. Tokens arecontenteditable="false", so a trailing chip is never entered.InlineReferenceTextis 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/corepatch, alongside the existing hunks; thesrc/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 buildandnode --test --test-concurrency=4 "dist/**/*.test.js": 562 tests, 561 pass. The one failure (composer-model-picker-recovery) fails identically onmain.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 thementionMatchRankcases inchat-input-behavior.test.ts.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) andproduct-attachments--skill-token-without-a-frozen-reference(no chip at all).ni→你after a chip producedProject 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, andbiome linton the changed files: clean. The seven stories that touch these surfaces were rendered and theirplayfunctions finish without errors.Composer: where the staged chip sits on the text line
Transcript: a chip drawn from the token
Review focus
insertToken,ChatTokenizedText,chatComposerSelection,ChatComposerInput), and the caret guard is duplicated in the composer's owncaretToContentEndbecause that helper has focus semantics Astryx's does not.InlineReferenceTextnow 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
Tool(s) and scope: Maka diagnosed the four defects, implemented the fixes and wrote the tests; every commit carries a
Generated-by: Makatrailer.Checklist
Does this PR entail a change in behavior?
Follow-up commits (review fixes)
819feeac7— transcript chips now render at grammar match positions:ChatTokenizedTextre-locates a token by its value, which chipped positions the grammar rejects (a/skill:xbeside 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./Chatbarrel re-exportsplaceCaretAtEnd, 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.mdgained the rationale + delete-when entries for the caret, chip-geometry and re-export hunks.f02bedad—mentionMatchRank's every-word-in-name tier was unreachable (the/query never contains whitespace); dropped, docblock corrected.3f2229542— the new row tests use the sharedtranscript-test-dommount instead of a third hand-rolled copy.