fix(omp): read the ghost by colour CHANGE, not by an unstyled draft - #142
Conversation
omp 18 paints the composer draft in an explicit theme foreground while the agent is working, so composerGhost — which claimed "the trailing coloured run that follows UNSTYLED text" — found no anchor, left the inline suggestion in the draft, and every reply to a busy pane stalled again with "Message did not reach the input box". The rule is now relative on both sides: the ghost is the trailing run of segments sharing one foreground that DIFFERS from the text before it. Same refusals, all fail-closed — a row painted in one foreground end to end, a trailing run with no foreground, and a run with nothing but blanks before it all claim nothing.
Review raised the trailing-colour heuristic eating text the operator typed. Measured: omp paints its magic keywords as a per-character gradient, so a draft ending in one loses its LAST character — and the previous rule was worse here, taking all of `ultrathink` where this one takes `k`. It cannot move a send verdict: draftCarriesSend accepts any contiguous run of the draft's visible characters inside what was typed, so a contained draft stays contained when a character comes off the end. What it costs is the stranded-draft preview, one character. Tightening the other way — refusing a tail that changes colour more than once — puts every @mention- and placeholder-ending draft back into the permanent stall, so it is rejected on purpose and the trade is now a pinned test rather than an accident. Also: retarget the new fixture's welcome banner to the version it represents (same length, no cell moved) and correct the all-LF list in the fixture README.
Review response: the over-claim is real, its harm chain is not, and the shipped rule was worseA reviewer pass raised one P1 — "a partial pane read ending at a coloured typed token produces a shortened draft; 1. The decoration is real. omp 18 paints its magic keywords ( 2. It cannot move a send verdict. Over-claiming shortens a string that was already contained. It cannot turn a refusal into an acceptance for a partial read, because there was no refusal to turn. The only way stripping flips 3. The shipped rule is worse in exactly this case. Both rules, same row, an unstyled draft ending in a gradient-painted keyword:
So merging reduces this exposure from ten characters to one; declining keeps the larger one and keeps every busy pane stalled. 4. What it costs, and why I did not tighten further. The residual cost is the stranded-draft preview: "Take over" can hand back a draft one character short. The obvious tightening — refuse a tail that changes colour more than once — kills the gradient case, but it also refuses every Pushed
Two smaller review notes were correct and are fixed in the same commit; the remaining ones (ANSI |
|
Thank you — the live capture of the omp 18.0.11 bottom border is exactly the evidence this needed, and reading the ghost by colour change rather than by an unstyled anchor is the more durable rule. Appreciated you also pinning the bounded over-claim and saying what it costs. Verified against |
Brings #140, #141, #142 and #121 across. Two of them needed a hand on the way in, because git merged the text and not the meaning: · codex/markers.ts — #141 was written against main's `CONTROL_CHARS` regex. v1 replaced that constant with `hasControlChar()`, which scans by code unit the way links.ts does. The merge took #141's new `isDimSuffixFieldSegment` verbatim, so it called a name v1 had deleted. Swapped to the function; the two are byte-equivalent, tab exempted on both sides. · omp/markers.test.ts — #142's new cases bind `const row`, and this file has a module-level `row(name, i)` helper. main has no lint gate so it passed there; v1's oxlint calls it no-shadow, and it is right to. Renamed to `borderRow`, which is what v1's own version of the conflicted case already used. The one textual conflict was the same assertion under two names. Took main's "painted in one foreground end to end" — after #142 the rule reads a colour CHANGE, so v1's older "no unstyled text to anchor against" no longer describes what is being tested.
fix(omp): read the ghost by colour CHANGE, not by an unstyled draft
Brings #140, #141, #142 and #121 across. Two of them needed a hand on the way in, because git merged the text and not the meaning: · codex/markers.ts — #141 was written against main's `CONTROL_CHARS` regex. v1 replaced that constant with `hasControlChar()`, which scans by code unit the way links.ts does. The merge took #141's new `isDimSuffixFieldSegment` verbatim, so it called a name v1 had deleted. Swapped to the function; the two are byte-equivalent, tab exempted on both sides. · omp/markers.test.ts — #142's new cases bind `const row`, and this file has a module-level `row(name, i)` helper. main has no lint gate so it passed there; v1's oxlint calls it no-shadow, and it is right to. Renamed to `borderRow`, which is what v1's own version of the conflicted case already used. The one textual conflict was the same assertion under two names. Took main's "painted in one foreground end to end" — after #142 the rule reads a colour CHANGE, so v1's older "no unstyled text to anchor against" no longer describes what is being tested.
The stall is back on omp 18
#128 taught
composerGhostthat omp paints an inline completion suggestion after the operator's text, and that the suggestion is not in the input buffer. The rule it shipped was "the trailing run of COLOURED segments that follows UNSTYLED text", because on omp 17 the draft carried no foreground of its own.omp 18.0.11 paints the draft while the agent is working. Live capture of the bottom border, after
pane.send_textoflist the files in this repointo a busy pane (omp 18.0.11, 2026-08-30):The anchor is gone, so the rule refuses (fail-closed), the ghost stays in the draft, and
extractInputDraftreadslist the files in this report.draftCarriesSendrequires containment,reportis not contained, the submit key is withheld — and every reply to a busy omp pane stalls with "Message didn't reach the input box — a dialog may be waiting", exactly as before #128. An idle omp 18 pane still paints the draft unstyled, which is why this only bites the panes you most want to answer from your phone.The fix
Make the rule relative on both sides: a ghost is the trailing run of segments sharing one foreground that differs from the text before it. The draft's own colour is then whatever precedes the run — present or absent — and both shapes read the same.
The three refusals stay, all fail-closed, because a wrongly-claimed ghost shortens the draft the reply guard verifies:
Evidence
Measured with the production functions against the live captures above:
list the files in this report,draftCarriesSendfalselist the files in this repo, truelist the files in this repo, trueNew fixture
omp--draft-ghost-suggestion-busy.txt, derived fromomp--draft-ghost-suggestion.txtby splicing in the one SGR run omp 18 adds (no cell added or removed), filed into all three corpus lists.bun run testinweb/: the 22 harness suites pass (2500 tests). The 8 failing files on this machine are the pre-existinglocalStorage-in-runner ones and reproduce on a clean tree.Per the fork rule: no version bump, no CHANGELOG entry. Suggested line if you want one:
omp 18 paints the composer draft while working, so the inline-suggestion guard now reads the ghost by colour change rather than by an unstyled draft.