From c216e287a42715b8999a3bd07550ecd3e2ccbc11 Mon Sep 17 00:00:00 2001 From: Minglong Pan <56749246+minglong51@users.noreply.github.com> Date: Tue, 25 Aug 2026 01:38:14 -0700 Subject: [PATCH] fix(ui): loud clipboard denial, mid-width kill verb, phone legibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From the 2026-08-24 dogfood walk (founder-agent-os/prototypes/ dogfood-reports-2026-08-24/agent-usage-manager.md): - bug #1: copyHint had no .catch on navigator.clipboard.writeText — a denied write (permission, unfocused document) failed silently and the operator walked away believing they held a bootout command they didn't. Now flashes "copy failed — press ⌘C" on the chip, then selects the command for a manual copy (an active selection also pauses auto-refresh, so it can't be yanked). Guards the mid-flash re-render race (el.isConnected) so the console stays clean. - bug #2: the 701–~1494px dead zone — the ~1.5kpx nowrap table scrolled the last column (the only kill verb) off-screen while the phone stack only engages ≤700px. The actions column (.c-act) is now position:sticky on the right above the phone breakpoint, right-aligned so the verbs hug the pinned edge (the nowrap launchctl hint inflates the column). Measured at 800px, unscrolled: 0/40 kill buttons visible before, 40/40 + 5/5 launchctl chips after. - F1: phone rows rendered unitless cpu/mem ("1.0 155") — .unit spans, hidden on desk (headers carry the units), shown in the phone stack. - F2: phone child rows lost the cmdline, their only identity — child .c-cmd stays visible in the phone stack, wrapped. - F3: the phone launchd hint ellipsed the job label (the one part that differs) — now truncates from the left (direction: rtl) at 240px, so "…501/ai.hermes.gateway-frontdoor" instead of "launchctl boot…". Out of scope (follow-up): F4, the chronic idle daemon pinned top-row — needs cmdline-granular idle_ok matching in app.py, a config-semantics change. Verified: 75 pytest pass (incl. new test_index_frontend_regressions); browse-verified against an isolated :8766 instance (live :8765 never touched, zero kill clicks): clipboard denial simulated by overriding writeText to reject (flash at t=1ms, restore+select at t=1503ms; success path still flashes "copied ✓"); sticky verbs reachable at 800px and 1440px; phone rows carry units + child cmdlines; hint chips show the job-label tail; console clean. LLD §6 line cites refreshed per the design-doc contract. --- agent_usage_manager/static/index.html | 74 ++++++++++++++++++++++----- docs/design/LLD.md | 37 +++++++++----- tests/test_smoke.py | 24 +++++++++ 3 files changed, 108 insertions(+), 27 deletions(-) diff --git a/agent_usage_manager/static/index.html b/agent_usage_manager/static/index.html index 43ff8da..3b87728 100644 --- a/agent_usage_manager/static/index.html +++ b/agent_usage_manager/static/index.html @@ -27,6 +27,18 @@ td.cmd { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 380px; color: #8b949e; } td.num { text-align: right; font-variant-numeric: tabular-nums; } + /* The all-nowrap table measures ~1.5kpx wide, so between the phone stack + (≤700px) and a full-width window .wrap scrolls horizontally and the last + column — the only kill verb — sits off-screen. Pin the actions column to + the right edge so it's reachable at every width. The nowrap launchctl hint + inflates the column's width, so right-align too: the verbs hug the pinned + edge instead of the cell's left. Phone rows are stacked blocks with no + horizontal overflow, so this is desk/mid-width only. */ + @media (min-width: 701px) { + .c-act { position: sticky; right: 0; background: #0d1117; + border-left: 1px solid #21262d; text-align: right; } + tr.child td.c-act { background: #0b0f14; } + } .label { background: #1f6feb33; color: #79c0ff; padding: 1px 7px; border-radius: 10px; font-size: 12px; } .svc { background: #bb800933; color: #d29922; padding: 1px 7px; margin-left: 5px; @@ -44,6 +56,9 @@ .kids { color: #79c0ff; font-size: 11px; margin-left: 5px; cursor: pointer; border-bottom: 1px dotted #30363d; } .kids:hover { color: #a5d6ff; } + /* Units are hidden on desk — the column headers already say "cpu %"/"mem MB". + The phone stack drops the headers, so there the numbers carry their own. */ + .unit { display: none; } .spark { display: block; margin-top: 3px; } .spark polyline { fill: none; stroke: #d29922; stroke-width: 1.2; } .spark line { stroke: #21262d; stroke-width: 1; } @@ -72,11 +87,19 @@ @media (max-width: 700px) { th, td { padding: 6px 8px; } .c-status, .c-up, .c-cmd, .c-pid { display: none; } /* keep agent/cpu/mem/actions */ + /* …but a child row's cmdline IS its identity (on desk it's what tells the + zsh wrapper from the helper from the MCP server) — keep it, wrapped. */ + tr.child td.c-cmd { display: block; max-width: none; white-space: normal; + overflow-wrap: anywhere; } + .unit { display: inline; } /* headers are gone below — numbers self-label */ header { padding: 10px 12px; gap: 10px; } #stale { padding: 6px 12px; } - /* visually truncate the launchctl hint; click-to-copy still copies the full text */ - code.hint { display: inline-block; max-width: 110px; overflow: hidden; - text-overflow: ellipsis; vertical-align: middle; } + /* Truncate the launchctl hint from the LEFT: every supervised row starts + with the identical 'launchctl bootout gui/501/', so clipping the right + end renders every row's chip the same and ellipses the job label — the + one part that differs. Click-to-copy still copies the full text. */ + code.hint { display: inline-block; max-width: 240px; overflow: hidden; + text-overflow: ellipsis; vertical-align: middle; direction: rtl; } /* The flat table measures ~554px, so at 375px the kill verb sits off-screen. Stack each row instead — agent+badges / cpu·mem / actions — with column headers gone (they no longer map to anything) and the sparkline hidden @@ -109,7 +132,7 @@

agent usage manager

agentpidstatus cpu %mem MB gpu MBuptime - command + command @@ -234,21 +257,44 @@

agent usage manager

} function copyHint(el) { - navigator.clipboard?.writeText(el.textContent).then(() => { + // A denied clipboard write (permission prompt dismissed, unfocused document, + // no clipboard API) must not fail silently — the operator would walk away + // believing they hold a bootout command they don't have. Say so, then select + // the chip text so a manual ⌘C still works (an active selection also pauses + // auto-refresh — see refresh() — so the re-render can't yank it mid-copy). + const fail = () => { const prev = el.textContent; - el.textContent = "copied ✓"; - setTimeout(() => { el.textContent = prev; }, 1000); - }); + el.textContent = "copy failed — press ⌘C"; + setTimeout(() => { + // a refresh() re-render may have replaced the row mid-flash + if (!el.isConnected) return; + el.textContent = prev; + const range = document.createRange(); + range.selectNodeContents(el); + const sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + }, 1500); + }; + try { + const w = navigator.clipboard && navigator.clipboard.writeText(el.textContent); + if (!w) { fail(); return; } + w.then(() => { + const prev = el.textContent; + el.textContent = "copied ✓"; + setTimeout(() => { el.textContent = prev; }, 1000); + }).catch(fail); + } catch (e) { fail(); } } function childRows(tree) { return tree.filter(c => c.depth > 0).map(c => ` ↳ ${esc(c.name)} ${c.pid} - ${c.cpu_percent.toFixed(1)} - ${c.mem_mb.toFixed(0)} + ${c.cpu_percent.toFixed(1)}% + ${c.mem_mb.toFixed(0)} MB - ${esc(c.cmdline)} + ${esc(c.cmdline)} `).join(""); } @@ -312,12 +358,12 @@

agent usage manager

${esc(x.label)}${svc}${flag}${kids} ${x.pid} ${esc(x.status)} - ${x.cpu_percent.toFixed(1)}${spark(x.trend)} - ${x.mem_mb.toFixed(0)} + ${x.cpu_percent.toFixed(1)}%${spark(x.trend)} + ${x.mem_mb.toFixed(0)} MB ${x.gpu_mem_mb==null?'—':x.gpu_mem_mb.toFixed(0)} ${dur(x.uptime_s)} ${esc(x.cmdline)} - ${actions} + ${actions} `; } diff --git a/docs/design/LLD.md b/docs/design/LLD.md index c5de65e..0c33f14 100644 --- a/docs/design/LLD.md +++ b/docs/design/LLD.md @@ -1,6 +1,8 @@ # agent-usage-manager — Low-Level Design -**Refreshed:** 2026-08-19 (0.2.5 — `launchd_labels:`, `test-alert`, missing-config +**Refreshed:** 2026-08-25 (frontend dogfood fixes — clipboard-denial path, +sticky actions column, phone units/child-cmdline/left-truncated hint); +previously 2026-08-19 (0.2.5 — `launchd_labels:`, `test-alert`, missing-config surfacing, kill-confirm/token-prompt wording, `list` flags caveat). Code layout: one FastAPI module (`agent_usage_manager/app.py`), one CLI module @@ -314,18 +316,18 @@ Applies to every request: ## 6. Frontend (static/index.html) -Single inline `