Add Workspace base card (with test coverage) as the realm index card#5582
Add Workspace base card (with test coverage) as the realm index card#5582lukemelia wants to merge 2 commits into
Conversation
Workspace supersedes CardsGrid as a realm's index card: it bundles the CardsGrid experience under a Library tab and adds Home (setup progress, pinned entry points, README hero, space details), Activity (reverse-chron event feed), a Cmd+K Frame search, and an Edit settings format including realm publish/republish. Library delegates to the existing CardsGridLayout; the README field uses MarkdownDef; realm facts come from RealmInfo and the realm _types endpoint; publish wraps the host publish-realm tool with a typed PublishRealmInput. Setup-progress and remix jobs are discovered by querying the realm for ProcessCard/RemixCard via codeRef, so the card compiles without them and lights up once they exist. Accessibility, magic-number configurability, missing-RealmConfig fallback, prerender fast-path recognition, realm migration, and the full behavior test suite are handled separately; this change is the type-clean port plus a render smoke test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Preview deploymentsHost Test Results 1 files 1 suites 2h 57m 49s ⏱️ Results for commit fec6fa1. Realm Server Test Results 1 files ±0 1 suites ±0 13m 40s ⏱️ +10s Results for commit fec6fa1. ± Comparison against earlier commit 0619aa1. |
Export etaMinutes (with an injectable `now` for deterministic tests) and extract classifyActivityVerb from the Activity feed's inline verb logic, so both pure functions can be unit-tested directly. - unit: etaMinutes projection/edge cases and the >30-minute suppression; classifyActivityVerb created-vs-updated window. - integration: segment switching across the Home/Library/Activity tabs and presence of the Frame search input, via a bare render. - acceptance: a realm indexed by Workspace renders its shell in operator mode and switches segments, exercising the live realm-backed data paths. Publish/unpublish coverage is deferred until the realm-server recognizes Workspace adoption. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| if (!this.args.context?.store) { | ||
| return; | ||
| } | ||
| let processRef = codeRef(here, './process-card', 'ProcessCard'); |
There was a problem hiding this comment.
[Claude Code 🤖] The setup-progress feature is dead — these type refs resolve to nonexistent base-realm modules.
codeRef(here, './process-card', 'ProcessCard') and codeRef(here, './remix-card', 'RemixCard') resolve here (the base-realm module URL) against ./process-card / ./remix-card, i.e. <baseRealm>/process-card and <baseRealm>/remix-card. Neither module exists in packages/base — ProcessCard/RemixCard live only as host test fixtures / in other realms.
So loadJobs's query { any: [{ type: processRef }, { type: remixRef }] } never matches anything, and the whole setup-progress UI is inert: the "Setting up …" strip on Home, the collapsing progress dock on Activity, and the Activity-tab attention dot never render even while a real setup job runs in the realm.
Worse case: if the realm-server errors on an unresolvable type ref (rather than returning empty), loadFilterList/refreshOnIndex re-fire loadJobs on every incremental index and each throws. Please confirm the intended module location for these two card types.
| if (kind === 'file') { | ||
| fileTotal += summary.attributes.total ?? 0; | ||
| } else { | ||
| cardTotal += summary.attributes.total ?? 0; |
There was a problem hiding this comment.
[Claude Code 🤖] Rail counts are inflated because totals accumulate before the exclusion guards.
cardTotal += summary.attributes.total (and the fileTotal sibling) runs before the excludedTypeIds.includes(summary.id) and summary.id.endsWith('workspace/Workspace') guards a few lines down. So the Workspace index card itself (always present — it's the very card being rendered) plus any Cards Grid / CardDef instances get added into cardTotal.
But countFor returns cardTotal for the "Cards" row and cardTotal + fileTotal for "Everything", while those rows' queries exclude _cardType: 'Workspace' and _cardType: 'Cards Grid'. Net result: the sidebar shows e.g. "Everything (6)" / "Cards (6)" but the grid those rows open lists only 5 — off by at least one (the Workspace card counting itself) on every workspace. Move the += accumulation after the exclusion guards.
| type='button' | ||
| class='rail-row | ||
| {{if | ||
| (eq option.displayName this.activeFilter.displayName) |
There was a problem hiding this comment.
[Claude Code 🤖] Rail selection + countFor key on displayName, which collides with user type names.
Selected styling here uses (eq option.displayName this.activeFilter.displayName) (also at the card/file-type rows), and countFor switches on option.displayName — but selectFilter sets activeFilter by object identity. If a realm defines a user card/file type whose displayName happens to be Cards, Files, or Everything:
- both the library-group row and the user-type row highlight together when either is clicked, and
countForreturns the aggregatecardTotal/fileTotalfor the user type's row instead of that type's own count.
Key the selected check and countFor on the filter identity (or a stable id) rather than displayName.
| } | ||
| }); | ||
|
|
||
| private refreshOnIndex = (ev: RealmEventContent) => { |
There was a problem hiding this comment.
[Claude Code 🤖] (Plausible) refreshOnIndex reacts only to incremental index events, so full/copy reindexes leave the UI stale.
This guard runs loadFilterList / loadJobs / loadLatest / loadFeed only when ev.indexType === 'incremental'. After a full reindex or a realm copy/remix — which emit index events with a non-incremental indexType — the rail type list, counts, running-jobs dock, Home "latest" preview, and Activity feed stay stale until the user navigates away and back.
Note the primary user-triggered full-reindex path may be shielded: realm.ts broadcasts full-reindex invalidations via broadcastIncrementalInvalidationEvent (indexType incremental), so this only bites the true full/copy event paths. Worth confirming which reindex flows actually emit non-incremental events here and widening the guard to cover them.
| await loader.import<typeof CardsGridModule>('@cardstack/base/cards-grid') | ||
| ).CardsGrid; | ||
|
|
||
| Workspace = ( |
There was a problem hiding this comment.
[Claude Code 🤖] (Plausible) This unconditional import pulls the 3948-line workspace.gts graph into every setupBaseRealm test's beforeEach.
setupBaseRealm registers initialize() as a beforeEach, so this await loader.import('@cardstack/base/workspace') now runs for every rendering/integration test that uses the helper — synchronously loading workspace.gts plus its transitive graph (CardsGridLayout, several boxel-icons, BoxelInput, and the PublishRealmCommand host tool with its HostBaseTool/config chain) into the loader on each test, even tests that never touch Workspace.
The host suite is already documented to OOM shards and is gated by a memory-baseline check, so this added per-test retained module graph is a plausible trigger for the baseline check tripping red or a shard OOMing. Consider importing Workspace lazily (only in the tests that need it) rather than in the shared initialize().
| } | ||
| }); | ||
|
|
||
| private refreshOnIndex = (ev: RealmEventContent) => { |
There was a problem hiding this comment.
[Claude Code 🤖] (Efficiency) refreshOnIndex fans out to 3–4 full realm searches on every incremental index event.
Each incremental index event triggers loadFilterList (a _types fetch), loadJobs, loadLatest, and loadFeed (when Activity is open) — 3–4 independent realm round-trips per event. During active editing (which produces frequent incremental index events — e.g. an AI setup flow writing many cards) this multiplies index-event traffic 3–4×. The tasks are restartable, so this isn't a correctness bug, but consider debouncing/coalescing the refresh, or only refreshing the panels currently visible.
| return this.realms[0]; | ||
| } | ||
|
|
||
| private searchRealm = async (query: Query): Promise<CardDef[]> => { |
There was a problem hiding this comment.
[Claude Code 🤖] (Efficiency) searchRealm hard-forces page: { size: 100 }, so loadLatest hydrates up to 100 cards to read one title.
searchRealm spreads ...query then overrides page: { size: 100 }, ignoring any smaller page a caller passes. loadLatest only needs the single newest card (it sorts by lastModified desc and .find()s the first, reading just .cardTitle), but store.search hydrates up to 100 instances every time — on subscription setup and on every incremental index event. On a large realm that's 100 cards fetched and instantiated per index tick to display one line. Let callers pass their own page (default to 100 only when unspecified), and have loadLatest request size: 1.
| } | ||
| }; | ||
|
|
||
| get runningJobs() { |
There was a problem hiding this comment.
[Claude Code 🤖] (Efficiency) runningJobs is uncached and re-filters jobComponents on every template access.
get runningJobs() runs .filter over jobComponents on each read, and the isolated template reads this.runningJobs / this.runningJobs.length at ~6 sites plus dockSummary. Since jobComponents is a TrackedArray, every re-render re-runs the filter multiple times. The sibling chip getters (contentCardChips, fileChips, systemTypeCount) are all @cached — mark this one @cached too so it recomputes only when its tracked inputs change.
| } | ||
| }); | ||
|
|
||
| private loadFilterList = restartableTask(async () => { |
There was a problem hiding this comment.
[Claude Code 🤖] (Duplication) loadFilterList / setupRealmSubscription / teardownRealmSubscription / createCard are near-verbatim copies from cards-grid.gts.
These methods are near-identical to the same methods in packages/base/cards-grid.gts (loadFilterList 331–438, setupRealmSubscription 245–265, teardownRealmSubscription 239–243 — byte-for-byte identical, createCard 279–329). The _types fetch, error-construction block, excludedTypeIds list, kind grouping, and realm-subscription lifecycle now live in two places. Any fix to _types parsing or subscription teardown (e.g. a rolling-deploy compatibility change) has to be made twice, or one card silently diverges. Consider extracting a shared mixin/util.
| query: { | ||
| filter: { | ||
| every: [ | ||
| { not: { eq: { _cardType: 'Cards Grid' } } }, |
There was a problem hiding this comment.
[Claude Code 🤖] (Duplication) The Cards Grid / Workspace exclusion clause pair is repeated across 7 query sites.
This two-line { not: { eq: { _cardType: 'Cards Grid' } } } / 'Workspace' exclusion is repeated at 7 places (≈ lines 2301, 2406, 2456, 2650, 2818, 2853, 3233). If the Workspace displayName changes, or a third self-referential type needs hiding, all seven sites must change — miss one and the workspace/grid card leaks into the Library, search dropdown, Activity feed, or pin chooser. Extract a single excludeSystemCardsFilter (or const clause array) and spread it into each query.
What
Adds
Workspace(packages/base/workspace.gts), a base CardDef built to replaceCardsGridas a realm's index card, together with its tests.This PR lands the card and its tests only — it does not change which index card any realm uses. No realm-creation code or
index.jsonis touched here, so new realms still get aCardsGridindex and existing realms are unchanged. Making Workspace the default for newly-created realms (create-realm.ts) and migrating existing realms are separate changes (see Scope).The card bundles the existing CardsGrid experience under a Library tab and adds:
How it wires into existing infrastructure
CardsGridLayout(reusing itsVIEW_OPTIONS/SORT_OPTIONS/FilterOption), so the current grid behavior is reachable unchanged.readmefield usesMarkdownDef; realm facts come fromRealmInfoand the realm_typesendpoint; realm change notifications come fromsubscribeToRealm.publish-realmtool, invoked with a typedPublishRealmInputinstance.ProcessCard/RemixCardviacodeRef(not static imports), so this card compiles on its own and the Home job list populates once those base cards exist.Port notes
Ported from the proof-of-concept: dropped the edit-tracking header and the superscript annotation markers, removed opaque private design-doc references, converted absolute base-realm import URLs to relative, removed the unused first
VIEW_OPTIONSbinding, and replaced theas anycasts with real types. The current-module URL uses the sameimport.meta+@ts-ignoreidiom as the rest ofpackages/base.Tests
etaMinutes(exported, with an injectablenow) projection + every early-return + the >30-minute suppression;classifyActivityVerbacross the created-window boundary and missing timestamps.index.jsonadopts Workspace renders its shell in operator mode and switches segments, exercising the live realm-backed paths (subscribeToRealm, the_typesfetch, and the Activity feed).Scope — what makes Workspace the actual replacement (separate changes)
create-realm.tscurrently hardcodes theCardsGridindex card; that must switch to Workspace.index.jsonfiles off CardsGrid.RealmConfigfallback, and publish/unpublish test coverage.Test plan
glint (
ember-tsc --noEmit) clean over the full port;ember-template-lintand prettier clean. Run locally against a full dev stack, per module: unit 9 passed, integration segments 2 passed, render smoke 2 passed, acceptance 1 passed — 0 failed.🤖 Generated with Claude Code