uttrflow / uttrflow/uttrflow-swift

Stale AI suggestions: the ghost is not withdrawn when the caret moves, and an answer for an older read is still drawn

Open
#549 0 comments 0 reactions 1 assignee Claimed by @naveen-bhatt View on GitHub
area:ai-suggestions bug P1
Dominant language
Swift
Stars
4
Forks
17
Avg merge
3h 32m
Merged PRs (30d)
277

Description

Seen in the released build 2026.9.14: AI suggestions go stale. A ghost stays on screen for text that no longer matches what is typed, drawn at a caret position the line has moved past.

## Root causes

1. **Nothing hides the ghost when the caret moves.** `keyPressed` (`Sources/Uttrflow/Suggestion/SuggestionCoordinator.swift:217-228`) cancels the pass in flight but leaves the panel where it is; the click monitor (`:182-186`) only wakes a turn. There is no scroll monitor and no Space-change observer at all, and the panel joins all Spaces. So the old ghost stays at the old caret while the user types, deletes, clicks, scrolls or changes Space, until some later turn happens to redraw or hide it.
2. **A late answer is drawn after the user typed on.** `SuggestionSession.resolve` (both overloads) and `resolveGenerated` (`Sources/UttrflowPredict/SuggestionSession.swift:205, 228, 244`) check only the turn's generation. A key that arrives while a turn is waiting on the corpus, the gates or the model does not start a new generation (the turn gate is busy, so the key's turn is queued), so the answer for the older line is drawn anyway, against the field read at the turn's start (`SuggestionCoordinator.swift:329-345`). The session already knows the count moved (`keystrokes` vs `pendingKeystroke`) and uses it only to stop Tab (#225), not to stop drawing. The test `aReadThatMissedTheKeystrokeIsStale` asserts the stale offer is drawn.
3. **`drawFresh` re-reads the field but ignores keys that arrived during the read** (`SuggestionCoordinator.swift:398-406`).
4. **A model line that differs from the typing only in case is drawn as a replacement.** `drawable` (`SuggestionSession.swift:275-281`) matches the line case-insensitively but keeps the model's casing, so a line typed with a capital that the model returns in lower case draws the whole typed line struck through and repeated after the caret, and Tab re-cases what the user wrote.

## Expected

A ghost is withdrawn by every event that may move the caret (key, click, scroll, app switch, Space change), and no result whose field read is older than the latest such event is ever drawn. A generated line keeps the typed text's own spelling.

Related: #225, #58.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.