From 8979fa1946ce1602257890d66bac5588af0d7428 Mon Sep 17 00:00:00 2001 From: Evanfeenstra Date: Tue, 10 Nov 2026 20:57:33 -0800 Subject: [PATCH 1/2] Store front: a redesign, and the Concepts as a tree instead of 752 chips The store (`?store=1`, index.stakwork.ai) wore the admin console's 12 px type and palette, and a package's page listed every Concept it carries as a chip: openlaw/legal-desk's page was 32,500 px tall. - Concepts are one tree everywhere (`ConceptTree`): the roots with their counts, a branch opened in place and its children drawn only then, a filter over every page. The package page and the Concepts page share it; "Under this page" and search results are rows, 24 at a time. `conceptChildren` (store-links.ts, tested) is the tree's branches. - The top search returns the matching Concept pages as well as packages; `/` focuses it. - The look: its own scale and a darker palette (the shell's variables redefined on `.store`), a hero with the index's counts, cards, a package header with a stat strip that jumps to each part, workflows as rows that open to their inputs, steps and agent tools, a sticky aside, skeletons while a read is out, a phone layout. A "For agents" block carries the one line that adds the store to an MCP client. - `GET /index/packages/:publisher/:name?docs=0` leaves each Concept page's text out (`withoutConceptDocs`); the store reads a package that way. legal-desk's page was 2.7 MB of JSON for a tree drawn from names. - `STRUT_DEV_INDEX= npm run dev` (web/vite.config.ts; the `store` entry in .claude/launch.json) runs this checkout's store over a deployed index's open reads. --- .claude/launch.json | 6 + AGENTS.md | 8 +- specs/API.md | 2 +- src/createStrut.ts | 8 +- src/packages/index.test.ts | 10 +- src/packages/index.ts | 10 +- web/src/api.ts | 4 +- web/src/components/Store.tsx | 573 ++++++++++++++++++++---------- web/src/icons.tsx | 12 + web/src/store-links.test.ts | 10 +- web/src/store-links.ts | 19 + web/src/styles/components.css | 646 +++++++++++++++++++++++++++------- web/vite.config.ts | 6 +- 13 files changed, 974 insertions(+), 340 deletions(-) diff --git a/.claude/launch.json b/.claude/launch.json index 91e9a66a..f5547550 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -6,6 +6,12 @@ "runtimeExecutable": "npm", "runtimeArgs": ["--prefix", "web", "run", "dev"], "port": 5173 + }, + { + "name": "store", + "runtimeExecutable": "env", + "runtimeArgs": ["STRUT_DEV_INDEX=https://index.stakwork.ai", "npm", "--prefix", "web", "run", "dev", "--", "--port", "5183", "--strictPort"], + "port": 5183 } ] } diff --git a/AGENTS.md b/AGENTS.md index f44c5fbc..8dc87ba8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -261,7 +261,7 @@ strut/ ├── attachments.ts # the chat composer's files read for `POST /chat` (a PNG/JPEG past 1568 px scaled down first; the server's limits checked early) ├── automation-form.ts # the Automations editor's flat form state ⇄ trigger draft (pure; no calendar math — the server owns that) ├── earnings.ts # Earnings' pure parts (plans/paid-runs.md §8; tested): fmtUsd / fmtSats, subjectLabel, the ranges, priceLine, withdrawBlocked (why Withdraw is off), maxWithdrawMsat, readWithdrawInvoice (the invoice read with src/payments/bolt11.ts and its fee cap from fee.ts — what the person confirms is what the route checks; the network left to the wallet), withdrawRefusal (a refusal said for a person) - ├── store-links.ts # the STORE FRONT's pure parts (plans/marketplace.md §7; tested): parseStoreView / storeQuery (what `?store=1&pkg=|concept=|q=` names), the DEEP LINK out of the store — `installLink(strut, { pkg, index: { key, url } })` → `/?install=&index=&index_url=`, `parseInstallLink` (the shell's side), `indexIdOf` (the id a strut files a followed index under: its host), `normalizeStrutUrl` — and the folds the page draws from the open reads: `appsOf` (the hits' apps first, by title), `conceptRoots` (the tree's roots with how much sits under each), `canWords` + ├── store-links.ts # the STORE FRONT's pure parts (plans/marketplace.md §7; tested): parseStoreView / storeQuery (what `?store=1&pkg=|concept=|q=` names), the DEEP LINK out of the store — `installLink(strut, { pkg, index: { key, url } })` → `/?install=&index=&index_url=`, `parseInstallLink` (the shell's side), `indexIdOf` (the id a strut files a followed index under: its host), `normalizeStrutUrl` — and the folds the page draws from the open reads: `appsOf` (the hits' apps first, by title), `conceptRoots` (the tree's roots with how much sits under each), `conceptChildren` (the pages under each page, by name: what the tree opens a level at a time), `canWords` ├── store-run.ts # the store's RUN FORM, its pure parts (plans/paid-runs.md §10 "The store"; tested): `priceWords` / `priceBadge` / `priceNotes` (free, fixed, metered, both; the reserve; what recent runs were charged), `runFields` (a workflow's `input:` block as the Run popover draws it — run-inputs.ts `bindingsFromInputBlock` — or a step's input JSON Schema), `inputOf` (the values as an input: empty left out, numbers and JSON read, a required one asked for), `httpCallResult` (a publisher's HTTP answer in the shape `callPaying` reads: 2xx its body, 402 `{ payment_required }`, else an error with its code and `Retry-After`), `newLaunchKey` (one per press), the runs a tab launched (`loadRuns` / `rememberRun`, `RUNS_KEPT`), `runFileUrl` (a run's file on its publisher, the run token as `?t=`), the sessionStorage keys per publisher ├── store-purse.ts # what the store page holds per publisher's server (browser glue over ./visitor-pay and ./store-run): `storePurse(base)` — the purse (credential, spares, invoices shown) and the runs launched there in this page's `sessionStorage` (`strut.store.purse:`, `strut.store.runs:`), one claim keeper per server resumed at load (`api.liveClaim`), one sheet at a time (`payInTurn`); `payWith` — show a challenge's sheet with its invoice claimed from that moment, a top-up replacing the credential it carries from ├── package-release.ts # the Packages tab's pure parts (plans/packages-ux.md): pickerGroups (the workflows a package may take, by category — an installed package's, a seed's and a service's left out), sidebarGroups / nameInGroup / installedTitle (the sidebar's groups: an installed package's workflows a group of its own, drawn from `GET /packages` and never written — its segment as the label, its version beside it, each workflow by its name inside the package), comesAlong (what a dry build carries beyond the picks, the picks it leaves to the host, and `needs` — its `leftToInstaller`, shown once as "Needs on the installing strut"), picksFromRelease (what a release held, as the names here; a Concept the release carried alone, `alone` on its items, picked alone again), packageLabel, nextVersion (the last number up by one), PACKAGE_NAME, PACKAGE_SEGMENT (tested) @@ -285,7 +285,7 @@ strut/ │ ├── AppHost.tsx # an app's page, hosted (plans/apps.md §7.3): the version's files listed (`useFileScope` mints the token), `dist/index.html` fetched under it (the server writes the app's identity into it), the TEXT framed as the srcdoc of `