Skip to content

UX redesign v1 + v1.5: routing, discover, variants, protocol#1

Open
jmcferreira wants to merge 22 commits into
mainfrom
ux-redesign-v1
Open

UX redesign v1 + v1.5: routing, discover, variants, protocol#1
jmcferreira wants to merge 22 commits into
mainfrom
ux-redesign-v1

Conversation

@jmcferreira
Copy link
Copy Markdown

@jmcferreira jmcferreira commented May 26, 2026

Summary

Implements v1 + v1.5 of the AntStation UX redesign — 15 issues across 4 + 3 swarm rounds, sequenced as a single integrated branch. Design specs at docs/ux-redesign.md and prds/antstation-ux-redesign.md + prds/antstation-v1-5-protocol-enrichment.md (kept local).

The visible shift over both phases:

  • The buyer picks a model, AntStation routes the provider automatically under a Cheapest / Fastest / Most-Trusted priority.
  • Multi-provider customizations are first-class. A variant dropdown materializes in the chat header when a model has fine-tuned offerings.
  • Discover view collapses to one card per canonical model — using a fuzzy fallback in v1, and an authoritative protocol field + curated table in v1.5.

v1 — UI redesign (no protocol changes)

  • Per-chat routing-priority chip. Three priorities (Cheapest / Fastest / Most-Trusted, Most-Trusted uses on-chain signals only). Plumbed through a new x-antseed-routing-priority header.
  • First-chat ? state + tooltip. One-time per-buyer; picking seeds the buyer's global default.
  • Sticky-per-chat routing. Reuse the picked peer silently until error or ≥2× rolling-keepalive degradation triggers re-selection. Priority change clears sticky.
  • Discover card redesign. One card per canonical model via fuzzy-fallback dedup. Forecast (~$X/1k · ~Xms · N providers) under the active priority. Stats off the card surface.
  • Stats details drawer. Per-card affordance opens a side drawer with reputation, sybil flags, channel count, volume, lifetime requests/tokens, latency, and per-provider breakdown. X / Escape / backdrop close with focus trap + restore.
  • Inline onboarding banner. Dismissible banner with 7 curated category chips (buyer-scoped persistence in config.json). Selecting seeds a "Recommended for …" section above the main grid. Re-openable from the filter drawer.

v1.5 — Protocol enrichment

  • canonical protocol field on ProviderAnnouncement (METADATA_VERSION bumped 8→9). Plugin authors declare serviceId → canonicalId mappings; the renderer's resolveCanonicalKey ladder uses declared > curated table > v1 fuzzy heuristic.
  • customization protocol field on ProviderAnnouncement. Each entry is { variant: string; description?: string } keyed by serviceId — the protocol-level source for variant declarations.
  • Curated fallback table with 29 in-repo plugin model strings. Gives day-1 dedup coverage for external plugins emitting the same well-known strings without migrating.
  • 7 in-repo plugins migrated to declare canonical via a shared buildCanonicalMap helper extracted to @antseed/provider-core (46 lines of duplicated logic removed).
  • Variant chip in the chat header. Renders only when the chosen model's candidate peer set declares ≥1 variant. Base + sorted variants. x-antseed-routing-variant header plumbs the choice to the buyer-proxy.
  • Variant-aware routing. filterCandidatesByVariant runs BEFORE sortPeersByPriority. Base routing excludes peers declaring any variant for the requested serviceId (Base = "the unmodified base model", not "anything other than the chosen variant"). Sticky-peer clears on variant change.

Decoder backward-compat (PRD open question #1 closed): the metadata decoder accepts version >= MIN_SUPPORTED (currently 7), so v8-aware buyers seeing v9 announcements either decode gracefully or surface a clean truncation error rather than a silent crash. Verified by a test that patches the version byte to 8 on a v9 fixture.

What's NOT in this PR

  • v2 Multi-Model (Single/Multi-Model toggle, judge, synthesize, Compare view, retry-once-then-drop). Separate PRD, 4–6 weeks.
  • Phase 3 polish from v1.5 (variant hint chip on cards, drawer enrichment with per-variant provider counts).
  • Per-variant pricing (variants currently share the parent service's servicePricing).
  • Tag-whitelist work for serviceCategories (brand pollution / quality props cleanup).
  • A shared workspace package for the per-priority comparator (currently mirrored across CLI and renderer with reciprocal MIRROR: doc-comment headers; flagged as a follow-up).

Test plan

  • CI: pnpm --filter @antseed/node run test → 646 / 646 pass.
  • CI: pnpm --filter @antseed/cli run test → 132 / 132 pass.
  • CI: pnpm --filter @antseed/desktop exec npm run build:main && node --test apps/desktop/dist/main/*.test.js → 126 / 126 pass.
  • CI: all 4 typechecks (@antseed/node, @antseed/cli, @antseed/desktop run typecheck:renderer, @antseed/desktop exec tsc -p tsconfig.main.json --noEmit) clean.
  • CI: pnpm run build from root clean — surfaces any cross-package type drift.
  • Manual: fresh chat shows ? routing-priority chip; picking persists and seeds global default.
  • Manual: changing priority mid-chat reroutes the next prompt; sticky-routing reuses the picked peer; an erroring peer is silently replaced.
  • Manual: discover view shows one card per canonical model (authoritative for plugins declaring canonical, curated/fuzzy otherwise); forecast reflects active priority.
  • Manual: clicking the (i) affordance on a card opens the stats drawer; Escape / click-outside close; focus restored.
  • Manual: onboarding banner appears on fresh buyer state; selecting categories produces the "Recommended for …" section; dismissal persists; filter-drawer affordance re-opens preserving selections.
  • Manual: Variant ▾ chip is hidden by default (no peer declares variants yet on the local network). To verify variants end-to-end, declare a customization entry on one peer's announcement and confirm the chip appears + variant choice filters the candidate set.

Architecture notes for reviewers

  • Routing engine: sortPeersByPriority in apps/cli/src/proxy/routing-priority.ts. Most-Trusted uses only the five on-chain signals. Even-weight strawman blend documented inline (tuning is a follow-up).
  • Variant filter: filterCandidatesByVariant in apps/cli/src/proxy/buyer-proxy.ts runs before sortPeersByPriority on a per-request basis.
  • Sticky layer: apps/desktop/src/main/chat-sticky-peer.ts — EMA latency baseline (α = 0.3), degradation factor 2. Skip-list in-memory only.
  • Buyer-scoped persistence: config.json gains four new fields under buyerdefaultRoutingPriority, routingPriorityTooltipDismissed, onboardingBannerDismissed, recommendedCategories. Per-chat fields (routingPriority, routingVariant, sticky peer) use the SessionManager custom-entry pattern.
  • Renderer/CLI comparator mirror: the renderer's pickWinnerByPriority in apps/desktop/src/renderer/core/forecast.ts mirrors the CLI's sortPeersByPriority. Both files carry reciprocal MIRROR: doc-comment headers; cross-check tests in forecast.test.ts include a divergent-winners fixture.
  • Protocol v9: packages/node/src/discovery/peer-metadata.ts adds ProviderAnnouncement.canonical and .customization. Encode/decode in metadata-codec.ts is version-gated under the same v9 byte for both fields. Validation in metadata-validator.ts.
  • Curated table: apps/desktop/src/renderer/core/canonical-model.ts — 29 entries, sourced from in-repo plugin defaults. Intentionally not a global catalog.

Follow-ups (separate PRs)

  • Factor the per-priority comparator into a shared workspace package to eliminate the renderer ↔ CLI mirror.
  • Tune the Most-Trusted coefficients against live network data.
  • Tighten the fuzzy-fallback dedup heuristic against the 148-tag landscape baseline.
  • v2 Multi-Model (Single/Multi-Model toggle, judge, synthesize, Compare view).
  • Phase 3 v1.5 polish: variant hint chip on cards, drawer enrichment with per-variant provider counts.
  • Plugin authors start declaring customization on their announcements (currently only the protocol + UI are in; no in-repo plugin declares variants yet — that's deliberate, the field is opt-in).

gabrielpoca and others added 17 commits May 26, 2026 10:11
Why:

* The showOnboarding conditional was already commented out,
  leaving dead state and callbacks with no live usage.

This change addresses the need by:

* Deleting the showOnboarding local, its commented-out render
  branch, and all state/effects/callbacks that existed solely
  to support it (onboardingDismissed, hasConversations,
  handleStartChatting, re-show effect).
* Removing the now-unused DiscoverWelcome and useActions imports
  and the useCallback import.

Implements issue: Remove dead showOnboarding block
Why:

* Discover view needs to collapse multiple rows for the same model
  into one card; no shared normalization existed.

This change addresses the need by:

* New pure module at apps/desktop/src/renderer/core/canonical-model.ts
  exports canonicalizeModelId (strips known provider prefixes, lowercases)
  and groupByCanonical (groups DiscoverRow[] by canonical key).
* 20 unit tests verify true-positive collapse (openai/gpt-4o ↔ gpt-4o)
  and false-negative tolerance (r1-distilled stays separate from r1).

Implements issue: Fuzzy-fallback model dedup helper
Why:

* The onboarding banner and persistent filter need a single source of
  truth mapping the 7 user-facing categories to the freeform
  serviceCategories tags that providers advertise.

This change addresses the need by:

* Exporting CATEGORIES (ordered list of 7 Category objects with label,
  key, tagMatchers, and optional emptyState flag) from
  apps/desktop/src/renderer/core/categories.ts.
* Exporting matchesCategory(category, row) which returns true when at
  least one of the category's tagMatchers is present in the row's
  categories array; the Images placeholder (emptyState: true) always
  returns false as it has no current matchers.
* Adding 31 unit tests in categories.test.ts covering all matchers,
  the empty-state flag, case-sensitivity, and multi-category peers.

Implements issue: Curated category → wild-tags mapping module
Why:

* Buyers need a way to tune per-chat routing toward cost, speed, or
  on-chain trust without touching config files.

This change addresses the need by:

* Introducing `RoutingPriority` ('cheapest' | 'fastest' | 'most-trusted')
  and `sortPeersByPriority` in the CLI routing layer, ranking peers by
  price, latency, or a five-signal on-chain trust blend respectively.
* Persisting the chosen priority per chat via a new SessionManager custom
  entry type ('antseed:routing-priority'), defaulting to 'most-trusted'
  for all existing and new conversations.
* Wiring an IPC handler ('chat:ai-set-routing-priority'), preload bridge
  method, renderer state field (chatRoutingPriority), chat-module action
  (setRoutingPriority), and app-level action registration so the UI can
  read and write the priority end-to-end.
* Adding a RoutingPriorityChip component in the chat header (next to the
  service dropdown) that renders the current priority and lets the user
  cycle through the three values; the next request in that chat routes
  under the new priority.
* Covering all three priorities with unit tests (routing-priority.test.ts)
  and the persistence layer with chat-routing-priority.test.ts; all 203
  tests pass and both renderer/main typechecks are clean.

Implements issue: Routing priority chip end-to-end (TRACER BULLET)
Why:

* Cycle 1 added sortPeersByPriority and the chip UI but left zero
  production call sites: the chip stored state but never changed
  which peer the next prompt routed to

This change addresses the need by:

* Wires sortPeersByPriority into the request path so the chip
  actually re-routes the next prompt — buyer-proxy.ts reads the
  x-antseed-routing-priority header and sorts candidate peers
  before dispatch; pi-chat-engine.ts reads the session's stored
  routing priority and uses selectPeerIdByPriority to pick the
  highest-ranked peer from the catalog when no peer is pinned
* Adds x-antseed-routing-priority header forwarding from the
  desktop to the proxy so the priority travels with each request
* Adds integration tests covering all three priorities end-to-end
  through the wired buyer-proxy path (_dispatchToPeer spy confirms
  the correctly-ranked peer is selected)
* Replaces pixel literals in RoutingPriorityChip.module.scss with
  CSS custom properties and rem fallbacks (no raw px or hex)

Implements issue: Routing priority chip end-to-end (TRACER BULLET)
Why:

* Discover cards need a forecast string from real routing winner stats

This change addresses the need by:

* Adding computeForecast(rows, priority) — picks winning DiscoverRow
  under cheapest/fastest/most-trusted, returns pricePer1kUsd/latencyMs/
  providerCount; pure function, no side effects
* Mirroring CLI routing-priority.ts comparators in renderer-side
  pickWinnerByPriority and computeRowTrustScore; cross-check tests
  verify the renderer comparator agrees with CLI tie-break rules

Implements issue: Per-model forecast computation
…ents

Why:

* Cycle-1 cross-check tests used 2-peer fixtures so tie-break ordering
  could not be verified; the reviewer required ≥3 peers with at least one
  fixture where different priorities pick different winners.
* No comment linked the two mirrored comparator implementations, making
  future drift silent.

This change addresses the need by:

* Option C taken: CLI dist is not guaranteed to exist at renderer-test
  time and tsconfig excludes renderer test files from compilation, making
  runtime co-execution (Option A) genuinely unworkable; Option B (shared
  package) was judged too invasive for a docs-level fix.
* Added MIRROR comment headers to both forecast.ts and
  apps/cli/src/proxy/routing-priority.ts, with a note that the test file
  serves as the behavioural spec reviewed at code-review time.
* Replaced 2-peer cross-check fixtures with ≥3-peer fixtures throughout;
  added a shared makeDivergentFixture (peerA=fast, peerB=trusted,
  peerC=cheapest) and an explicit invariant test that all three priorities
  pick a DIFFERENT winner on that fixture.
* Each priority test now asserts the full ordering of all peers (via
  sortedIds helper) in addition to the single winner.

Implements issue: Per-model forecast computation
Why:

* Brand-new chats need a discoverable way to pick routing priority before the first message is sent, with a one-time explanation of the three options.

This change addresses the need by:

* Extending RoutingPriorityChip to accept undefined value and render a ? badge with a tooltip explaining Cheapest/Fastest/Most Trusted
* Persisting the buyer's global default priority and tooltip-dismissed flag in config.json under buyer.defaultRoutingPriority / buyer.routingPriorityTooltipDismissed
* Seeding that global default whenever a priority is picked from the ? chip, so future new chats skip the ? state
* Defaulting to Most Trusted when the tooltip is dismissed without picking

Implements issue: First-chat routing chip ? state + tooltip
Why:

* Routing without stickiness re-selects a peer on every request, causing
  unnecessary latency and inconsistency within a single conversation.

This change addresses the need by:

* Adding chat-sticky-peer.ts: per-chat in-memory state that tracks the
  currently chosen peer, a skip-list of failed/degraded peers, and a
  rolling EMA latency baseline (α=0.3) for the 2× degradation heuristic.
* Wiring getStickyPeer/markPeerFailed/markPeerSuccess into the
  runStreamingPrompt request path in pi-chat-engine.ts so the sticky
  peer is consulted before selectPeerIdByPriority runs, and the skip-list
  filters catalog entries on re-selection.
* Clearing sticky state on priority-chip changes (setRoutingPriority IPC)
  so the next prompt picks a fresh peer under the new priority.
* 19 unit tests cover happy-path reuse, error/degradation-triggered
  re-selection, and priority change; 4 integration tests prove the full
  lifecycle mirrors the production request path.

Implements issue: Sticky-per-chat routing + auto-fallback
Why:

* The card grid was showing one card per peer/service, producing
  duplicates for the same model offered by multiple providers

This change addresses the need by:

* Replacing ad-hoc dedup in buildCardsFromRows with groupByCanonical
  from canonical-model.ts — one card per canonical model key
* Adding computeForecast (from forecast.ts) to derive ~$X/1k · ~Xms ·
  N providers footer line under the user's active routing priority
* Removing reputation badge, sybil warnings, channel count, volume, and
  request/token counts from the card surface — data retained on CardItem
  (and in new groupedRows field) for the issue-6 details drawer

Implements issue: Discover card hierarchy redesign
Why:

* cycle-1 introduced a raw font-size: 11px in .card-forecast
  which violates the repo's no-raw-px style preference

This change addresses the need by:

* Replacing font-size: 11px with 0.6875rem (bare rem, matching
  the pattern used in this file where no CSS variables exist)

Implements issue: Discover card hierarchy redesign
Why:

* Per-card stats (reputation, sybil, channels, volume, requests,
  tokens, latency, proxy, per-provider breakdown) were removed from
  the card surface in issue 5 and need to be accessible on demand.

This change addresses the need by:

* Added a second drawer instance (StatsDrawer) with its own
  useState<CardItem|null> — separate from the filter drawer — opened
  via an InformationCircleIcon affordance button in the card footer
  (stopPropagation prevents card click collision).
* Drawer closes via X button, Escape key, and click-outside backdrop;
  focus traps inside (Tab/Shift+Tab cycle), and restores to the opener
  button on close by capturing document.activeElement at open time.

Implements issue: Stats details drawer
Why:

* cycle-1 added raw px and hex literals in the drawer SCSS
  which violate the repo's no-raw-px/no-hex preference

This change addresses the need by:

* Replacing raw px values with rem or var(--x, Yrem) tokens
* Replacing raw hex with var(--accent-amber, ...) tokens

Implements issue: Stats details drawer
Why:

* cycle-2 sweep missed two raw px in outline focus-ring styles

This change addresses the need by:

* Replacing outline: 2px solid var(--accent) with a rem-based value
* Replacing outline-offset: 1px with a rem-based value

Implements issue: Stats details drawer
Why:

* First-time buyers had no guided way to express what they want to work
  on, making the network grid feel undifferentiated on first load.

This change addresses the need by:

* Rendering a dismissible banner above the controlsRow in DiscoverWelcome
  with multi-select category chips; selecting categories seeds a
  "Recommended for …" section above the main grid, filtered via
  matchesCategory from categories.ts.
* Persisting bannerDismissed + selectedCategories as buyer-scoped fields
  in config.json (new onboarding-prefs.ts helper + IPC handlers); the
  filter drawer gains a "Show category recommendations" button that
  flips the dismissal flag back to false without losing prior selections.

Implements issue: Inline onboarding banner
João Ferreira added 5 commits May 27, 2026 10:08
Why:

* Buyers need a provider-declared stable model id for
  deduplication across peers advertising aliased/dated variants.

This change addresses the need by:

* Bumping METADATA_VERSION to 9, adding canonical field to
  ProviderAnnouncement, and encoding/decoding it gated on
  version >= 9 in metadata-codec.ts; decoder accepts v7+ so
  v9 metadata is not hard-rejected by v8-aware buyers.
* Adding resolveCanonicalKey precedence ladder (declared >
  curated table > fuzzy heuristic) in canonical-model.ts and
  wiring CURATED_CANONICAL_TABLE placeholder for issue 2.
* Migrating provider-anthropic to declare canonical via
  buildCanonicalMap (identity map by default), propagating
  the field through PeerInfo, buyer.state.json, NetworkPeerAddress,
  ChatServiceCatalogEntry, DiscoverRowEntry, and DiscoverRow.

Implements issue: `canonical` end-to-end + provider-anthropic (TRACER BULLET)
Why:

* External plugins advertising well-known model strings get dedup
  coverage even if they don't declare the v9 canonical field.

This change addresses the need by:

* Seeding CURATED_CANONICAL_TABLE from 7 official in-repo plugins
  (Anthropic, Claude Code, Claude OAuth, OpenAI, OpenAI Responses,
  Local LLM, router-local) with ~30 model-string-to-canonical entries,
  collapsing dated variants to their stable base names.
* Adding tests for precedence (declared > table > fuzzy), ≥3 table
  entries verified, and dedup of dated/base sonnet via the table.

Implements issue: Curated canonical fallback table
Why:

* Closes the protocol-coverage gap — all in-repo providers now declare
  canonical so routers can deduplicate aliased service variants (v9+).

This change addresses the need by:

* Migrates provider-openai, provider-claude-code, provider-claude-oauth,
  provider-local-llm, and provider-openai-responses to call
  buildCanonicalMap(allowedServices) from @antseed/provider-core and
  pass the result as canonical in their BaseProvider constructor (or
  OpenAIResponsesProvider for the responses plugin).
* Adds two canonical-coverage tests per plugin: one asserting a non-empty
  canonical map when services are configured, one asserting undefined when
  no services are given.

Implements issue: Plugin migration sweep — remaining 5 in-repo plugins
Why:

* Providers have no way to declare service-level variants (e.g.
  TEE-hardened, fine-tuned) in the announcement protocol, so buyers
  cannot surface or filter on this information.

This change addresses the need by:

* Adding ServiceCustomization interface (variant + description?) and
  customization? field to ProviderAnnouncement; codec encodes/decodes
  under the existing v9 gate alongside canonical (empty map = 0 bytes).
* Validating customization in metadata-validator (keys in services,
  variant required 1-32 chars matches serviceId regex, description
  optional max 256 chars); propagating through NetworkPeerAddress →
  ChatServiceCatalogEntry → DiscoverRow with normalizeDiscoverRow
  coverage; codec round-trip and validator rejection tests added.

Implements issue: `customization` field end-to-end
Why:

* Variant dropdown specced in PRD but never rendered — no UI existed
  to filter candidate peers by their declared service customization

This change addresses the need by:

* VariantChip component with per-chat routingVariant state persisted via
  SessionManager custom entries (chat-routing-variant.ts), threaded
  through makeProxyService as x-antseed-routing-variant header

* buyer-proxy reads x-antseed-routing-variant header and applies
  filterCandidatesByVariant before sortPeersByPriority — Base routing
  excludes variant-declaring peers; named variant keeps only matching
  peers; absent requestedService is a no-op

* Changing the variant chip clears the sticky peer (clearStickyState)
  mirroring the existing priority-change pattern in pi-chat-engine.ts

Implements issue: Variant chip + variant-aware routing
@jmcferreira jmcferreira changed the title UX redesign v1: routing chip, discover dedup, onboarding UX redesign v1 + v1.5: routing, discover, variants, protocol May 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants