From 4b5b7830d0a3efc627c63644bf76e6aec68c4e95 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sat, 3 Oct 2026 02:08:27 +0800 Subject: [PATCH] fix(mobile): another agent waiting is not missed, and dismissing is only that - The card for another agent that needs you stays until it is answered or put away (it went after 15 seconds); a finished one still goes on its own. While any other agent waits, the back button carries a dot. - The card's dismiss, a banner's action and Jump to latest took themselves away under the finger, and the same tap then reached the pane and brought up its keyboard; a clear cover takes that tap now. - A row's Current tag no longer gets cut to 'C...' by a long name. --- mobile/src/main.ts | 66 ++++++++++++++++++++++++++++++++++++++------ mobile/src/style.css | 65 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 9 deletions(-) diff --git a/mobile/src/main.ts b/mobile/src/main.ts index bcb2fc6d..8fa2d75d 100644 --- a/mobile/src/main.ts +++ b/mobile/src/main.ts @@ -2116,8 +2116,8 @@ function paneRow(host: Host, place: Place, ws: WorkspaceView, tab: TabView, pane { class: "row-text" }, h( "span", - { class: "row-title" }, - name, + { class: "row-title tagged" }, + h("span", { class: "row-name" }, name), // The tab in front on the desktop: where you were. current && h("span", { class: "tag current" }, "Current"), tab.hibernated && h("span", { class: "tag" }, "Asleep"), @@ -2610,7 +2610,7 @@ function terminalScreen(host: Host, place: Place, pane: PaneView, title: string, const bar = h( "header", { class: "term-nav" }, - h("button", { class: "round", ariaLabel: `Back to ${host.name}`, onclick: back }, ico("back")), + h("button", { class: "round term-back", ariaLabel: `Back to ${host.name}`, onclick: back }, ico("back")), h("div", { class: "term-titles" }, h("span", { class: "term-title" }, title), sub), menu, ); @@ -3444,7 +3444,9 @@ function terminalScreen(host: Host, place: Place, pane: PaneView, title: string, const buf = term.buffer.active; latest.hidden = buf.baseY - buf.viewportY < 3; }); + latest.onpointerdown = (e) => e.preventDefault(); latest.onclick = () => { + shield(); feel("tick"); setFrac(0); term.scrollToBottom(); @@ -3858,13 +3860,33 @@ function terminalScreen(host: Host, place: Place, pane: PaneView, title: string, readChoices(); }); }); + // A button over the pane that takes itself away when tapped leaves the + // tap to land again on the pane beneath, which brings up its keyboard; + // a clear cover takes that tap instead, for a moment. + const shield = () => { + const cover = h("div", { class: "term-shield" }); + view.querySelector(".term-wrap")?.append(cover); + window.setTimeout(() => cover.remove(), 400); + }; const showBanner = (text: string, action?: { label: string; run: () => void }) => banner.replaceChildren( h( "div", { class: "term-banner" }, h("span", {}, text), - action && h("button", { class: "button tinted small", onclick: action.run }, action.label), + action && + h( + "button", + { + class: "button tinted small", + onpointerdown: (e: Event) => e.preventDefault(), + onclick: () => { + shield(); + action.run(); + }, + }, + action.label, + ), ), ); @@ -3995,14 +4017,33 @@ function terminalScreen(host: Host, place: Place, pane: PaneView, title: string, let others = new Map(); let heard = false; let peekTimer: number | undefined; + // The pane the card on screen is about, so it goes once that is settled. + let peekKey: string | null = null; const ownKey = `${place?.key ?? ""}/${pane.id}`; - const showPeek = (where: Place, ws: WorkspaceView, tab: TabView, other: PaneView) => { + const dropPeek = () => { + clearTimeout(peekTimer); + peekKey = null; + peek.replaceChildren(); + }; + const showPeek = (key: string, where: Place, ws: WorkspaceView, tab: TabView, other: PaneView) => { const agent = other.agent!; const name = /^[~/]/.test(tab.name) ? baseName(tab.name) : tab.name; + const close = h("button", { class: "peek-close", ariaLabel: "Dismiss" }, ico("close")); + close.onpointerdown = (e) => e.preventDefault(); + // Faded, then gone: taken away under the finger at once, the tap + // lands again on the pane beneath and brings up its keyboard. + close.onclick = (e) => { + e.stopPropagation(); + card.classList.add("leaving"); + peekKey = null; + clearTimeout(peekTimer); + peekTimer = window.setTimeout(dropPeek, 400); + }; const card = h( - "button", + "div", { class: `peek ${agent.status}`, + role: "button", onclick: () => terminalScreen(host, where, other, name, tabRef(ws, tab, name)), }, avatar(agent), @@ -4012,13 +4053,18 @@ function terminalScreen(host: Host, place: Place, pane: PaneView, title: string, h("span", { class: "peek-title" }, agent.status === "waiting" ? `${agentLook(agent.kind).name} needs you` : `${agentLook(agent.kind).name} is done`), h("span", { class: "peek-sub" }, agent.message || name), ), - ico("chevron", "icon row-chevron"), + close, ); peek.replaceChildren(card); + peekKey = key; feel("tick"); clearTimeout(peekTimer); - peekTimer = window.setTimeout(() => peek.replaceChildren(), 15_000); + // One waiting stays until it is answered or put away; one that is + // done is news for a moment. + if (agent.status !== "waiting") peekTimer = window.setTimeout(dropPeek, 15_000); }; + // While another agent waits, the way back carries a dot, card or no card. + const backButton = view.querySelector(".term-back"); let peeking: number | null = null; api .watch(host.id, (msg) => { @@ -4036,8 +4082,10 @@ function terminalScreen(host: Host, place: Place, pane: PaneView, title: string, now.set(key, other.agent.status); const was = others.get(key); const news = other.agent.status === "waiting" || other.agent.status === "done"; - if (heard && key !== ownKey && news && was !== other.agent.status) showPeek(where, ws, tab, other); + if (heard && key !== ownKey && news && was !== other.agent.status) showPeek(key, where, ws, tab, other); } + if (peekKey && now.get(peekKey) === "working") dropPeek(); + backButton?.classList.toggle("attention", [...now].some(([key, status]) => key !== ownKey && status === "waiting")); others = now; heard = true; }) diff --git a/mobile/src/style.css b/mobile/src/style.css index 7f3e9247..f89932fc 100644 --- a/mobile/src/style.css +++ b/mobile/src/style.css @@ -2756,3 +2756,68 @@ html[data-nav="pop"]::view-transition-new(root) { opacity: 0.35; pointer-events: none; } + +.peek-close { + flex: none; + width: 30px; + height: 30px; + border: 0; + border-radius: 15px; + display: grid; + place-items: center; + background: var(--fill); + color: var(--ink-2); +} + +.peek-close .icon { + width: 14px; + height: 14px; +} + +/* Another agent is waiting: a dot on the way back to the list. */ +.term-back { + position: relative; +} + +.term-back.attention::after { + content: ""; + position: absolute; + top: 2px; + right: 2px; + width: 9px; + height: 9px; + border-radius: 50%; + background: var(--waiting); + box-shadow: 0 0 0 2px var(--canvas); +} + +/* A row's name gives way before its tags do: "Current" stays whole. */ +.row-title.tagged { + display: flex; + align-items: center; +} + +.row-title.tagged .row-name { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.row-title.tagged .tag { + flex: none; +} + +.peek.leaving { + opacity: 0; + transform: translateY(-8px); + transition: + opacity 0.2s, + transform 0.2s; +} + +.term-shield { + position: absolute; + inset: 0; + z-index: 8; +}