Skip to content

feat: unify app shell, add fuzzy command palette, harden agent_bidding commit-reveal - #466

Merged
devJaja merged 5 commits into
Epta-Node:mainfrom
kaluuba-org:feat/shell-palette-bidding-hardening-348
Aug 31, 2026
Merged

feat: unify app shell, add fuzzy command palette, harden agent_bidding commit-reveal#466
devJaja merged 5 commits into
Epta-Node:mainfrom
kaluuba-org:feat/shell-palette-bidding-hardening-348

Conversation

@kaluuba-org

@kaluuba-org kaluuba-org commented Aug 30, 2026

Copy link
Copy Markdown

Summary

Closes #348, Closes #350, Closes #352.

Three issues that turned out to share a dependency: #352 introduces a single
source of truth for navigation, and #348's palette reads from it so the two can
never drift apart. #350 is independent — a security review and hardening pass on
the agent_bidding commit-reveal auction.

One thing to flag up front: main does not build. Several files carry two
concatenated copies of themselves from bad merges. The frontend was unbuildable,
so #348 and #352 could not be written or verified without repairing it — that
repair is the first commit and is described in detail below. The backend and one
contract crate are broken the same way and are deliberately untouched; see
Pre-existing breakage.

Changes

fix(frontend): repair merge debris that broke the build

Where a file held two versions, the one kept is the one the rest of the codebase
already depends on:

File Kept Why
ToastContext.tsx the version exporting ToastContext hooks/useToast.ts imports it
SendXLMForm.tsx react-hook-form + zod the file's own imports point at it
TaskSubmissionForm.tsx the i18n-aware makeTaskSchema factory the static import cannot take t
Hero.tsx plain elements opening tags were already un-wrapped; only closing tags still said motion.*

AgentDetailModal.tsx regained the useAgentReputation call its JSX still read
from; Toast.css lost an orphaned @keyframes body that failed postcss. Fixing
the parse errors surfaced a second layer of type errors that had been masked
behind them.

Two behaviour fixes fell out of this:

  • The task submit button was gated on !isValid, so an untouched form could
    never be submitted — a dead button with no explanation. Validation now runs on
    click and says what is wrong.
  • AgentReputationRadar.test.tsx stubbed ResponsiveContainer without passing
    dimensions down, so recharts drew nothing. It failed on main too.

feat(frontend): unify the application shell#352

The sidebar, drawer, and top nav each carried their own navigation list, which
is how they drifted: the drawer's labels were hardcoded English while the
sidebar's were translated, and the sidebar's "Dashboard" pointed at / — the
public landing page — rather than /dashboard. layout/navigation.ts is now the
single source of truth for all of them, plus the breadcrumb and the page title.

  • Grouped collapsible sidebar (Overview / Work / Account). Collapsing hides
    labels visually but keeps them in the accessibility tree; previously they were
    removed from the DOM and the buttons lost their accessible names.
  • Animated active state — one shared layoutId pill slides between items
    instead of blinking. Disabled under prefers-reduced-motion.
  • Active matching is exact-or-descendant: /tasks/new/step-2 highlights
    "New Task", while /tasks/abc-123 (a detail page with no nav entry) correctly
    highlights nothing.
  • Slide-over drawer now enters from the left — the side the sidebar occupies
    on desktop — rather than sliding up from an edge nothing else in the app uses.
  • Breadcrumbs render structural segments like /tasks as plain text instead
    of links that 404, and wrap rather than pushing the page sideways.
  • Per-user sidebar state: keyed by wallet (sidebar_collapsed:<publicKey>).
    Signed-out users keep the old unscoped key, so no existing preference is
    dropped. Storage access is guarded — private-mode browsers throw, and the shell
    falls back to an expanded sidebar rather than failing to render.
  • Breakpoint 768px → 1024px per the acceptance criteria.

feat(frontend): fuzzy command palette#348

Matching was String.includes(), which finds nothing unless you type a
contiguous run, and results came back in source-iteration order.

  • Scored subsequence matcher (utils/fuzzy.ts): dsh → "Dashboard",
    cpad → "Copy wallet address". Scoring rewards consecutive characters, word
    starts (including camelCase humps) and early matches; penalises gaps and long
    unmatched prefixes — the difference between a match and the match. It also
    returns matched indices, which the palette uses to highlight the exact
    characters that hit.
  • Sources: pages (from the shared nav config), actions, agents (name and
    capabilities), tasks (id, prompt, status), and recent history — searchable as a
    source, so re-running an earlier query needs only a few of its characters.
    Field and category weights rank a title hit above the same hit in a subtitle,
    and actions above recents.
  • Actions: run new task, jump to agent, open wallet, copy address, toggle
    theme. Copy-address is hidden with no wallet connected rather than offered dead.
  • Keyboard-first: arrows and Tab move selection, Home/End jump to the ends,
    Enter runs, Escape dismisses. Tab is trapped. Focus stays on the input and the
    list is driven via aria-activedescendant over a real listbox/option
    structure, so screen readers announce the highlighted row without focus moving.

Selecting a recent search now refills the query — it previously fired a search
whose results were thrown away. All strings are translated (en + zh).

feat(contracts): harden agent_bidding#350

Full threat model, per-attack mitigation, and residual risks in
smart-contracts/docs/agent-bidding-security.md;
every claim there names the test that proves it.

Attack Mitigation
A-1 reveal_bids was callable the instant bidding closed, so an attacker could reveal their own poor bid and finalise in the same ledger, scoring out everyone who had not yet landed a reveal Bounded reveal window; finalisation refused while the window is open and any bid is still sealed. Early finalisation stays allowed once nothing is outstanding, which cannot be forced
A-2 Non-reveal was free, making a sealed bid a costless option to walk away after reading the field Bonds settle asymmetrically — revealers refunded, non-revealers forfeit
A-3 The pre-image bound only the bidder, so identical plaintext committed identically across auctions and deployments, leaking still-sealed bids Domain separation over (domain, contract_id, task_id); commitment_of published so tooling cannot drift from what reveal_bid verifies
A-6 Unbounded price, terms, and bidder count — the last of which could push reveal_bids past the resource limit and wedge an auction, stranding every bond max_price, MAX_BID_PRICE, MAX_BIDDERS, MAX_TERMS_LEN, bounded durations, checked arithmetic, paginated get_bidders
A-7 An auction nobody revealed on locked bonds forever abort_auction rescue path
A-8 reveal_bids and award_contract mutated state with no caller and no require_auth Both authenticate; award_contract is restricted to creator or winner

The doc is equally explicit about what is not covered — reputation is
self-declared, reveals are public as they land, Sybil resistance is bond cost
alone, and bonds/escrow are modelled as state rather than custodied here. Those
are integration-critical.

Tests grow 27 → 50, covering replay (cross-auction, cross-bidder,
re-reveal), late reveal, double commit, the finalisation front-run, forfeiture,
every cap, and the abort path.

Cargo.lock is refreshed: it still listed three removed crates and missed
agent-registry's upgrade-manager dependency, which fails CI's --locked
builds.

Verification & Testing

  • npm test (Frontend) — 35 files, 319 tests pass
  • npm run build (Frontend) — clean, tsc --noEmit clean
  • cargo test --locked -p agent-bidding50 pass
  • cargo fmt --all --check && cargo clippy --locked -p agent-bidding --all-targets -- -D warnings — clean
  • cargo build --locked -p agent-bidding --target wasm32v1-none --release — clean
  • npm test (Backend) — cannot run, see below

Pre-existing breakage not addressed here

Verified present on upstream/main before this branch:

  1. Backend does not compile. package.json is invalid JSON (two manifests
    concatenated), tsconfig.json and jest.config.js likewise, and src/api/app.ts
    holds two different createApp factories. Repairing the manifests leaves
    ~45 type errors across ~12 files.
  2. upgrade-manager does not compile (25+ errors), which breaks
    agent-registry, which the contracts CI job builds.

I stopped short of both. Each bad merge means judging which half of someone
else's work is canonical, across a subsystem none of these three issues touch —
and a wrong guess there is worse than leaving the breakage visible. The frontend
was different: #348 and #352 live in it, and I could not verify my own work
without it building.

Because CI's contracts job is gated on needs: [backend, frontend], the
backend failure will keep the contract checks from running on this PR. The
agent_bidding commands above were run locally and all pass. Happy to open a
separate PR for the backend repair if maintainers want it.

Contributor Checklist

  • PR title follows Conventional Commits
  • All CI checks that can pass locally, do
  • Documentation updated — new smart-contracts/docs/agent-bidding-security.md, updated contract README and frontend/LAYOUT_IMPLEMENTATION.md
  • No secrets or credentials committed

mananuf and others added 4 commits August 30, 2026 17:28
…ning (Epta-Node#350)

Analyses the sealed-bid auction's attack surface and closes the gaps found.
The full threat model, per-attack mitigation, and residual risks live in
smart-contracts/docs/agent-bidding-security.md; each claim there names the
test that proves it.

Front-running (A-1): `reveal_bids` became callable the instant bidding
closed, so an attacker could reveal their own poor bid and finalise in the
same ledger, scoring out every bidder who had not yet landed a reveal. Adds
a bounded reveal window (`reveal_deadline`) and refuses finalisation while
the window is open and any bid is still sealed. Early finalisation stays
allowed once `revealed_count == bid_count`, where nothing is left to race.

Free last-look option (A-2): non-reveal was costless, making a sealed bid a
free option to walk away after reading the field. `award_contract` now
settles bonds asymmetrically — revealers are refunded, non-revealers
forfeit via a new `SealedBid.forfeited` flag.

Commitment replay (A-3): the pre-image bound only the bidder, so identical
plaintext committed identically across auctions and deployments, leaking
still-sealed bids. Adds domain separation over
(domain, contract_id, task_id) and publishes `commitment_of` so off-chain
tooling cannot drift from what `reveal_bid` verifies.

Position-size caps (A-6): `max_price` per auction, global `MAX_BID_PRICE`,
`MAX_BIDDERS`, `MAX_TERMS_LEN`, bounded phase durations, checked scoring
arithmetic, and a paginated `get_bidders`. The bidder cap is what keeps the
loops in `reveal_bids`/`award_contract` from being pushed past the resource
limit and wedging an auction.

Also adds `abort_auction` so an auction nobody reveals on cannot strand
bonds forever (A-7), and `require_auth` on `reveal_bids`/`award_contract`,
which previously mutated state with no caller at all (A-8).

Tests grow 27 → 50, covering replay (cross-auction, cross-bidder, re-reveal),
late reveal, double commit, the finalisation front-run, forfeiture, every
cap, and the abort path.

Cargo.lock is refreshed: it still listed three removed crates and missed
agent-registry's upgrade-manager dep, which fails CI's `--locked` builds.

Resolves Epta-Node#350

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`main` does not type-check or build. Several files carry two concatenated
copies of themselves from bad merges — duplicate imports, duplicate state
declarations, orphaned JSX closing tags, and a stray `@keyframes` tail. The
syntax errors also masked the type errors behind them, so fixing the parse
failures surfaced a second layer.

Where a file held two versions, the one kept is the one the rest of the
codebase already depends on:

- `ToastContext.tsx` — kept the version exporting `ToastContext` (which
  `hooks/useToast.ts` imports) and delegating to the shared `ToastContainer`.
- `SendXLMForm.tsx` — kept react-hook-form + zod, which the file's own
  imports (`@hookform/resolvers`, `walletTransferSchema`, `FormField`) point
  at. The balance check the schema cannot express moved into a resolver
  wrapper so the error still lands on the `amount` field.
- `Hero.tsx` — dropped the half-removed framer-motion wrappers; the opening
  tags were already plain elements with CSS `slide-up` animations, only the
  closing tags still said `motion.*`.
- `TaskSubmissionForm.tsx` — kept the i18n-aware `makeTaskSchema` factory over
  the static import, restored the missing `z`/`AlertCircle` imports, and
  dropped the duplicated hook calls.
- `AgentDetailModal.tsx` — restored the `useAgentReputation` call whose
  results the JSX still read.
- `Toast.css` — removed an orphaned `@keyframes` body (a `}` with no opening
  rule, which failed postcss) and the container properties that had leaked
  into the `.toast__dismiss` button rule.

Two behaviour fixes fell out of the repair:

- The submit button was gated on `!isValid`, so an untouched form could never
  be submitted and the user got a dead button with no explanation. Validation
  now runs on click and reports what is wrong.
- `AgentReputationRadar.test.tsx` stubbed `ResponsiveContainer` without passing
  dimensions down, so recharts drew nothing and the assertion never passed —
  it failed on `main` too.

Toast dismissal picks up an i18n label rather than hardcoded English, matching
the rest of the component.

Frontend is now green: `tsc --noEmit` clean, `vite build` succeeds, and the
vitest suite passes. The backend is broken by the same class of bad merge but
across ~12 files, which is a separate repair and is deliberately untouched
here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The sidebar, top nav, and mobile drawer each carried their own copy of the
navigation list, which is how they drifted apart: the drawer's labels were
hardcoded English while the sidebar's were translated, and the sidebar's
"Dashboard" pointed at `/` — the public landing page — rather than
`/dashboard`.

`layout/navigation.ts` is now the single source of truth. The sidebar, the
drawer, the breadcrumb labels, and the TopNav page title all read from it, so
a rename moves every surface at once.

- **Grouped, collapsible sidebar.** Items are grouped (Overview / Work /
  Account). Collapsing hides labels and group headings visually but keeps them
  in the accessibility tree via `.visually-hidden`, where before they were
  removed from the DOM entirely and the buttons lost their names.
- **Animated active state.** A single shared `layoutId` pill slides between
  items instead of blinking out and back in. Disabled under
  `prefers-reduced-motion`.
- **Active matching** is exact-or-descendant, so `/tasks/new/step-2`
  highlights "New Task" while `/tasks/abc-123` — a detail page with no nav
  entry — correctly highlights nothing.
- **Slide-over drawer.** The drawer now enters from the left, the side the
  sidebar occupies on desktop, rather than sliding up as a bottom sheet from
  an edge nothing else in the app uses.
- **Breadcrumbs** derive labels from the nav config, render structural
  segments like `/tasks` as plain text instead of links that 404, and wrap
  rather than pushing the page sideways.
- **Per-user sidebar state.** The collapsed flag is keyed by wallet
  (`sidebar_collapsed:<publicKey>`), so two people sharing a browser profile
  keep separate preferences. Signed-out users keep the old unscoped key, so no
  existing preference is dropped. Reads and writes are guarded: private-mode
  browsers throw on storage access, and the shell falls back to an expanded
  sidebar rather than failing to render.
- **Breakpoint moved 768px → 1024px**, per the acceptance criteria. The
  threshold lives in `MOBILE_BREAKPOINT_QUERY` and in the `@media` blocks; both
  are commented as needing to stay in step.

The skip-to-content link had no styles at all and was permanently visible; it
is now off-screen until focused.

Tests cover grouping, collapsed-state accessibility, descendant active
matching, per-wallet persistence, storage failure, and breadcrumb construction.

Resolves Epta-Node#352

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…pta-Node#348)

The palette matched with `String.includes()`, which finds nothing unless you
type a contiguous run, and returned results in whatever order the sources were
iterated — so a page could rank above the exact agent you named.

**Fuzzy matching** (`utils/fuzzy.ts`) is a scored subsequence matcher, so any
subset finds its target: `dsh` → "Dashboard", `cpad` → "Copy wallet address".
Scoring rewards consecutive characters, word starts (including camelCase
humps), and matches near the front, and penalises gaps and a long unmatched
prefix — the difference between "is a match" and "is the match the user meant".
It returns the matched indices too, which the palette uses to highlight the
characters that actually hit. A DP over (query × target) with a running max
keeps it linear rather than quadratic.

**Sources.** Pages come from the shared nav config (`Epta-Node#352`), so the palette
cannot drift from the sidebar. Agents match on name *and* capabilities; tasks
on id, prompt, and status. Recent history is a searchable source, not just a
list shown on an empty query — so re-running an earlier query needs only a few
of its characters. Field weights make a title hit outrank the same hit buried
in a subtitle, and category weights put actions first, recent searches last.

**Actions** are new: run new task, jump to agent, open wallet, copy address,
toggle theme. Copy-address is hidden when no wallet is connected rather than
offered as a dead entry.

**Keyboard-first.** Arrows and Tab move the selection, Home/End jump to the
ends, Enter runs, Escape dismisses. Tab is trapped, since walking focus into
the page behind a modal is how a palette loses you. Focus stays on the input
and the list is driven through `aria-activedescendant` over a proper
listbox/option structure, so screen readers announce the highlighted row
without focus moving. The selected row scrolls into view — guarded, because
`scrollIntoView` does not exist outside a layout engine.

Selecting a recent search now refills the query instead of firing a search
whose results were discarded, and the palette stays open so you can refine it.
A stale selection index is reset when the result list shrinks, which would
otherwise run the wrong action on Enter.

All strings are translated (en + zh); the palette was entirely hardcoded
English before.

Adds 25 tests for the matcher (matching, rejection, ranking, highlight
indices) and 29 for the hook (shortcuts, each source, each action, history
persistence and eviction, API failure).

Resolves Epta-Node#348

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@drips-wave

drips-wave Bot commented Aug 30, 2026

Copy link
Copy Markdown

@kaluuba-org Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@vercel

vercel Bot commented Aug 30, 2026

Copy link
Copy Markdown

@mananuf is attempting to deploy a commit to the Jaja's projects Team on Vercel.

A member of the Team first needs to authorize it.

@devJaja
devJaja merged commit 2cb16c6 into Epta-Node:main Aug 31, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants