UX redesign v1 + v1.5: routing, discover, variants, protocol#1
Open
jmcferreira wants to merge 22 commits into
Open
UX redesign v1 + v1.5: routing, discover, variants, protocol#1jmcferreira wants to merge 22 commits into
jmcferreira wants to merge 22 commits into
Conversation
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
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
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.
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.mdandprds/antstation-ux-redesign.md+prds/antstation-v1-5-protocol-enrichment.md(kept local).The visible shift over both phases:
v1 — UI redesign (no protocol changes)
x-antseed-routing-priorityheader.?state + tooltip. One-time per-buyer; picking seeds the buyer's global default.~$X/1k · ~Xms · N providers) under the active priority. Stats off the card surface.config.json). Selecting seeds a "Recommended for …" section above the main grid. Re-openable from the filter drawer.v1.5 — Protocol enrichment
canonicalprotocol field onProviderAnnouncement(METADATA_VERSIONbumped 8→9). Plugin authors declareserviceId → canonicalIdmappings; the renderer'sresolveCanonicalKeyladder uses declared > curated table > v1 fuzzy heuristic.customizationprotocol field onProviderAnnouncement. Each entry is{ variant: string; description?: string }keyed by serviceId — the protocol-level source for variant declarations.canonicalvia a sharedbuildCanonicalMaphelper extracted to@antseed/provider-core(46 lines of duplicated logic removed).Base+ sorted variants.x-antseed-routing-variantheader plumbs the choice to the buyer-proxy.filterCandidatesByVariantruns BEFOREsortPeersByPriority. 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
servicePricing).serviceCategories(brand pollution / quality props cleanup).MIRROR:doc-comment headers; flagged as a follow-up).Test plan
pnpm --filter @antseed/node run test→ 646 / 646 pass.pnpm --filter @antseed/cli run test→ 132 / 132 pass.pnpm --filter @antseed/desktop exec npm run build:main && node --test apps/desktop/dist/main/*.test.js→ 126 / 126 pass.@antseed/node,@antseed/cli,@antseed/desktop run typecheck:renderer,@antseed/desktop exec tsc -p tsconfig.main.json --noEmit) clean.pnpm run buildfrom root clean — surfaces any cross-package type drift.?routing-priority chip; picking persists and seeds global default.canonical, curated/fuzzy otherwise); forecast reflects active priority.Variant ▾chip is hidden by default (no peer declares variants yet on the local network). To verify variants end-to-end, declare acustomizationentry on one peer's announcement and confirm the chip appears + variant choice filters the candidate set.Architecture notes for reviewers
sortPeersByPriorityinapps/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).filterCandidatesByVariantinapps/cli/src/proxy/buyer-proxy.tsruns beforesortPeersByPriorityon a per-request basis.apps/desktop/src/main/chat-sticky-peer.ts— EMA latency baseline (α = 0.3), degradation factor 2. Skip-list in-memory only.config.jsongains four new fields underbuyer—defaultRoutingPriority,routingPriorityTooltipDismissed,onboardingBannerDismissed,recommendedCategories. Per-chat fields (routingPriority,routingVariant, sticky peer) use the SessionManager custom-entry pattern.pickWinnerByPriorityinapps/desktop/src/renderer/core/forecast.tsmirrors the CLI'ssortPeersByPriority. Both files carry reciprocalMIRROR:doc-comment headers; cross-check tests inforecast.test.tsinclude a divergent-winners fixture.packages/node/src/discovery/peer-metadata.tsaddsProviderAnnouncement.canonicaland.customization. Encode/decode inmetadata-codec.tsis version-gated under the same v9 byte for both fields. Validation inmetadata-validator.ts.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)
customizationon their announcements (currently only the protocol + UI are in; no in-repo plugin declares variants yet — that's deliberate, the field is opt-in).