Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6142,10 +6142,22 @@ function ChatViewContent(props: ChatViewProps) {
},
}));
const snapshot = composerRef.current?.readSnapshot();
// Avoid ping-pong when skill picker inserts plain text while editor is still
// showing old value: the controlled effect (activePendingProgress.customAnswer
// -> composer) will rewrite editor to new value and place cursor. Calling
// focusAt now would move selection in old content (clamped) and fire
// handleEditorChange with old value, overwriting the just-inserted skill.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟑 Medium components/ChatView.tsx:6149

The isPlainInsert guard only recognises appended text (value.startsWith(snapshot.value)). When a skill picker replaces the typed query (e.g. $br β†’ $Browser ), isPlainInsert is false, so focusAt(nextCursor) fires against the editor's stale $br snapshot. That synchronously re-emits onChange with the old value, overwriting the pending-answer state update that was just queued β€” the selected skill never appears.

The comment above the guard already states the real invariant: skip focusAt whenever the editor shows a different value than the one being written, because the controlled effect will rewrite the editor. Broaden the check to snapshot.value !== value instead of the append-only heuristic.

πŸ€– Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/ChatView.tsx around line 6149:

The `isPlainInsert` guard only recognises appended text (`value.startsWith(snapshot.value)`). When a skill picker **replaces** the typed query (e.g. `$br` β†’ `$Browser `), `isPlainInsert` is `false`, so `focusAt(nextCursor)` fires against the editor's stale `$br` snapshot. That synchronously re-emits `onChange` with the old value, overwriting the pending-answer state update that was just queued β€” the selected skill never appears.

The comment above the guard already states the real invariant: skip `focusAt` whenever the editor shows a different value than the one being written, because the controlled effect will rewrite the editor. Broaden the check to `snapshot.value !== value` instead of the append-only heuristic.

const isPlainInsert =
snapshot !== undefined &&
typeof snapshot.value === "string" &&
value.length > snapshot.value.length &&
value.startsWith(snapshot.value);
Comment on lines +6150 to +6154

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

isPlainInsert only matches insertions appended to the end (value.startsWith(snapshot.value)). A skill inserted with the caret mid-answer produces a value that is not a prefix-extension, so focusAt still runs against the stale editor content and the ping-pong this guard describes can still overwrite the insertion. (typeof snapshot.value === "string" is also dead β€” value is typed string on the snapshot.)

Suggest keying off an explicit signal that the change came from a programmatic replacement (e.g. a flag threaded from applyPromptReplacement, or having the editor handle acknowledged controlled writes) rather than inferring it from string shape, so mid-text inserts are covered too.

Posted via Macroscope β€” UI Consistency


if (
snapshot?.value !== value ||
snapshot.cursor !== nextCursor ||
snapshot.expandedCursor !== expandedCursor
!isPlainInsert &&
(snapshot?.value !== value ||
snapshot.cursor !== nextCursor ||
snapshot.expandedCursor !== expandedCursor)
) {
composerRef.current?.focusAt(nextCursor);
}
Expand Down
Loading
Loading