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.
This commit is contained in:
l0ng-ai
2026-10-03 20:27:44 +08:00
parent 9860d44fcc
commit 4b5b7830d0
2 changed files with 122 additions and 9 deletions
+57 -9
View File
@@ -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<string, AgentStatus>();
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<HTMLElement>(".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;
})
+65
View File
@@ -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;
}