A message beside its open editor stands in the browser too (improvise-1x9h)
The toast hid every status after six seconds, so a refusal the user was still fixing vanished with its prompt open: su4u's bug on a timer. The browser now asks the TUI's question. EditorSession moves into improvise-protocol, ClientState records which editor a status was written in, and RenderModel::status_stands tells the toast to hold it until that editor closes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
+2
-2
@@ -112,7 +112,7 @@ const shell = createShell({
|
||||
// it stays reachable rather than being lost to the cap.
|
||||
el.status.title = text;
|
||||
},
|
||||
onModel: (text) => toasts.offer(text),
|
||||
onModel: (text, stands) => toasts.offer(text, stands),
|
||||
timeoutMs: HANDSHAKE_TIMEOUT_MS,
|
||||
});
|
||||
// The harness asks whether the watchdog is ARMED, because a negative assertion
|
||||
@@ -268,7 +268,7 @@ function repaint() {
|
||||
el.hint.textContent = model.hint;
|
||||
paintHelp(model);
|
||||
paintMinibuffer(model);
|
||||
shell.setModel(model.status);
|
||||
shell.setModel(model.status, model.status_stands);
|
||||
el.viewName.textContent = model.view_name;
|
||||
// The title bar, off the cache `src/draw.rs::draw_title` folds — one
|
||||
// derivation, both front ends (improvise-z754, improvise-5jna).
|
||||
|
||||
+18
-5
@@ -16,7 +16,10 @@ export const TOAST_MS = 6000;
|
||||
|
||||
/** `ViewState::status_msg` is STATE, not an event: a repaint re-offers the
|
||||
* string that is already on screen, so dismissal keys off message IDENTITY
|
||||
* and never off a timer alone. What that costs: context/code/clients.md. */
|
||||
* and never off a timer alone. What that costs: context/code/clients.md.
|
||||
*
|
||||
* `stands` is Rust's answer (`RenderModel::status_stands`): while it holds the
|
||||
* message is on no timer, and when it stops the message goes (improvise-1x9h). */
|
||||
export function createToasts({
|
||||
onShow,
|
||||
onHide,
|
||||
@@ -28,12 +31,21 @@ export function createToasts({
|
||||
// mechanism: a repaint offering it again is recognised and dropped.
|
||||
let offered = "";
|
||||
let timer = null;
|
||||
let standing = false;
|
||||
|
||||
return {
|
||||
offer(text) {
|
||||
offer(text, stands = false) {
|
||||
const next = text ?? "";
|
||||
if (next === offered) return;
|
||||
if (next === offered) {
|
||||
// Standing ends once: the editor it belonged to has closed.
|
||||
if (standing && !stands) {
|
||||
standing = false;
|
||||
onHide();
|
||||
}
|
||||
return;
|
||||
}
|
||||
offered = next;
|
||||
standing = next !== "" && stands;
|
||||
clearTimer(timer);
|
||||
timer = null;
|
||||
if (next === "") {
|
||||
@@ -41,6 +53,7 @@ export function createToasts({
|
||||
return;
|
||||
}
|
||||
onShow(next);
|
||||
if (standing) return;
|
||||
timer = setTimer(() => {
|
||||
timer = null;
|
||||
onHide();
|
||||
@@ -349,8 +362,8 @@ export function createShell({
|
||||
setShell(text) {
|
||||
onShell(text);
|
||||
},
|
||||
setModel(text) {
|
||||
onModel(text);
|
||||
setModel(text, stands = false) {
|
||||
onModel(text, stands);
|
||||
},
|
||||
/** A peer that never answers the handshake. Deliberately about the *first
|
||||
* frame*, not either peer's own failure modes — clients.md. */
|
||||
|
||||
@@ -125,6 +125,61 @@ test("a host that clears the status hides the toast without waiting", () => {
|
||||
assert.equal(clock.count(), 0, "a cleared message leaves no timer behind");
|
||||
});
|
||||
|
||||
/// improvise-1x9h: a message that stands beside an open editor is the user's
|
||||
/// until the editor closes, not the timer's. Evidence: clients.md.
|
||||
test("a standing message outlives the timer and goes with its editor", () => {
|
||||
const clock = fakeClock();
|
||||
const shown = [];
|
||||
let visible = false;
|
||||
const toasts = createToasts({
|
||||
onShow: (t) => {
|
||||
shown.push(t);
|
||||
visible = true;
|
||||
},
|
||||
onHide: () => {
|
||||
visible = false;
|
||||
},
|
||||
ttlMs: 6000,
|
||||
setTimer: clock.setTimer,
|
||||
clearTimer: clock.clearTimer,
|
||||
});
|
||||
|
||||
toasts.offer("Formula error", true);
|
||||
assert.equal(clock.count(), 0, "a standing message arms no timer");
|
||||
clock.fire();
|
||||
assert.equal(visible, true, "the refusal hid while its editor was open");
|
||||
toasts.offer("Formula error", true);
|
||||
assert.deepEqual(shown, ["Formula error"], "a repaint is not a second event");
|
||||
|
||||
toasts.offer("Formula error", false);
|
||||
assert.equal(visible, false, "the refusal outlived its editor");
|
||||
assert.equal(clock.count(), 0, "leaving hides now, not on a timer");
|
||||
toasts.offer("Formula error", true);
|
||||
assert.equal(visible, false, "a message that stopped standing came back");
|
||||
});
|
||||
|
||||
/// The other direction: a message that does not stand is on the timer exactly
|
||||
/// as before (improvise-1x9h).
|
||||
test("a message that does not stand keeps its timer", () => {
|
||||
const clock = fakeClock();
|
||||
let visible = false;
|
||||
const toasts = createToasts({
|
||||
onShow: () => {
|
||||
visible = true;
|
||||
},
|
||||
onHide: () => {
|
||||
visible = false;
|
||||
},
|
||||
ttlMs: 6000,
|
||||
setTimer: clock.setTimer,
|
||||
clearTimer: clock.clearTimer,
|
||||
});
|
||||
toasts.offer("Formula added", false);
|
||||
assert.equal(clock.count(), 1);
|
||||
clock.fire();
|
||||
assert.equal(visible, false);
|
||||
});
|
||||
|
||||
test("an empty status on the first frame shows nothing", () => {
|
||||
// The common case: every repaint before any command offers "". A feed that
|
||||
// treated the first offer as new would flash an empty toast on load.
|
||||
|
||||
Reference in New Issue
Block a user