Skip to content
Closed
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
19 changes: 10 additions & 9 deletions docs/VERIFICATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ proves recall degrades to recency when embeddings are unavailable. `npm run veri
narration containing that value.
`npm run verify:packaged-natural-memory-turn` also keeps live Ollama enabled, teaches a stated preference through
packaged `turnRun`, waits for the extracted profile fact, relaunches, asks about it through a later turn, and asserts
the floating Ask shows NO success banner after recall (the receipt state is empty) while a memory-status beat proves the
the floating Ask shows a brief `done` nod after recall (the receipt state reads `done`) while a memory-status beat proves the
recall used memory. The natural-language packaged smoke uses the floating Ask smoke hook to inspect receipt state, but it
does not launch the packaged app in floating-window mode or prove the failure receipt is visibly rendered; use
`npm run verify:floating-live-turn` for the visible floating Ask path.
Expand Down Expand Up @@ -408,16 +408,17 @@ It is opt-in (needs a display + a vite build) and not in CI. Checks: `#floating-
`collapsed`; pill reads "Ask Roro…"; `#floating-stop` exists and is not `armed`; clicking the pill →
`expanded` with the input actually visible; the memory profile bridge responds before teardown; Escape
→ `collapsed`; smoke submit → `tasked` with trimmed pill copy and visible Stop before `run.started`;
pre-run Stop calls the no-id cancel path and shows neutral `Stopped.` copy; universal `runEnd`
collapses answer/clarify turns with no `run.started` and leaves NO receipt on success (the surface stays clean); `run.started` keeps Stop visibly armed and
pre-run Stop calls the no-id cancel path and shows neutral `Stopped.` copy; a tasked turn shows a live
`thinking…` progress line; universal `runEnd` collapses answer/clarify turns with no `run.started` and shows a
brief `done` nod that fades; `run.started` keeps Stop visibly armed and
targets the captured run id; real `run.failed` disarms Stop, shows actionable error copy, hides raw
spawn text, and the error remains visible after `runEnd` collapse until the next summon clears it.

`scripts/smoke-floating-live-turn.mjs` is the opt-in live counterpart. It launches the dev app in
floating mode, leaves `RORO_FLOATING_SMOKE` and `RORO_DEBUG_BRIDGE` off, drives the visible Ask form,
and verifies real `window.companion.turnRun` answer and executor turns over the public push stream.
The answer turn must emit `ActionEvent`s, reach `runEnd`, avoid the coding executor, and collapse the
floating Ask with NO success receipt (a completed turn shows no banner; memory use is proven by the status beat, not a receipt). In the
floating Ask with a brief `done` nod (an answer turn changes no files; memory use is proven by the status beat). In the
deterministic fake-Ollama path, the answer narration must include the requested phrase exactly enough
to prove the public event stream carried the model answer. In the optional real-Ollama path, the
narration check accepts a substantive non-placeholder answer because that mode validates the local
Expand All @@ -429,8 +430,8 @@ never complete/write the aborted file, collapse from `runEnd`, and show neutral
following executor success turn must launch Codex with the expected
`exec --json --skip-git-repo-check -s workspace-write -C <project>` shape, keep Stop armed for the
accepted/running turn, emit a completed `file_change` and `run.completed`, write inside the selected
test project (disposable by default), disarm Stop, collapse from `runEnd`, and show NO success receipt
(the surface stays clean on success). By default it uses tiny
test project (disposable by default), disarm Stop, collapse from `runEnd`, and show a brief `done · N files`
nod that fades. By default it uses tiny
local fake Ollama and Codex servers/binaries so the product-loop proof is deterministic; set
`RORO_FLOATING_LIVE_USE_REAL_OLLAMA=1` when you specifically want to validate the local model path.
Ignored-`SIGTERM` slot retention remains covered by the unit gate above, not this live smoke. Keep it out
Expand All @@ -446,10 +447,10 @@ Start the app (`ollama serve` first if you want a real turn): `npm start`.
| 2 | Click the pill (or ⌘⇧Space) | Input expands and is focused; pill chrome hidden |
| 3 | Press Enter on an **empty** input | Nothing happens — no thinking flash, stays expanded |
| 4 | Type "add a logout route" + Enter | After readiness accepts the turn, the cat snaps to *thinking*, the pill shows `tasked: add a logout route`, and Stop appears before any executor event |
| 5 | While a `run_agent` turn runs | `#floating-stop` remains visible with the `armed` class |
| 5 | While a `run_agent` turn runs | `#floating-stop` stays visible with the `armed` class; the status line reads a calm `working…` the whole time (never blank) |
| 6 | Click Stop | Run cancels; Stop shows `Stopping...`, then disarms/hides; Ask collapses and shows neutral `Stopped.` copy |
| 7 | An answer/clarify turn (no executor run) | Ask still collapses when the turn ends (universal `runEnd`); no success banner is shown (the cat's animation conveys "done") |
| 8 | A successful executor turn completes | Ask collapses and Stop disarms; no success banner is shown. Only a failure (sticky red) or a Stop (neutral `Stopped.`) leaves a receipt |
| 7 | An answer/clarify turn (no executor run) | Ask collapses when the turn ends (universal `runEnd`) and shows a brief `done` nod that fades after a few seconds |
| 8 | A successful executor turn completes | Ask collapses and Stop disarms; a brief `done · N files` nod appears, then fades. Only a failure (sticky red) lingers |
| 9 | Press Esc while expanded | Collapses back to the "Ask Roro…" pill |

A CSS regression shows up as: a state that doesn't visually change (e.g. input stays hidden when
Expand Down
3 changes: 2 additions & 1 deletion scripts/smoke-floating-ask.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -208,11 +208,12 @@ try {
await evalJs(`window.__roroFloatingAskSmoke.startTask('answer without an executor')`);
await sleep(100);
check('#floating-stop arms for accepted answer/clarify turn', await evalJs(`document.getElementById('floating-stop').classList.contains('armed')`));
check('#floating-error shows a live "thinking…" progress line while tasked', await evalJs(`document.getElementById('floating-error').textContent === 'thinking…' && document.getElementById('floating-error').classList.contains('progress') && !document.getElementById('floating-error').hidden`));
await evalJs(`window.__roroFloatingAskSmoke.runEnd()`);
await sleep(100);
check('#floating-ask collapses on successful runEnd without run.started', await evalJs(`document.getElementById('floating-ask').classList.contains('collapsed')`));
check('#floating-stop hides after successful runEnd without run.started', !(await isVisible('#floating-stop')));
check('#floating-error stays HIDDEN after successful runEnd (success shows no banner)', await evalJs(`document.getElementById('floating-error').hidden === true && !document.getElementById('floating-error').classList.contains('success')`));
check('#floating-error shows a brief "done" nod after successful runEnd', await evalJs(`document.getElementById('floating-error').textContent === 'done' && document.getElementById('floating-error').classList.contains('success') && !document.getElementById('floating-error').hidden`));

console.log('[smoke] asserting executor Stop targets run id after run.started…');
await evalJs(`document.getElementById('ask-pill').click()`);
Expand Down
10 changes: 4 additions & 6 deletions scripts/smoke-floating-live-turn.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1007,9 +1007,8 @@ try {
const el = document.getElementById('floating-error');
return { hidden: el?.hidden, text: el?.textContent ?? '', success: el?.classList.contains('success') ?? false };
})()`, {}, 'answer receipt check');
check('floating receipt stays HIDDEN for successful answer turn (no success banner)', answerReceipt.hidden === true, JSON.stringify(answerReceipt));
check('floating receipt has no success tone for answer turn', answerReceipt.success === false, JSON.stringify(answerReceipt));
check('floating receipt shows no text for successful answer turn', answerReceipt.text === '', JSON.stringify(answerReceipt));
check('floating Ask shows a brief "done" nod for successful answer turn', answerReceipt.hidden === false && answerReceipt.success === true, JSON.stringify(answerReceipt));
check('floating answer nod reads "done" (an answer turn changes no files)', answerReceipt.text === 'done', JSON.stringify(answerReceipt));

if (!USE_REAL_OLLAMA) {
const stoppedTranscript = `${STOP_TRANSCRIPT}. Start a coding task that should be stopped before the executor starts.`;
Expand Down Expand Up @@ -1310,9 +1309,8 @@ try {
const el = document.getElementById('floating-error');
return { hidden: el?.hidden, text: el?.textContent ?? '', success: el?.classList.contains('success') ?? false };
})()`, {}, 'executor receipt check');
check('floating receipt stays HIDDEN for successful executor turn (no success banner)', executorReceipt.hidden === true, JSON.stringify(executorReceipt));
check('floating receipt has no success tone for executor turn', executorReceipt.success === false, JSON.stringify(executorReceipt));
check('floating receipt shows no text for successful executor turn', executorReceipt.text === '', JSON.stringify(executorReceipt));
check('floating Ask shows a "done" nod for successful executor turn', executorReceipt.hidden === false && executorReceipt.success === true, JSON.stringify(executorReceipt));
check('floating executor nod reports the changed-file count', /^done · \d+ files?$/.test(executorReceipt.text), JSON.stringify(executorReceipt));
const codexInvocations = await readFile(fakeCodexArgsFile, 'utf8')
.then((text) => JSON.parse(text))
.catch(() => []);
Expand Down
4 changes: 2 additions & 2 deletions scripts/smoke-packaged-memory.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1028,8 +1028,8 @@ try {
check('natural-language recall turn completed with runEnd', natural?.ok === true && Boolean(natural.runEnd), natural?.message);
check('natural-language recall runEnd matches turnRun result', natural?.runEnd?.runId === natural?.turnResult?.runId);
check(
'natural-language floating Ask shows NO success banner after recall (memory use proven by the status beat below)',
natural?.receiptText === '',
'natural-language floating Ask shows a brief "done" nod after recall (memory use proven by the status beat below)',
natural?.receiptText === 'done',
natural?.receiptText || JSON.stringify(natural?.floatingState ?? {}),
);
check('natural-language recall emitted a memory status beat', Boolean(memoryStatus), JSON.stringify(naturalEvents));
Expand Down
7 changes: 7 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,13 @@ body.floating-window #floating-error.success {
background: rgba(18, 78, 48, 0.9);
border-color: rgba(126, 230, 164, 0.5);
}
/* Live "roro is working" status — calm slate, clearly neither success (green) nor failure (red). */
body.floating-window #floating-error.progress {
color: #eef2fb;
background: rgba(28, 36, 56, 0.86);
border-color: rgba(150, 175, 230, 0.4);
opacity: 0.92;
}
body.floating-window #floating-stop.armed + #floating-error:not([hidden]) {
bottom: 88px;
}
Expand Down
80 changes: 45 additions & 35 deletions src/renderer/ask/floatingAsk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,43 +179,40 @@ describe('floatingAsk shell (jsdom)', () => {
expect(h.stop.classList.contains('armed')).toBe(false);
});

it('shows NO receipt after a successful answer turn (the cat conveys "done", not a banner)', async () => {
it('shows "thinking…" while planning, then a brief "done" nod after a successful answer turn', async () => {
h.pill.click();
h.input.value = 'what did we decide?';
submit(h.form);
await flush();
expect(h.error.classList.contains('progress')).toBe(true); // live "I'm on it" while it works
expect(h.error.textContent).toBe('thinking…');
h.fireAction(memoryUsed);
h.fireRunEnd();
expect(h.form.classList.contains('collapsed')).toBe(true);
// Success leaves the surface clean — no lingering "Done." banner over the user's screen.
expect(h.error.hidden).toBe(true);
expect(h.error.classList.contains('success')).toBe(false);
expect(h.error.textContent).toBe('');
// A nod (not a lingering banner) — an answer turn changes no files.
expect(h.error.hidden).toBe(false);
expect(h.error.classList.contains('success')).toBe(true);
expect(h.error.classList.contains('progress')).toBe(false);
expect(h.error.textContent).toBe('done');
});

it('shows NO receipt after a successful executor turn (even with changed files + memory)', async () => {
it('shows a persistent "working…" line through a multi-event task (never a frozen gap)', async () => {
h.pill.click();
h.input.value = 'edit it';
h.input.value = 'build it';
submit(h.form);
await flush();
expect(h.error.textContent).toBe('thinking…');
h.fireAction(started);
expect(h.error.textContent).toBe('working…'); // run.started → sustained "working" feedback
// mid-task events keep the working line visible — the bug was it going blank for 30–90s.
h.fireAction({ kind: 'file_change', runId: 'r1', itemId: 'f1', status: 'completed', files: [{ path: 'a.ts', op: 'update' }], ts: 2 });
h.fireAction(memoryUsed);
h.fireAction({
kind: 'file_change',
runId: 'r1',
itemId: 'file-1',
status: 'completed',
files: [{ path: 'src/app.ts', op: 'update' }],
ts: 2,
});
h.fireAction({ kind: 'run.completed', runId: 'r1', ok: true, finalText: 'done', ts: 3 });
h.fireRunEnd();
expect(h.error.hidden).toBe(true);
expect(h.error.classList.contains('success')).toBe(false);
expect(h.error.textContent).toBe('');
expect(h.error.hidden).toBe(false);
expect(h.error.classList.contains('progress')).toBe(true);
expect(h.error.textContent).toBe('working…');
});

it('leaves no lingering receipt across two consecutive successful turns', async () => {
it('shows "done · 1 file" after a successful executor turn', async () => {
h.pill.click();
h.input.value = 'edit it';
submit(h.form);
Expand All @@ -231,20 +228,31 @@ describe('floatingAsk shell (jsdom)', () => {
ts: 2,
});
h.fireAction({ kind: 'run.completed', runId: 'r1', ok: true, finalText: 'done', ts: 3 });
h.fireRunEnd('r1');
expect(h.error.hidden).toBe(true); // first success: clean surface

h.pill.click();
h.input.value = 'what now?';
h.turnRun.mockResolvedValueOnce({ runId: 'r2' });
submit(h.form);
await flush();
h.fireRunEnd('r2');
h.fireRunEnd();
expect(h.error.hidden).toBe(false);
expect(h.error.classList.contains('success')).toBe(true);
expect(h.error.textContent).toBe('done · 1 file');
});

// Second success also leaves nothing — no banner from either turn lingers over the screen.
expect(h.error.hidden).toBe(true);
expect(h.error.classList.contains('success')).toBe(false);
expect(h.error.textContent).toBe('');
it('fades the "done" nod after a few seconds (a nod, not a lingering banner)', async () => {
vi.useFakeTimers();
try {
h.pill.click();
h.input.value = 'edit it';
submit(h.form);
await vi.advanceTimersByTimeAsync(5); // resolve the async submit (turnRun mock + flush)
h.fireAction(started);
h.fireAction({ kind: 'file_change', runId: 'r1', itemId: 'file-1', status: 'completed', files: [{ path: 'src/app.ts', op: 'update' }], ts: 2 });
h.fireAction({ kind: 'run.completed', runId: 'r1', ok: true, finalText: 'done', ts: 3 });
h.fireRunEnd('r1');
expect(h.error.textContent).toBe('done · 1 file'); // nod shown…
expect(h.error.hidden).toBe(false);
await vi.advanceTimersByTimeAsync(3500); // past DONE_NOD_MS
expect(h.error.hidden).toBe(true); // …then it fades on its own
expect(h.error.textContent).toBe('');
} finally {
vi.useRealTimers();
}
});

it('ignores unrelated floating events and runEnd signals once the accepted run id is known', async () => {
Expand All @@ -262,7 +270,9 @@ describe('floatingAsk shell (jsdom)', () => {
h.fireRunEnd('r1');

expect(h.form.classList.contains('collapsed')).toBe(true);
expect(h.error.hidden).toBe(true); // success → no banner; the unrelated 'other-run' status never surfaced
// Success nod for the real run; the unrelated 'other-run' run never drove the surface.
expect(h.error.classList.contains('success')).toBe(true);
expect(h.error.textContent).toBe('done'); // no file_change for r1 → plain 'done'
});

it('keeps actionable failure copy visible after runEnd collapses the Ask', async () => {
Expand Down
Loading
Loading