design: multi-surface design foundation (DESIGN.md + CLAUDE.md) - #5
Merged
Conversation
…routing Reframe to human-sovereign / agent-delegated. Locks the design language grounded in REAL Linear/Conductor/Splits product shots (not prose): - two-signal actor model (amber=human, cyan=agent) + shape backup - IA: Conductor sidebar tree (Projects -> Wallets + Agents), 2-pane, contextual views; Splits-style wallet rows - color/type/spacing tokens (dark + light), components WITH states, trust affordances (clear-signing, seed reveal, network warning) - review-settled rules: neutral primary buttons (amber only for caution/where-you-are/hold/focus), thin threshold budget bars, whitespace grouping, circular status glyphs, money $/precision rules Agent registration / MCP model intentionally left open (noted in DESIGN.md). CLAUDE.md points all UI work at DESIGN.md and the grounding rule.
hellno
added a commit
that referenced
this pull request
Jun 19, 2026
…a blind-approve regression) A second adversarial pass over the prior fix commit (5734602) caught that two of those fixes introduced NEW bugs — exactly why fixes get the same scrutiny as features. 1. BLIND-APPROVE regression (HIGH, shell.rs). Fix #5's filter fallback turned a safe no-op into a real wrong-row resolve: when a review for card X is open and a background poll expires X, the render falls through to the feed (it's &self, can't clear activity_reviewing) with a DIFFERENT over-cap card Y highlighted. A one-key ⌘Enter then approved Y — whose clear-signing card was never shown — violating "no blind-approve of a spend" (the agent would then execute Y's overage). Real fix: clear `activity_reviewing` IN refresh_activity, atomically with the snapshot swap, when its id leaves the pending set. Now approve sees None → re-opens the highlighted row's review (no blind-approve), and deny targets the visible highlighted row (fail-safe, not the old no-op). 2. False-positive `expired` match (HIGH, demo-agent.sh). mcp_is_terminal_refusal grepped a BARE "expired", which also matches transient RPC errors ("connection expired", "session expired") embedded in broadcast_failed/signer_error prose — mis-classifying a transient failure as terminal and abandoning a still-valid card's funds. Anchored to the exact daemon copy ("this request expired", "a human denied this request"). Verified: terminal copies match, RPC errors don't. 3. Two-signal mismatch (MED, activity_view.rs). The amber LABEL was re-keyed to `human_acted` but the "→ You" actor-chain link still gated on bare `!auto_allowed`, so a STOP-revoked auto-allow showed the amber label with NO human in the chain. Re-keyed `needed_human` to `human_acted` too. 4. Propose-path baseline clarity (demo-agent.sh). `BASELINE_WEI=public_wei` is numerically the just-now propose-time balance (symmetric with try_execute's PENDING_BASE), but read as "absorb the whole wallet". Rewrote as `wei_add BASELINE_WEI surplus` (identical value) so the intent — "skip exactly the funds the daemon just refused" — is plain. Verified: fmt --check, just check (clippy -D, default + tray), cargo test --workspace (274 pass, 0 fail), shellcheck + bash -n, and a matcher-precision simulation.
hellno
added a commit
that referenced
this pull request
Jun 19, 2026
…nner (#61) + collapse/fold redesign (#67) * feat(signerd): surface agent over-cap proposals to the approval inbox Extend the wire contract + daemon so the GUI approval queue can render agent-initiated over-cap proposals, ordered and counted down by expiry. - contract: add ProposalOrigin{App,Agent}; carry it on SignerRequest::Propose; append remaining_ms + origin to PendingRecord (the inbox wire shape). - daemon: stamp each PendingReq with its origin; PendingList now calls expire_stale() BEFORE building records (expiry is daemon-authoritative, never a bypassable UI timer) and computes remaining_ms from expires_at (panic-free). - client: thread origin through propose / propose_blocking. - tests: new pending_list suite (origin round-trip, expire-before-list, remaining_ms==0 on terminal); update propose callers across the e2e suites. * feat(app): agent approval queue + Activity ledger (MVP) A top-level Approvals inbox and Activity ledger built on the daemon's PendingList. Money-safety: approval ALWAYS routes through the clear-signing review (no blind-approve from a list row); agent proposals are Resolve-only — the app never broadcasts, the agent executes its own write once Allowed. - approvals_view: dense soonest-to-expire queue (loading / empty / fail-loud / expired states) + the clear-signing review (agent band, danger-early, ⌘Enter approve / Esc cancel). - activity_view: terminal records as the two-actor "Atlas → You" chain, honestly labelled session-scoped (in-memory). - shell: Approvals + Activity surfaces, ⌘⇧A, a PendingList poller, and resolve-only approve/deny that is non-optimistic (re-fetch authoritative status on success, fail loud on error). - shell_chrome: sidebar Approvals entry with an amber pending-count badge, plus an Activity entry. - palette: Approvals / Activity / Approve / Deny commands. signer: resolve_blocking. * feat(mcp): tag agent proposals + point approval copy at the queue - sidecar: propose with ProposalOrigin::Agent so the queue attributes the agent (Atlas) versus a human-composed action. - copy: the needs-approval guidance (sidecar / server / failure) no longer says "the approval UI is not in this alpha" — it now sends the human to the Deckard app's Approvals queue (⌘⇧A) + clear-signing review card. - acceptance: assert the shield tool docs carry the "Approvals queue" keyword. - common: ignore origin when destructuring Propose in the mock. * feat: the first agent demo — see-and-stop Activity feed (#60) + headless runner (#61) + Superhuman collapse/fold redesign #60 — the Activity FEED (not just an inbox): a daemon-side ActivityRecord ledger (additive, byte-stable wire) that retains auto-allowed + executed agent actions (tx_hash, timestamp, the ACTUAL breached cap — per-tx vs daily, daemon-computed, no hardcoded lie), an honest auto_allowed flag (hands-free vs human-approved), and expire-on-read. App: a Superhuman-style inbox — a "NEEDS YOU" triage band you clear to "All clear" + a settled LOG, inline clear-signing review, a STOP control, two-signal fidelity (muted label / glyph carries color / amber on rows you acted on), and the shield payoff made visible ("moved X ETH to your private balance"). #61 — a self-contained headless runner (scripts/demo-agent.sh + just demo-vault/ demo-deposit/demo-agent) that drives the same 6 key-less MCP tools in a poll -> shield -> execute loop (idempotent ratchet, over-cap re-poll, STOP/locked detection, mainnet refusal). No new authority, no new wire. The Claude-Desktop prompt doc stays as the narrative version. Redesign (post-review): collapsed the redundant Approvals surface into the feed and deleted it; deleted the demo "agent acting" toggle + breathing pulse (the real runner replaces them); folded the "Agent" sidebar entity INTO the Wallet (Atlas is key-less automation on the same EOA, not a separate account) — the policy fence now lives in the wallet cockpit. Invariants held: agent stays key-less; ONE approval path (Resolve over the control channel, no Execute on approve, no blind-approve); STOP zeroizes + denies in-flight; the verdict path is frozen; wire is additive + byte-stable; never signs chain_id=0. Verification: cargo fmt clean; clippy -D warnings clean (default + tray); 245 workspace tests; shellcheck clean. Reviewed by a multi-agent workflow + codex (static); all confirmed findings fixed (notably the CRITICAL blind-approve path and a policy-fence honesty bug after STOP). * docs: first-agent-demo screenshots (driving the real app) * fix: pin the Activity STOP control + make the demo hands-free (clear inherited EIP-7702 delegation) UX (#2): the feed heading + STOP control are now PINNED outside the scroll region, so the panic brake can never scroll off screen — only the body (needs-you + log) scrolls beneath it. (The header was inside the scroll container before; in a normal-width window STOP showed, but it could scroll away — and a safety control shouldn't.) Demo (#4 root cause): `just demo-deposit` now clears any inherited EIP-7702 delegation on the demo wallet before funding it. The demo wallet is anvil's PUBLIC account-0 key, and on the forked Sepolia someone had 7702-delegated that address (its on-chain code = 0xef0100<delegate>). A plain ETH transfer to a 7702-delegated account runs the DELEGATE contract instead of crediting the EOA, so the balance never rose and the agent's deposit-delta detector never fired (shields, which only SIGN, still worked — which is why it looked half-broken). Clearing it back to a plain EOA makes the loop fully hands-free: deposit arrives -> Atlas notices -> shields -> broadcasts, no manual MCP commands. Verified end-to-end against the live app + runner. Plus the new STOP-flow screenshots (pinned STOP, armed "Confirm STOP", the revoked banner). fmt + clippy (default + tray) + 245 workspace tests + shellcheck all green. * fix(swap): tag GUI swap orders App-origin so the feed doesn't label a human swap "Atlas" Codex/two-signal finding: a user-initiated GUI swap produced two feed rows with MISMATCHED actors — the order row hardcoded ProposalOrigin::Agent (cyan "Atlas wants to swap…") while its companion exact-gross approve was correctly App (amber "You…"). A foreground human action narrated as the agent's action is exactly the trust-signal confusion the two-signal model exists to prevent. propose_order is only ever called from the app, so every order in the feed was mislabeled; the Swap surface is unflagged + user-reachable, so it's user-visible on Sepolia/mainnet. Fix (symmetric to Propose, which already carries origin): SignerRequest::ProposeOrder gains an additive `origin` field, threaded through Daemon::propose_order + SignerClient::propose_order[_blocking]; the daemon stores the caller's origin instead of a hardcoded Agent; the app's GUI swap (swap.rs + shell.rs) passes App. Reserves Agent for a future sidecar swap path. Display-only — the verdict path (evaluate_order) is untouched. Round-trip test extended for both origins; all call sites updated. Verified: fmt --check, clippy -D warnings (default + tray), full workspace tests (0 failures). * fix(activity): responsive feed width (max-w 760, shrink on narrow windows so the STOP control + row trailing cluster never clip off the right edge) * fix: close the wrong-deny race in the activity feed + tighten the demo-deposit chain guard Codex full-pass must-fix: refresh_activity reconciled the feed selection by RAW NUMERIC INDEX, not by request_id. The ~2s poller can insert/remove proposed rows between renders, so a stale index could point at a DIFFERENT pending record than the operator sees highlighted — and `x`/deny (and the palette deny-selected) resolve the SELECTED record. Result: a deny could fire against the wrong pending card under background churn. (Wrong-APPROVE was never reachable — approve self-guards by opening the request_id-keyed review first.) Fix: preserve the highlight by request_id across every refresh — capture the selected id from the old snapshot, re-key to it in the new pending subset, and clamp only if it's gone. Corrected the now-accurate approve/deny comment (deny is one-key by design: it only REFUSES a spend — the fail-safe direction — and now targets the highlighted record by id). demo-deposit HIGH footgun: the chain guard only refused chain 1, so any OTHER real chain fronted at :8545 (a forwarded Sepolia/Holesky node, a tunnel) would pass and `cast send` with a PUBLIC dev key. Switched to an allowlist — refuse unless the RPC reports the exact demo chain id (matches the `demo` recipe's own enforcement). Verified: fmt --check, clippy -D warnings (default + tray), full workspace tests (0 failures), shellcheck. * fix: STOP-cancel expired-but-settleable swap orders + redesign the demo runner + amber-honesty Three codex-pass follow-ups, all approved as fix-now. 1. STOP-integrity hole (HIGH, swap path). `stop()` skipped the on-chain `invalidateOrder` for any order whose LOCAL approval record had lapsed to `ApprovalStatus::Expired` (the 120s approval TTL). But a signed order is loose on the CoW orderbook and settleable until its REAL `valid_to` (up to 24h) — those are different clocks. So a signed order could still settle AFTER STOP. Fix: gate STOP's cancel on the order's `valid_to`, never the local `Expired` status; skip only genuinely-unsettleable (`valid_to` in the past) orders. Extracted the selection into a pure `select_orders_to_cancel(requests, now)` with unit tests (a locally-Expired-but-settleable order IS selected; unsigned / already-cancelled / past-valid_to / plain-Tx are skipped). 2. Demo runner redesign (HIGH+LOW, scripts/demo-agent.sh). The loop tracked deposits with a single high-water mark it ratcheted BOTH up (on deposit) and down (after a shield settled). That dual-direction scalar raced the settlement: a deposit landing in the ~2s window between a broadcast and the balance dropping got absorbed by the downward rebaseline and lost (and an unexpected shield reply orphaned a deposit). Replaced with a FIXED baseline set once at startup — work each poll is just (public - baseline), new proposals gated on "nothing in flight", and the daemon's deterministic request_id is the idempotency authority (re-propose returns the same record as-is). No ratchet, no race, less code. Aligned docs/build/32-agent-loop-prompt.md to the same model. 3. Amber-honesty (MEDIUM, the feed). A lapsed card mapped to `Decided{approved:false}`, so the feed tinted it amber "you acted" / "not approved" even though NOBODY acted. Split out a new additive `ActivityLifecycle::Expired` (roundtrip-tested): a human denial / STOP revoke stay `Decided{false}` (a human DID act → amber is honest); a lapsed window renders neutral (muted "expired" label + a neutral dash glyph, never amber). The two-signal "amber = a human acted" promise now holds. Verified: cargo fmt --check, just check (clippy -D warnings, default + tray), cargo test --workspace (272 passed, 0 failed), shellcheck + bash -n on demo-agent.sh. * fix: the 5 confirmed bugs /code-review found in this branch's new code A high-recall /code-review pass (7 angles → verify) over our authored #60/#61 + fixes diff surfaced five real, reachable bugs — every one in code added this session. All fixed, with regression tests. 1. Runner wedges forever (HIGH, scripts/demo-agent.sh). try_execute() only dropped a saved id on broadcast / STOP / user_denied; an `expired` (120s approval-TTL lapse) fell through to "keep waiting", so the id stuck in PENDING_IDS — and the redesign's `pending_count == 0` gate then blocked EVERY future shield. Same for a re-proposed user_denied card (propose path treated it as transient → infinite "retry" spam). Fix: classify the daemon's terminal verdicts (mcp_is_terminal_refusal = expired | a human denied | user_denied) in BOTH try_execute and the propose-failure handler — drop the id AND advance the baseline past the refused funds (recorded per-id in a new PENDING_BASE array), so the loop gives up on dead funds and keeps watching for new ones. Aligned docs/build/32-agent-loop-prompt.md (it had the same expired-handling gap). 2. `--once` smoke shields the whole wallet (MED, justfile). Pinning baseline=0 against the prefunded demo wallet (anvil acct 0 holds ~10 ETH on the fork) treated the whole balance as surplus → over the per-tx cap → no broadcast → exit 1. Added `just demo-smoke`: captures the PRE-deposit balance, deposits, then runs --once pinned to that baseline so it shields exactly the deposit. The bare --once no longer defaults baseline to 0 (reads live balance = safe no-op). 3. Approved-then-lapsed erased the amber signal (MED, daemon.rs). A human-approved over-cap card that lapsed before execute mapped to ActivityLifecycle::Expired (neutral) — erasing that a human acted. Fix: activity_lifecycle now maps `Expired if req.approved` → Decided{approved:true}. 4. STOP-revoke of an auto-allow showed muted, not amber (MED, activity_view.rs). The amber tint keyed on bare `!auto_allowed`, but lock() leaves auto_allowed=true on a STOP-revoked row. A STOP is a human action → must read amber. Extracted a tested `human_acted` helper (any Decided{approved:false} OR !auto_allowed) and keyed the tint on it. 5. Stale activity_reviewing → dropped refusal (MED, shell.rs). resolve_activity_target preferred a set `activity_reviewing` even after a poll settled that record and the render fell through to the feed, so the first `x` resolved a stale off-screen id (a silent no-op) instead of the highlighted row. Fix: honor `activity_reviewing` only while its id is still in the pending subset, else fall back to the highlighted row. Verified: fmt --check, just check (clippy -D, default + tray), cargo test --workspace (274 pass, 0 fail — +2 regression tests), shellcheck + bash -n. * fix: bugs the fix-verification pass found IN the review fixes (incl. a blind-approve regression) A second adversarial pass over the prior fix commit (5734602) caught that two of those fixes introduced NEW bugs — exactly why fixes get the same scrutiny as features. 1. BLIND-APPROVE regression (HIGH, shell.rs). Fix #5's filter fallback turned a safe no-op into a real wrong-row resolve: when a review for card X is open and a background poll expires X, the render falls through to the feed (it's &self, can't clear activity_reviewing) with a DIFFERENT over-cap card Y highlighted. A one-key ⌘Enter then approved Y — whose clear-signing card was never shown — violating "no blind-approve of a spend" (the agent would then execute Y's overage). Real fix: clear `activity_reviewing` IN refresh_activity, atomically with the snapshot swap, when its id leaves the pending set. Now approve sees None → re-opens the highlighted row's review (no blind-approve), and deny targets the visible highlighted row (fail-safe, not the old no-op). 2. False-positive `expired` match (HIGH, demo-agent.sh). mcp_is_terminal_refusal grepped a BARE "expired", which also matches transient RPC errors ("connection expired", "session expired") embedded in broadcast_failed/signer_error prose — mis-classifying a transient failure as terminal and abandoning a still-valid card's funds. Anchored to the exact daemon copy ("this request expired", "a human denied this request"). Verified: terminal copies match, RPC errors don't. 3. Two-signal mismatch (MED, activity_view.rs). The amber LABEL was re-keyed to `human_acted` but the "→ You" actor-chain link still gated on bare `!auto_allowed`, so a STOP-revoked auto-allow showed the amber label with NO human in the chain. Re-keyed `needed_human` to `human_acted` too. 4. Propose-path baseline clarity (demo-agent.sh). `BASELINE_WEI=public_wei` is numerically the just-now propose-time balance (symmetric with try_execute's PENDING_BASE), but read as "absorb the whole wallet". Rewrote as `wei_add BASELINE_WEI surplus` (identical value) so the intent — "skip exactly the funds the daemon just refused" — is plain. Verified: fmt --check, just check (clippy -D, default + tray), cargo test --workspace (274 pass, 0 fail), shellcheck + bash -n, and a matcher-precision simulation. * fix: make a blind-approve structurally impossible (3rd entry point the verify loop found) Round-3 verification found a THIRD path to the same blind-approve hazard the round-2 fix only partially closed: `review_activity_row` (the proposed-row click handler) set `activity_reviewing` UNCONDITIONALLY, so a click landing one frame after a background poll settled that row left a stale review id with no refresh before the next ⌘Enter — and `approve_activity`'s `.or_else(highlighted)` fallback then resolved a DIFFERENT highlighted over-cap row whose clear-signing card was never shown. Whack-a-mole on entry points won't converge, so this is the DEEP fix: approve now resolves ONLY the actively-reviewed, STILL-PENDING record — it has NO highlighted-row fallback. The decision is a pure `approve_target(reviewing, pending)` helper (returns the id to resolve, or None = "open a review first, resolve nothing"), so a blind-approve of an unreviewed spend is structurally impossible no matter how `activity_reviewing` went stale (poll settle, raced click, anything). Deny keeps its one-key highlighted-row fallback (it only REFUSES — the fail-safe direction). Also hardened `review_activity_row` to set `activity_reviewing` only when the clicked row is still pending (no phantom review for a just-settled row). The round-2 refresh-time clear stays as state hygiene but is no longer load-bearing for the invariant. New unit test `approve_target_never_falls_back_to_the_highlighted_row` pins the guarantee: a reviewed id that has left the pending set resolves NOTHING (never the other highlighted row). Verified: fmt --check, just check (clippy -D, default + tray), cargo test --workspace (275 pass, 0 fail), shellcheck.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Evolves
DESIGN.mdfrom the v0 single-screen system into a unified multi-surface design foundation for Deckard, reframed as a human-sovereign / agent-delegated wallet. Locks the design language grounded in real Linear / Conductor / Splits product screenshots (not prose descriptions): a two-signal actor model (amber = human, cyan = agent), a Conductor-style sidebar IA (Projects → Wallets + Agents) with contextual views, full color/type/spacing tokens for dark + light, every component with its states, and the clear-signing / seed-reveal trust affordances. Incorporates a full design-review pass — neutral primary buttons (amber reserved for caution / where-you-are / hold / focus), whitespace grouping, circular status glyphs, and money$/precision rules. AddsCLAUDE.mdrouting all future UI work toDESIGN.mdand the grounding rule. No app code changes; the agent registration / MCP surface is intentionally left open and noted inDESIGN.md.