Skip to content
Merged
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
6 changes: 4 additions & 2 deletions web/src/fixtures/panes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ Capturing it is the first thing the later Tier-2 contribution owes, ahead of any
| `omp--done--tool-result.txt` | Completed turn ending in a boxed tool result (`╰───╯`, corner-to-corner) plus a `※ recap:` line. The negative control for the composer-bottom literal: this box closes with no gutter | `idle` |
| `omp--draft-single.txt` | A stranded draft that fits one row, written into the bottom border: `╰─ list the files in this repo ─╯` | `idle` |
| `omp--draft-ghost-suggestion.txt` | The same draft with omp's **inline completion suggestion** painted after it: `repo` unstyled, then `sitory` in a muted foreground, then the padding. The ghost is not in the input buffer, so `extractInputDraft` must read `list the files in this repo` — reading the row verbatim stalled every reply with "Message didn't reach the input box" (`composerGhost`, omp/markers.ts). **Derived** from `omp--draft-single.txt`: the SGR run and six ghost cells were spliced in and six padding cells taken out, so the row still measures 189 cells and every other byte is carried over | `idle` |
| `omp--draft-ghost-suggestion-busy.txt` | The same ghost on the shape **omp 18** draws while the agent is WORKING: the draft itself carries an explicit theme foreground (`38;2;242;244;248`), so the suggestion is no longer "colour after no colour". The first `composerGhost` rule anchored on the draft being unstyled, found no anchor here, and every busy pane went back to stalling. **Derived** from `omp--draft-ghost-suggestion.txt`: one SGR run spliced in before the draft, and the welcome banner's version retargeted `v17.2.12` → `v18.0.11` (same length), so no cell is added or removed | `idle` |
| `omp--draft-wrapped.txt` | A 355-char draft soft-wrapped over three rows — two `│ … │` continuations ABOVE the bottom border, which carries the tail (`hand`). Regression fixture for the fold direction | `idle` |
| `omp--menu-dismissed.txt` | The welcome panel (a 100-cell `╭───┴───╮` box) plus an MCP failure notice above an empty composer. Negative control: a second, narrower box on screen must not be spliced into the composer's geometry | `idle` |
| `omp--slash-palette.txt` | `/` typed: the autocomplete renders BELOW the box, at the box's own width, with one wrapped entry (3 rows) — a `skill:…` row, which omp assembles from the capturing machine and which is therefore NOT an omp built-in. `extractInputDraft` reads `"/"` | `idle` |
Expand Down Expand Up @@ -305,8 +306,9 @@ all-LF** — never mixed, never a lone `\r`, and none ends in a trailing newline
count always equals its `wc -l`, one FEWER than the rows it draws (27 CRLFs ⇒ 28 rows). The counts
below are that `wc -l`, i.e. what `grep -c` reports. Twelve are all-CRLF: `menu-dismissed` 27, `select-menu` and
`select-menu-moved` 55, `menu-model*` / `menu-resume*` / `menu-settings*` 56, `select-multi*` 58. The
other nine — `fresh-idle`, `working`, `done`, `done--tool-result`, `draft-single`,
`draft-ghost-suggestion`, `draft-wrapped`, `slash-palette` and `slash-palette--filtered` — are all-LF
other ten — `fresh-idle`, `working`, `done`, `done--tool-result`, `draft-single`,
`draft-ghost-suggestion`, `draft-ghost-suggestion-busy`, `draft-wrapped`, `slash-palette` and
`slash-palette--filtered` — are all-LF
with **zero**. The alternate screen is a
good guess at which is which but not a rule: `omp--menu-dismissed.txt` paints an ordinary inline
screen and is still all-CRLF, so re-measure rather than infer (`grep -c $'\r' <file>`). Any edit must
Expand Down
28 changes: 28 additions & 0 deletions web/src/fixtures/panes/omp--draft-ghost-suggestion-busy.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

╭─── omp v18.0.11 ─────────────────────────────────────────────────────────────────────────────────╮
│ │ Tips │
│ Welcome back! │ # for prompt actions │
│ │ / for commands │
│ ▀██████████▀ │ ! to run bash │
│ ╘██ ██ │ $ to run python │
│ ██ ██ │ ───────────────────────────────────────────────────────────────────── │
│ ██ ██ │ LSP Servers │
│ ▄██▄ ▄██▄ │ No LSP servers │
│ │ │
│ Auto │ │
│ vendor │ │
│ │ ───────────────────────────────────────────────────────────────────── │
│ │ Recent sessions │
│ │ No recent sessions │
│ │ │
│ │ │
│ │ │
│ │ │
╰──────────────────────────┴───────────────────────────────────────────────────────────────────────╯
Tip: `/shake` rips heavy tool results out of context to reclaim tokens without a full /compact —
 `/shake images` drops just images

✔ New session started

╭── π  > ⬢ Auto > 🗑 …abc-0123456789ab/scratchpad/omp-sandbox > ⑂ master > ◫ 11.5%/200K ⟲ > (sub) ▶──────────────────────────────────────────────────────────────────────────────────────────╮
╰─ list the files in this repository  ─╯
9 changes: 6 additions & 3 deletions web/src/lib/harness/omp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const DECLINED = [
"omp--done--tool-result.txt",
"omp--done.txt",
"omp--draft-ghost-suggestion.txt",
"omp--draft-ghost-suggestion-busy.txt",
"omp--draft-single.txt",
"omp--draft-wrapped.txt",
"omp--fresh-idle.txt",
Expand Down Expand Up @@ -81,7 +82,7 @@ const DECLINED = [

// Nothing is up-levelled, so there is no own cohort. `describeAdapterConformance` registers a todo for
// each leg that needs one rather than passing vacuously, and still runs the leg that matters here:
// raw-only on all 21 omp captures and all 38 claude ones.
// raw-only on all 22 omp captures and all 38 claude ones.
const ownFixtures: string[] = [];
const neutralFixtures = allOmpFixtures.filter((f) => DECLINED.includes(f));

Expand All @@ -98,6 +99,7 @@ describe("the omp corpus", () => {
const PINNED = [
"omp--done--tool-result.txt",
"omp--done.txt",
"omp--draft-ghost-suggestion-busy.txt",
"omp--draft-ghost-suggestion.txt",
"omp--draft-single.txt",
"omp--draft-wrapped.txt",
Expand All @@ -119,11 +121,11 @@ describe("the omp corpus", () => {
"omp--working.txt",
];

it("is exactly the 21 captures this adapter was developed against", () => {
it("is exactly the 22 captures this adapter was developed against", () => {
expect(allOmpFixtures).toEqual(PINNED);
});

it("declines all twenty-one — nothing is up-levelled", () => {
it("declines all twenty-two — nothing is up-levelled", () => {
expect(neutralFixtures).toEqual(PINNED);
expect(ownFixtures).toEqual([]);
});
Expand Down Expand Up @@ -167,6 +169,7 @@ const COMPOSER_FIXTURES = [
"omp--done--tool-result.txt",
"omp--done.txt",
"omp--draft-ghost-suggestion.txt",
"omp--draft-ghost-suggestion-busy.txt",
"omp--draft-single.txt",
"omp--draft-wrapped.txt",
"omp--fresh-idle.txt",
Expand Down
8 changes: 8 additions & 0 deletions web/src/lib/harness/omp/chrome.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const COMPOSER_FIXTURES = [
"omp--done--tool-result.txt",
"omp--done.txt",
"omp--draft-ghost-suggestion.txt",
"omp--draft-ghost-suggestion-busy.txt",
"omp--draft-single.txt",
"omp--draft-wrapped.txt",
"omp--fresh-idle.txt",
Expand All @@ -73,6 +74,9 @@ describe("locateComposer — the real corpus, pinned so any change to the walk s
// The same screen with omp's inline suggestion painted after the draft: the ghost changes what
// the row SAYS, never where the box is.
{ fixture: "omp--draft-ghost-suggestion.txt", top: 26, bottom: 27, suggestEnd: 28 },
// The same ghost again, on the shape omp 18 draws while the agent is WORKING: the draft carries an
// explicit foreground of its own, so the ghost is no longer "colour after no colour".
{ fixture: "omp--draft-ghost-suggestion-busy.txt", top: 26, bottom: 27, suggestEnd: 28 },
// A draft long enough to wrap: two continuation rows ABOVE the bottom border (omp folds the
// other way from Claude, which indents continuations BELOW its `❯` line).
{ fixture: "omp--draft-wrapped.txt", top: 26, bottom: 29, suggestEnd: 30 },
Expand Down Expand Up @@ -123,6 +127,10 @@ describe("extractInputDraft", () => {
// buffer, so it is not the draft: returning `list the files in this repository` here stalled
// every send with "Message didn't reach the input box" (markers.ts `composerGhost`).
{ fixture: "omp--draft-ghost-suggestion.txt", draft: "list the files in this repo" },
// The same row with the draft painted in omp 18's explicit foreground. The first ghost rule
// anchored on the draft being UNSTYLED and returned `list the files in this repository` here,
// which brought the stall back verbatim on every busy pane.
{ fixture: "omp--draft-ghost-suggestion-busy.txt", draft: "list the files in this repo" },
{
fixture: "omp--draft-wrapped.txt",
// Three fragments folded into one line: the two continuation rows, top-down, then the tail off
Expand Down
37 changes: 35 additions & 2 deletions web/src/lib/harness/omp/markers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest";

import { parseAnsi } from "../../ansi";
import { splitLines } from "../../blocks";
import { draftCarriesSend } from "../../reply-action";
import {
composerBottomText,
composerContText,
Expand Down Expand Up @@ -66,31 +67,63 @@ describe("composerBottomText", () => {
describe("composerGhost — omp's inline completion suggestion", () => {
const CORNER = "\x1b[38;2;74;80;88m";
const SUGGEST = "\x1b[38;2;111;115;119m";
const DRAFT = "\x1b[38;2;242;244;248m"; // omp 18 paints the draft itself while the agent is working
const PAD = " ".repeat(10);

it("claims the trailing coloured run that follows the operator's own text", () => {
const row = `${CORNER}╰─ \x1b[0mlist the files in this repo${SUGGEST}sitory\x1b[0m${PAD}${CORNER} ─╯\x1b[0m`;
expect(composerGhost(splitLines(parseAnsi(row))[0]!)).toBe("sitory");
});

it("reads it off the real capture, and leaves the plain one alone", () => {
// omp 18's busy shape, and the regression this rule was rewritten for: the draft is no longer
// unstyled, so "coloured run after unstyled text" saw nothing and the suggestion stayed in the
// draft. What separates the two runs is that their colours DIFFER, not that one of them is absent.
it("claims it just the same when the draft carries a foreground of its own", () => {
const row = `${CORNER}╰─ ${DRAFT}list the files in this repo${SUGGEST}sitory\x1b[0m${PAD}${CORNER} ─╯\x1b[0m`;
expect(composerGhost(splitLines(parseAnsi(row))[0]!)).toBe("sitory");
});

it("reads it off the real captures, and leaves the plain one alone", () => {
expect(composerGhost(styledRow("omp--draft-ghost-suggestion.txt", 27))).toBe("sitory");
expect(composerGhost(styledRow("omp--draft-ghost-suggestion-busy.txt", 27))).toBe("sitory");
expect(composerGhost(styledRow("omp--draft-single.txt", 27))).toBe("");
expect(composerGhost(styledRow("omp--fresh-idle.txt", 27))).toBe("");
});

// Both of these keep a real draft whole. A wrongly-claimed ghost SHORTENS the text the reply guard
// verifies, so every ambiguous row must claim nothing.
it("claims nothing when the row carries no unstyled text to anchor the run against", () => {
it("claims nothing when the row is painted in one foreground end to end", () => {
const row = `${SUGGEST}╰─ list the files in this repo${PAD} ─╯\x1b[0m`;
expect(composerGhost(splitLines(parseAnsi(row))[0]!)).toBe("");
});

it("claims nothing when the draft and the run share one colour", () => {
const row = `${CORNER}╰─ ${DRAFT}list the files in this repo${DRAFT}sitory\x1b[0m${PAD}${CORNER} ─╯\x1b[0m`;
expect(composerGhost(splitLines(parseAnsi(row))[0]!)).toBe("");
});

it("claims nothing when the coloured run is not at the end of the draft", () => {
const row = `${CORNER}╰─ \x1b[0m${SUGGEST}red\x1b[0m then plain text${PAD}${CORNER} ─╯\x1b[0m`;
expect(composerGhost(splitLines(parseAnsi(row))[0]!)).toBe("");
});

// The known, bounded over-claim, pinned so it stays a DECISION rather than an accident: omp paints
// its magic keywords as a per-character gradient, so a draft ending in one loses its last character
// to the rule. Two things this asserts alongside it — that the damage is one character and not the
// whole keyword (the previous rule took all of `ultrathink`), and that it cannot move the send
// guard's verdict, which is the property that makes the trade acceptable. See the header note.
it("takes one character off a draft ending in a gradient-painted keyword, and no send verdict with it", () => {
const gradient = [..."ultrathink"]
.map((c, i) => `\x1b[38;2;${100 + i * 10};${50 + i * 5};${200 - i * 7}m${c}`)
.join("");
const row = `${CORNER}╰─ ${DRAFT}please ${gradient}\x1b[0m${PAD}${CORNER} ─╯\x1b[0m`;
expect(composerGhost(splitLines(parseAnsi(row))[0]!)).toBe("k");

const sent = "please ultrathink";
expect(draftCarriesSend(sent, "please ultrathink")).toBe(true);
expect(draftCarriesSend(sent, "please ultrathin")).toBe(true); // shortened, same verdict
});

it("is not a border predicate — a row that is not the bottom border has no ghost", () => {
expect(composerGhost(splitLines(parseAnsi(`${SUGGEST}● Wrote the file\x1b[0m`))[0]!)).toBe("");
});
Expand Down
Loading
Loading