uttrflow / uttrflow/uttrflow-swift
Each AI suggestion turn scans the caret's whole line several times on the main actor before refusing it as too long: 14 ms per keystroke at 100k characters, 78 ms for Devanagari
- Dominant language
- Swift
- Stars
- 4
- Forks
- 17
- Avg merge
- 3h 32m
- Merged PRs (30d)
- 277
Description
## What happens
On a long line, every AI suggestion turn does several full scans of the caret's line on the main actor, before the 256-character cap throws the line away.
- `FocusedFieldReader.snapshot` copies the field's whole `AXValue` on every turn, with no size cap (`Sources/UttrflowContext/FocusedFieldReader+System.swift:133`).
- `FocusedFieldSnapshot.currentLine` is a computed property. It converts the caret's UTF-16 offset to an index, scans back to the previous newline and copies the line (`Sources/UttrflowContext/FocusedFieldSnapshot.swift:103-109`, `:145-160`).
- `SuggestionCoordinator.turn`, which is `@MainActor`, evaluates it at least three times per turn: `:311`, `context(of:)` at `:721`, and `remember` → `reason.event(holding: snapshot.currentLine…)` at `:575`. It also calls `caretAtLineEnd` (`:721`, another offset conversion), and in `situation(of:)` (static on a `@MainActor` type) calls `preceding(maxLength:)` twice and `value.contains(where: \.isNewline)` over the whole value (`:528-537`).
- Only afterwards does `SuggestionSession.turn` refuse the line as `lineTooLong` (`Sources/UttrflowPredict/SuggestionSession.swift:186`).
## Measured
A release-built harness with these helpers copied verbatim from `f969b41`, on a string bridged through `NSString` as an Accessibility value arrives, caret at the end. "Per turn" = 3 × `currentLine` + `caretAtLineEnd` + 2 × `preceding` + one newline scan:
| Caret's line | currentLine | per turn |
|---|---|---|
| 10,000 UTF-16 units, Latin, one paragraph | 0.22 ms | 1.3 ms |
| 100,000 Latin, one line | 2.3 ms | 14 ms |
| 1,000,000 Latin, one line | 27 ms | 179 ms |
| 10,000 Devanagari, one paragraph | 1.5 ms | 8.7 ms |
| 100,000 Devanagari, one line | 13 ms | 78 ms |
| 1,000,000 Devanagari, one line | 537 ms | 2,155 ms |
The same text split into paragraphs costs microseconds. The cost is the length of the caret's own line, and it is several times worse for non-Latin text. The measurement is on this machine, so on an M1 it is roughly 1.5–2× these figures.
## Why it matters
A long paragraph without a hard return (Hindi prose, a chat message, a minified file, a long spreadsheet cell) costs milliseconds to seconds of main-actor time per keystroke. That can hitch Uttrflow's UI and delays every other turn. It is all spent on a line that is then refused.
## How to reproduce
The harness is a single Swift file: copy `line(of:endingAt:)`, `index(in:atUTF16Offset:)`, `caretAtLineEnd` and `preceding(maxLength:)`, build with `swiftc -O`, and time them on the strings above.
## Acceptance criteria
- The line is computed once per turn, and never on the main actor for more than `maximumTypedLength + 1` characters. For example, scan back at most 257 characters from the caret and report "too long" without copying the rest.
- The newline check in `situation(of:)` stops at the first newline.
- A test in `Tests/UttrflowContextTests` pins that a 1,000,000-unit single-line value yields `lineTooLong` with bounded work, for example by counting characters visited as `SecretShapes.tally` does.
Contributor guide
Research direction
Start with FocusedFieldReader.snapshot, FocusedFieldSnapshot.currentLine, SuggestionCoordinator.turn/context(of:)/situation(of:), and SuggestionSession.turn. Reproduce with the single-file harness using swiftc -O, then inspect Tests/UttrflowContextTests and SecretShapes.tally for the bounded-work test pattern. Done means one bounded line computation per turn, an early-stopping newline check, and a test showing a 1,000,000-unit line yields lineTooLong.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- swift
- Domain
- desktop, performance, testing-qa
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100