facebook / facebook/astryx

[Bug] ChatComposerInput: inline tokens are lost on any external value update (deserialize is declared but never called)

Open
#4,655 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
13.1k
Forks
1.1k
Avg merge
1d 14h
Merged PRs (30d)
669

Description

### Description

`ChatComposerTrigger` declares:

```ts
/**
* Parse serialized tokens back into rendered tokens.
* Used when loading a previous message for editing.
*/
deserialize?: (value: string) => ChatComposerToken | null;
```

Nothing calls it. Code search for `deserialize` in this repo returns two files, and in `packages/core/src/Chat/ChatComposerInput.tsx` the only occurrence is the declaration itself (line 139 on `main`). `useTriggerMenu.tsx` never references it.

The effect that syncs the controlled value does this:

```ts
// ChatComposerInput.tsx:420 (main)
editable.textContent = controlledValue;
```

That assignment removes every `[data-astryx-token]` span, and `cleanupPortals()` then drops the matching portals. A token is a DOM span plus a React portal, and the `value` string carries neither, so there is nothing left to rebuild from. Any genuine external `value` update turns every inline chip into its raw serialized text.

The paths that hit this are ordinary ones: session switching, draft restore, prompt-history recall, and the "loading a previous message for editing" case named in the prop's own doc comment.

It also defeats a feature that is on by default. `pasteAsToken` collapses a paste over 200 characters into a `{ value: , label: "N lines, M chars" }` badge. After an external `value` write that badge is gone and the full pasted text sits in the input, which is the flooding the feature was added to stop.

**Expected**: after an external `value` update, serialized tokens render as tokens again. That is what `deserialize` is for. The paste token needs an equivalent, since it belongs to no trigger.

**Actual**: they become plain text and stay that way. Retyping is the only way back.

### Reproduction

Default configuration, no custom trigger:

```tsx
function Demo() {
const [drafts, setDrafts] = useState({ a: '', b: '' });
const [active, setActive] = useState<'a' | 'b'>('a');
return (
<>
setDrafts((d) => ({ ...d, [active]: v }))}
/>
setActive('b')}>draft B
setActive('a')}>draft A

);
}
```

1. Focus the input and paste about 500 characters. It collapses to a `500 chars` badge.
2. Click "draft B", then "draft A".
3. The badge is gone and all 500 characters are in the input.

The same three steps with a `triggers` entry whose `onSelect` returns a `ChatComposerToken` lose the chip the same way, with or without `deserialize` supplied.

### Astryx Version

`@astryxdesign/core@0.2.0`, also checked against current `main`.

### Environment

Electron 43.1.1, macOS 15. Nothing here is engine-specific; the loss happens in the `textContent` assignment.

### Related

#2473 (`ChatComposerInput` internal-authoritative state model) is aimed at this exact line and this exact tension. Its API sketch is text-only (`setValue` / `insertText` / `getText`), so as written it would move the string write without bringing tokens back. Token round-tripping belongs inside that state model rather than in a patch after it. Filing this separately so the constraint is recorded either way.

Contributor guide

Open the contributing guide

Research direction

Start in packages/core/src/Chat/ChatComposerInput.tsx, at the deserialize declaration around line 139 and the controlled-value effect around line 420; compare its behavior with useTriggerMenu.tsx and the state-model discussion in #2473. Reproduce the draft-switching case with a paste token and a trigger token, then consider the design needed for external values to restore both token types without losing their portals.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
frontend
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.